🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

devframe

Package Overview
Dependencies
Maintainers
1
Versions
45
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

devframe - npm Package Compare versions

Comparing version
0.8.1
to
0.8.2
+157
dist/cac-D4_9ok0z.mjs
import { n as colors } from "./diagnostics-reporter-CsIG85Q5.mjs";
import { createBuild } from "./adapters/build.mjs";
import { n as resolveDevServerPort, t as createDevServer } from "./dev-Bc49u3qg.mjs";
import process from "node:process";
import cac$1 from "cac";
//#region src/adapters/flags.ts
/**
* Identity helper that preserves the literal schema-map type — use this
* so `InferCliFlags<typeof myFlags>` resolves to the right object shape.
*
* ```ts
* const appFlags = defineCliFlags({
* depth: v.pipe(v.number(), v.integer()),
* config: v.optional(v.string()),
* })
*
* defineDevframe({
* cli: { flags: appFlags },
* setup(ctx, info) {
* const flags = info.flags as InferCliFlags<typeof appFlags>
* flags.depth // number
* flags.config // string | undefined
* },
* })
* ```
*/
function defineCliFlags(flags) {
return flags;
}
/**
* Best-effort, dependency-free probe of a schema to decide whether the
* corresponding CAC option takes a value. Duck-types the `type` /
* `wrapped` / `inner` / `pipe` fields exposed by valibot and by devframe's
* built-in `s` builder, unwrapping `optional` / `nullable` / `nullish` /
* `pipe` wrappers then matching on the inner kind. Validators that don't
* expose these fields (e.g. zod) fall through to a value-taking option.
*/
function getSchemaKind(schema) {
let current = schema;
while (current) {
const kind = current.type;
if (kind === "optional" || kind === "nullable" || kind === "nullish" || kind === "undefined") {
current = current.wrapped ?? current.inner;
continue;
}
if (kind === "pipe" && Array.isArray(current.pipe) && current.pipe.length > 0) {
current = current.pipe[0];
continue;
}
return kind ?? "unknown";
}
return "unknown";
}
/** Whether the CAC option for this schema should be a boolean flag. */
function isBooleanFlag(schema) {
return getSchemaKind(schema) === "boolean";
}
/** Validate the raw cac-parsed bag against a {@link CliFlagsSchema}. */
function parseCliFlags(schema, raw) {
const flags = {};
const issues = [];
for (const [key, fieldSchema] of Object.entries(schema)) {
const result = fieldSchema["~standard"].validate(raw[key]);
if (result instanceof Promise) {
issues.push(`--${toKebab(key)}: async flag validation is not supported`);
continue;
}
if (result.issues) issues.push(`--${toKebab(key)}: ${result.issues.map((i) => i.message).join(", ")}`);
else flags[key] = result.value;
}
for (const [key, value] of Object.entries(raw)) if (!(key in schema) && !(key in flags)) flags[key] = value;
return issues.length ? {
flags,
issues
} : { flags };
}
function toKebab(camel) {
return camel.replaceAll(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
}
/** Kebab-case a schema key for CAC option registration. */
function flagKeyToOption(camel) {
return toKebab(camel);
}
//#endregion
//#region src/adapters/cac.ts
/**
* Wrap a {@link DevframeDefinition} in a `cac`-powered command-line
* interface exposing `dev` / `build` / `mcp` subcommands.
*
* Requires the optional `cac` peer dependency.
*/
function createCac(d, options = {}) {
const defaultPort = options.defaultPort ?? d.cli?.port ?? 9999;
const defaultHost = d.cli?.host ?? "localhost";
const cli = cac$1(d.cli?.command ?? d.id);
const devCommand = cli.command("[...args]", "Start a local dev server").option("--port <port>", "Port to listen on").option("--host <host>", "Host to bind to", { default: defaultHost }).option("--open", "Open the browser on start").option("--no-open", "Do not open the browser").option("--no-auth", "Disable the interactive authentication gate").option("--mcp", "Expose an MCP server over HTTP at /__mcp (use --no-mcp to disable) [experimental]");
if (d.cli?.flags) for (const [key, schema] of Object.entries(d.cli.flags)) {
const optionName = flagKeyToOption(key);
const description = schema.description ?? "";
if (isBooleanFlag(schema)) devCommand.option(`--${optionName}`, description);
else devCommand.option(`--${optionName} <value>`, description);
}
devCommand.action(async (_args, rawFlags) => {
const flags = resolveTypedFlags(d, rawFlags);
const host = flags.host ?? defaultHost;
const port = flags.port ?? await resolveDevServerPort(d, {
host,
defaultPort
});
const mcp = flags.mcp;
await createDevServer(d, {
host,
port,
flags,
mcp,
onReady: options.onReady
});
});
if (d.capabilities?.build !== false) cli.command("build", "Build a self-contained static deploy of the devframe").option("--out-dir <outDir>", "Output directory", { default: "dist-static" }).option("--base <base>", "URL base", { default: "/" }).option("--pretty", "Pretty-print dump JSON (larger on disk)").action(async (flags) => {
await createBuild(d, {
outDir: flags.outDir,
base: flags.base,
pretty: flags.pretty
});
});
cli.command("mcp", "Start an MCP server exposing agent-facing tools (stdio) [experimental]").action(async () => {
const { createMcpServer } = await import("./adapters/mcp.mjs");
await createMcpServer(d, {
transport: "stdio",
onReady: ({ transport }) => {
console.error(`[devframe] "${d.id}" MCP server ready (${transport})`);
}
});
});
d.cli?.configure?.(cli);
options.configureCli?.(cli);
cli.help();
cli.version("0.0.0");
return {
cli,
async parse(argv = process.argv) {
cli.parse(argv, { run: false });
await cli.runMatchedCommand();
}
};
}
function resolveTypedFlags(d, raw) {
if (!d.cli?.flags) return raw;
const { flags, issues } = parseCliFlags(d.cli.flags, raw);
if (issues?.length) {
for (const issue of issues) console.error(colors.red`[devframe] invalid flag — ${issue}`);
process.exit(1);
}
return flags;
}
//#endregion
export { defineCliFlags as n, parseCliFlags as r, createCac as t };
import { n as randomToken } from "./crypto-token-XCqTSMg9.mjs";
import { t as createStorage } from "./storage-CTNVbuFB.mjs";
import { n as revokeAuthToken, t as revokeActiveConnectionsForToken } from "./revoke-DZbJ7Cl0.mjs";
import { join } from "pathe";
//#region src/node/hub-internals/context.ts
const internalContextMap = /* @__PURE__ */ new WeakMap();
function getInternalContext(context) {
if (!internalContextMap.has(context)) {
const storage = createStorage({
filepath: join(context.host.getStorageDir("global"), "auth.json"),
initialValue: { trusted: {} }
});
const remoteTokens = /* @__PURE__ */ new Map();
function revokeRemoteToken(token) {
if (!remoteTokens.delete(token)) return;
revokeActiveConnectionsForToken(context, token);
}
const internalContext = {
storage: { auth: storage },
revokeAuthToken: (token) => revokeAuthToken(context, storage, token),
remoteTokens,
allocateRemoteToken(dockId, origin, originLock) {
const token = randomToken();
remoteTokens.set(token, {
dockId,
origin,
originLock
});
return token;
},
revokeRemoteToken,
revokeRemoteTokensForDock(dockId) {
const tokensToRevoke = [];
for (const [token, record] of remoteTokens) if (record.dockId === dockId) tokensToRevoke.push(token);
for (const token of tokensToRevoke) revokeRemoteToken(token);
},
isRemoteTokenTrusted(token, requestOrigin) {
const record = remoteTokens.get(token);
if (!record) return false;
if (!record.originLock) return true;
return !!requestOrigin && record.origin === requestOrigin;
}
};
internalContextMap.set(context, internalContext);
}
return internalContextMap.get(context);
}
//#endregion
export { internalContextMap as n, getInternalContext as t };
import { b as DevframeNodeContext, ht as SharedState } from "./devframe-mQ4qZAuo.mjs";
//#region src/node/hub-internals/context.d.ts
interface InternalAnonymousAuthStorage {
trusted: Record<string, {
authToken: string;
ua: string;
origin: string;
timestamp: number;
} | undefined>;
}
interface RemoteTokenRecord {
dockId: string;
/** Dock URL origin — matched against WS handshake `Origin` header when `originLock` is on. */
origin: string;
originLock: boolean;
}
interface DevframeInternalContext {
storage: {
auth: SharedState<InternalAnonymousAuthStorage>;
};
/**
* Revoke an auth token: remove from storage and notify all connected clients
* using this token that they are no longer trusted.
*/
revokeAuthToken: (token: string) => Promise<void>;
/**
* Session-only tokens issued to remote-UI iframe docks. Not persisted —
* regenerated on every dev-server restart.
*/
remoteTokens: Map<string, RemoteTokenRecord>;
allocateRemoteToken: (dockId: string, origin: string, originLock: boolean) => string;
revokeRemoteToken: (token: string) => void;
revokeRemoteTokensForDock: (dockId: string) => void;
/**
* Returns true if `token` is a valid remote token and, when `originLock` is
* on, `requestOrigin` matches the recorded dock origin.
*/
isRemoteTokenTrusted: (token: string, requestOrigin?: string) => boolean;
/**
* Populated by `createWsServer` once the WS port is bound. Consumed by the
* docks host when enriching remote iframe URLs with a connection descriptor.
*/
wsEndpoint?: {
/** Full `ws://` or `wss://` URL with host and port. */
url: string;
};
}
declare const internalContextMap: WeakMap<DevframeNodeContext, DevframeInternalContext>;
declare function getInternalContext(context: DevframeNodeContext): DevframeInternalContext;
//#endregion
export { internalContextMap as a, getInternalContext as i, InternalAnonymousAuthStorage as n, RemoteTokenRecord as r, DevframeInternalContext as t };
import { DEVFRAME_CONNECTION_META_FILENAME } from "./constants.mjs";
import { n as createHostContext, t as createH3DevframeHost } from "./host-h3-D2hYZvDK.mjs";
import { t as diagnostics } from "./diagnostics-D7wM2cwM.mjs";
import { r as registerDevframeInstance } from "./instance-registry-BnnA2Eoh.mjs";
import { i as normalizeHttpServerUrl, t as startHttpAndWs } from "./server-CD8XmBXl.mjs";
import { n as resolveBasePath, t as normalizeBasePath } from "./_shared-bWRzeSa0.mjs";
import { t as open } from "./open-Deb5xmIT.mjs";
import { mountStaticHandler } from "./utils/serve-static.mjs";
import { createInteractiveAuth } from "./recipes/interactive-auth.mjs";
import { resolve } from "pathe";
import process$1 from "node:process";
import { networkInterfaces } from "node:os";
import { H3 } from "h3";
import { createServer } from "node:net";
import { joinURL, withBase, withLeadingSlash, withoutLeadingSlash } from "ufo";
//#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
//#region src/adapters/dev.ts
const DEFAULT_PORT = 9999;
/**
* Resolve the listening port for {@link createDevServer}, honoring the
* definition's `cli.port` / `cli.portRange` / `cli.random` settings.
* Exposed separately so authors who run their own argv parsing can
* resolve a port up-front (to print it, log it, etc.) before starting
* the server.
*/
async function resolveDevServerPort(def, options = {}) {
const host = options.host ?? def.cli?.host ?? "localhost";
const portOptions = {
port: options.defaultPort ?? def.cli?.port ?? DEFAULT_PORT,
host
};
if (def.cli?.portRange) portOptions.portRange = def.cli.portRange;
if (def.cli?.random) portOptions.random = def.cli.random;
return getPort(portOptions);
}
/**
* Start a devframe dev server for a {@link DevframeDefinition} —
* h3 + WebSocket RPC + (optionally) the author's SPA mounted at the
* resolved base path.
*
* When `distDir` is omitted (and `def.cli?.distDir` is unset) the
* server runs in **bridge mode**: only `__connection.json` and the WS
* endpoint are mounted, with no SPA mount. The SPA is expected to be
* hosted elsewhere (e.g. by a parent Vite/Nuxt dev server) — see
* `viteDevBridge({ devMiddleware })`.
*
* Returns the underlying {@link StartedServer} handle so callers can
* close it gracefully (SIGINT, hot-reload, test teardown).
*
* Use this directly when integrating devframe into an existing CLI
* framework (commander, yargs, hand-rolled CAC). For the all-in-one
* `dev` / `build` / `mcp` shell, reach for {@link createCac} instead.
*/
async function createDevServer(def, options = {}) {
const distDir = options.distDir ?? def.cli?.distDir;
const host = options.host ?? def.cli?.host ?? "localhost";
const port = options.port ?? await resolveDevServerPort(def, { host });
const flags = options.flags ?? {};
const basePath = options.basePath ? normalizeBasePath(options.basePath) : resolveBasePath(def, "standalone");
const app = options.app ?? new H3();
const h3Host = createH3DevframeHost({
origin: normalizeHttpServerUrl(host, port),
appName: def.id,
mount: (base, dir) => {
mountStaticHandler(app, base, dir);
}
});
const ctx = await createHostContext({
cwd: process$1.cwd(),
mode: "dev",
host: h3Host
});
const setupInfo = { flags };
await def.setup(ctx, setupInfo);
const mcpConfig = resolveMcpConfig(options.mcp ?? def.cli?.mcp);
let mcpDispose;
let mcpMeta;
if (mcpConfig) {
const mcpRoute = withoutLeadingSlash(mcpConfig.path ?? "__mcp");
const mcpPath = joinURL(basePath, mcpRoute);
let mountMcpHttp;
try {
({mountMcpHttp} = await import("./http-C4T68YV6.mjs"));
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw diagnostics.DF0017({
transport: "http",
reason,
cause: error
});
}
mcpDispose = mountMcpHttp(app, ctx, mcpPath, {
serverName: `${def.id} (devframe)`,
serverVersion: def.version ?? "0.0.0",
exposeSharedState: true,
allowedOrigins: mcpConfig.allowedOrigins
}).dispose;
mcpMeta = { path: mcpRoute };
}
const { bindPath, wsPort, meta } = resolveWsConnection(def, options, basePath);
const connectionMetaPath = joinURL(basePath, DEVFRAME_CONNECTION_META_FILENAME);
app.use(connectionMetaPath, () => ({
backend: "websocket",
websocket: meta,
...mcpMeta ? { mcp: mcpMeta } : {}
}));
if (distDir) mountStaticHandler(app, basePath, resolve(distDir));
const authOption = flags.auth === false ? false : options.auth !== void 0 ? options.auth : def.cli?.auth;
let authHandler;
let resolvedAuth;
if (authOption === false) resolvedAuth = false;
else if (typeof authOption === "object") {
authHandler = authOption;
resolvedAuth = authOption;
} else {
authHandler = createInteractiveAuth(ctx);
resolvedAuth = authHandler;
}
const started = await startHttpAndWs({
context: ctx,
host,
port,
app,
path: bindPath,
wsPort,
auth: resolvedAuth,
onPeerConnect: options.onPeerConnect,
onPeerDisconnect: options.onPeerDisconnect,
onReady: async (info) => {
authHandler?.printBanner();
await options.onReady?.(info);
await maybeOpenBrowser(def, flags, `${info.origin}${basePath}`, options.openBrowser, authHandler);
}
});
const registration = registerDevframeInstance({
pid: process$1.pid,
port: started.port,
origin: normalizeHttpServerUrl(host, started.port),
basePath,
id: def.id,
name: def.name,
rootDir: process$1.cwd(),
mcp: mcpConfig ? { path: joinURL(basePath, withoutLeadingSlash(mcpConfig.path ?? "__mcp")) } : null,
startedAt: Date.now()
});
const closeServer = started.close;
started.close = async () => {
registration.unregister();
await mcpDispose?.();
await closeServer();
};
return started;
}
/**
* Normalize the `cli.mcp` / `mcp` option (`boolean | McpRouteOptions`) into
* concrete options, or `undefined` when the MCP route is disabled.
*/
function resolveMcpConfig(mcp) {
if (!mcp) return void 0;
return mcp === true ? {} : mcp;
}
/**
* Resolve the `mcp` entry a `__connection.json` should advertise for a dev
* server started with the given `mcp` option (falling back to `def.cli?.mcp`,
* exactly like {@link createDevServer}), or `undefined` when the route is
* disabled.
*
* Hosted bridges that hand-roll their connection meta (`viteDevBridge`,
* `@devframes/next`'s handler) pass the side-car `port`: the advertised path
* becomes absolute (the side-car mounts at `/`) and the client dials
* `<page-host>:<port><path>`. Without `port` the path stays relative, resolved
* against `__connection.json`'s own location (the same-server default).
*
* @experimental
*/
function resolveMcpConnectionMeta(def, mcp, port) {
const config = resolveMcpConfig(mcp ?? def.cli?.mcp);
if (!config) return void 0;
const route = withoutLeadingSlash(config.path ?? "__mcp");
return port != null ? {
path: withLeadingSlash(route),
port
} : { path: route };
}
/**
* Resolve the three WS connection scenarios from the definition / call-site
* config into a concrete server bind path, optional dedicated port, and the
* `__connection.json` descriptor the browser resolves.
*/
function resolveWsConnection(def, options, basePath) {
const ws = options.ws ?? def.cli?.ws ?? {};
const route = withoutLeadingSlash(ws.route ?? "__devframe_ws");
if (ws.url) return {
bindPath: joinURL(basePath, route),
wsPort: void 0,
meta: ws.url
};
if (ws.port != null) return {
bindPath: withLeadingSlash(route),
wsPort: ws.port,
meta: {
port: ws.port,
path: route
}
};
return {
bindPath: joinURL(basePath, route),
wsPort: void 0,
meta: { path: route }
};
}
async function maybeOpenBrowser(def, flags, origin, override, authHandler) {
const flagsOpen = flags.open;
const cliOpen = def.cli?.open;
const resolved = override ?? flagsOpen ?? cliOpen;
if (resolved === void 0 || resolved === false) return;
const target = typeof resolved === "string" ? withBase(resolved, origin) : origin;
const authorizedTarget = authHandler?.buildOpenUrl?.(target) ?? target;
try {
await open(authorizedTarget);
} catch {}
}
//#endregion
export { resolveDevServerPort as n, resolveMcpConnectionMeta as r, createDevServer as t };

Sorry, the diff of this file is too big to display

import { t as devframeReporter } from "./diagnostics-reporter-CsIG85Q5.mjs";
import { defineDiagnostics } from "nostics";
//#region src/node/diagnostics.ts
const diagnostics = defineDiagnostics({
docsBase: "https://devfra.me/errors",
reporters: [devframeReporter],
codes: {
DF0006: { why: (p) => `RPC function "${p.name}" is not registered` },
DF0007: { why: "AsyncLocalStorage is not set, it likely to be an internal bug of the Devframe foundation" },
DF0008: { why: (p) => `distDir ${p.distDir} does not exist` },
DF0012: { why: (p) => `Failed to parse storage file: ${p.filepath}, falling back to defaults.` },
DF0013: { why: (p) => `Shared state of "${p.key}" is not found, please provide an initial value for the first time` },
DF0014: {
why: (p) => `RPC function "${p.name}" has an invalid \`agent\` field — \`description\` must be a non-empty string.`,
fix: "Provide a short description (~1–3 sentences) explaining what the tool does and when agents should invoke it."
},
DF0015: {
why: (p) => `Agent tool "${p.id}" is already registered.`,
fix: "Tool ids must be unique across RPC functions with an `agent` field and tools registered via `ctx.agent.registerTool()`."
},
DF0016: { why: (p) => `Agent resource "${p.id}" is already registered.` },
DF0017: { why: (p) => `Failed to start MCP server (${p.transport}): ${p.reason}` },
DF0029: {
why: (p) => `Stream "${p.channel}#${p.id}" dropped ${p.dropped} chunk(s) after exceeding the client high-water mark.`,
fix: "The consumer is too slow for the producer. Raise `highWaterMark` on the subscription, slow the producer, or batch chunks."
},
DF0030: {
why: (p) => `Stream "${p.channel}#${p.id}" is unknown — no producer has called \`channel.start({ id: "${p.id}" })\`.`,
fix: "Ensure the server-side producer is running before clients subscribe, or check for typos in the stream id."
},
DF0031: {
why: (p) => `Cannot write to closed stream "${p.channel}#${p.id}".`,
fix: "Track the producer lifecycle — guard writes with the `stream.signal.aborted` flag."
},
DF0032: {
why: (p) => `Streaming channel "${p.channel}" is already registered.`,
fix: "Each channel name must be unique within a context. Pick a different name or reuse the existing channel handle."
},
DF0033: {
why: (p) => `Failed to start dev RPC bridge for "${p.id}": ${p.reason}`,
fix: "Verify the bridge port is free and the devframe setup function does not throw. Pin a port via `cli.port` / `cli.portRange` on the definition, or via `devMiddleware.port` on `viteDevBridge`."
},
DF0034: {
why: (p) => `Scoped RPC registration for namespace "${p.namespace}" received an already-namespaced function name "${p.name}".`,
fix: "A scoped context auto-namespaces ids. Pass a bare name without a \":\" separator (e.g. `register({ name: \"get-cwd\" })`), or use the unscoped `ctx.base.rpc.register` for a fully-qualified name."
},
DF0035: {
why: (p) => `Failed to persist storage file: ${p.filepath}`,
fix: "Check that the storage directory is writable and has free space."
},
DF0036: {
why: (p) => `RPC call to "${p.name}" was rejected: the caller is not authorized.`,
fix: "Complete the auth handshake (or connect with a static/pre-shared token) before calling a trusted method. Untrusted callers may only call `anonymous:`-prefixed methods — see `isAnonymousRpcMethod`."
},
DF0037: {
why: (p) => `A service is already provided under "${p.id}".`,
fix: "Service ids are unique per context. Revoke the existing provider first (the `provide()` call returns a revoke function), or namespace the id with your plugin id to avoid collisions."
},
DF0042: {
why: (p) => `"${p.id}" declares \`capabilities.build: false\` — its static export is not meaningful (writes are excluded and any live-served data won't be there).`,
fix: "Pass `{ force: true }` to `createBuild()` if the degraded export is still useful to you, or drop `capabilities.build: false` on the definition."
},
DF0045: {
why: (p) => `Failed to update the devframe instance registry at "${p.file}": ${p.reason}`,
fix: "Discovery tooling (`devframe connect`) will not see this instance. Check that the registry directory is writable, point `DEVFRAME_INSTANCES_DIR` at a writable directory, or set `DEVFRAME_DISABLE_INSTANCE_REGISTRY=1` to opt out of registration."
},
DF0046: {
why: (p) => `\`devframe connect\` requires the optional peer dependency @modelcontextprotocol/server: ${p.reason}`,
fix: "Install it next to devframe (e.g. `npm install @modelcontextprotocol/server`) and run `devframe connect` again."
},
DF0047: {
why: (p) => `Agent tool "${p.id}" is hidden from the MCP surface: its wire name "${p.name}" collides with the tool "${p.existing}".`,
fix: "Wire names derive from tool ids (characters outside [a-zA-Z0-9_-] become \"_\"). Rename one of the two ids so they sanitize to distinct names."
},
DF0048: {
why: (p) => `Unknown shared-state key "${p.key}".`,
fix: "Call the devframe_state_read tool without arguments to list the available keys, then retry with one of them."
},
DF0049: {
why: "The devframe_connect_call-tool tool requires { port: number, tool: string }.",
fix: "Call devframe_connect_list-instances to get the port and tool names, then retry."
},
DF0050: {
why: (p) => `No running devframe instance on port ${p.port}.`,
fix: "Call devframe_connect_list-instances for the current instance list — the instance may have stopped or changed port."
},
DF0051: {
why: (p) => `The devframe instance on port ${p.port} has no MCP endpoint.`,
fix: "Restart the instance with the --mcp flag (or set `cli.mcp: true` on its definition) to expose its tools, then list instances again."
},
DF0052: {
why: (p) => `Failed to listen on ${p.host}:${p.port}: ${p.reason}`,
fix: "The port is likely already taken by another process (often a previous devframe instance). Free it, or pick another via `--port`, `cli.port` / `cli.portRange` on the definition, or `devMiddleware.port` on `viteDevBridge`. The original node error is available as `error.cause`."
}
}
});
//#endregion
export { diagnostics as t };
import { isAllowedOrigin } from "./rpc/transports/ws-server.mjs";
import { n as createHostContext } from "./host-h3-D2hYZvDK.mjs";
import { t as diagnostics } from "./diagnostics-D7wM2cwM.mjs";
import { t as toAgentToolName } from "./agent-tool-name-C3b5vEwJ.mjs";
import { randomUUID } from "node:crypto";
import { Diagnostic } from "nostics";
import { join } from "pathe";
import process from "node:process";
import { homedir } from "node:os";
import { Server, WebStandardStreamableHTTPServerTransport, isInitializeRequest } from "@modelcontextprotocol/server";
//#region src/adapters/mcp/stringify.ts
/**
* JSON-coercing serializer for MCP text payloads.
*
* MCP carries tool results and resource reads as plain text over a
* JSON-RPC transport, so we cannot use the `s:`-prefixed structured-clone
* format the WS RPC transport falls back to for non-JSON values. Instead,
* we coerce common non-JSON types into JSON-friendly forms so the LLM
* client sees something useful instead of `[object Object]`.
*
* Coercions:
* - `BigInt` → `"123n"`
* - `Date` → ISO string (via the native `toJSON`)
* - `Map` → `{ __type: 'Map', entries: [[k, v], …] }`
* - `Set` → `{ __type: 'Set', entries: [v, …] }`
* - `Error` → `{ name, message, stack, cause? }` (cause recurses)
* - `Function` → `"[Function: name]"`
* - `Symbol` → `value.toString()`
* - cycles → `"[Circular]"`
*/
function stringifyForMcp(value) {
if (value === void 0) return "undefined";
if (typeof value === "string") return value;
const seen = /* @__PURE__ */ new WeakSet();
return JSON.stringify(value, (_key, val) => {
if (typeof val === "bigint") return `${val}n`;
if (val instanceof Error) {
const out = {
name: val.name,
message: val.message,
stack: val.stack
};
if (val.cause !== void 0) out.cause = val.cause;
return out;
}
if (val instanceof Map) return {
__type: "Map",
entries: [...val.entries()]
};
if (val instanceof Set) return {
__type: "Set",
entries: [...val]
};
if (typeof val === "function") return `[Function: ${val.name || "anonymous"}]`;
if (typeof val === "symbol") return val.toString();
if (val !== null && typeof val === "object") {
if (seen.has(val)) return "[Circular]";
seen.add(val);
}
return val;
}, 2);
}
/**
* Format a thrown value for an MCP `isError` text payload.
*
* A nostics `Diagnostic` (every coded devframe error) becomes structured
* JSON — `{ error: { code, message, fix?, docs? } }` — so an agent receives
* the actionable next step (`fix`) and the docs URL instead of a bare
* message string. Other errors surface `Error.name`/`message`, plus one
* level of `cause.message` so context isn't dropped silently.
*/
function formatMcpError(error) {
if (error instanceof Diagnostic) return JSON.stringify({ error: {
code: error.code,
message: error.message,
...error.fix ? { fix: error.fix } : {},
...error.docs ? { docs: error.docs } : {}
} }, null, 2);
if (!(error instanceof Error)) return String(error);
const cause = error.cause;
const causeText = cause instanceof Error ? ` (cause: ${cause.message})` : cause !== void 0 ? ` (cause: ${String(cause)})` : "";
return `${error.name}: ${error.message}${causeText}`;
}
//#endregion
//#region src/adapters/mcp/to-json-schema.ts
const FALLBACK_OBJECT_SCHEMA = Object.freeze({
type: "object",
additionalProperties: true
});
/**
* Convert a Standard Schema to JSON Schema for the agent/MCP surface.
*
* Devframe stays validator-neutral, so conversion uses the schema's own
* [Standard JSON Schema](https://standardschema.dev/) converter
* (`~standard.jsonSchema`) when the validator provides one — zod 4 does,
* for example. Validators without a native converter (e.g. valibot) degrade
* to a permissive object schema rather than pulling in a converter library.
*/
function safeToJsonSchema(schema) {
const standard = schema["~standard"];
if (standard.jsonSchema) try {
return standard.jsonSchema.input({ target: "draft-2020-12" });
} catch {
return FALLBACK_OBJECT_SCHEMA;
}
return FALLBACK_OBJECT_SCHEMA;
}
/**
* JSON Schema for an RPC return value on the agent/MCP surface.
* @internal
*/
function returnToJsonSchema(schema) {
if (!schema) return void 0;
return safeToJsonSchema(schema);
}
/**
* JSON Schema for an RPC function's positional args on the agent/MCP
* surface. Each positional arg is advertised under `arg0` / `arg1` / … —
* matching how the agent bridge coerces the incoming object payload back
* into positional arguments.
*
* Returns `{ type: 'object', properties: {} }` when there are no args.
* @internal
*/
function argsToJsonSchema(args) {
if (!args || args.length === 0) return {
schema: {
type: "object",
properties: {}
},
unwrapped: false
};
const properties = {};
const required = [];
for (let i = 0; i < args.length; i++) {
const key = `arg${i}`;
properties[key] = safeToJsonSchema(args[i]);
required.push(key);
}
return {
schema: {
type: "object",
properties,
required,
additionalProperties: false
},
unwrapped: false
};
}
//#endregion
//#region src/adapters/mcp/build-server.ts
/**
* Wire an MCP {@link Server} to a devframe context. Returns the server
* plus a disposal function for the subscriptions it sets up. The
* transport is the caller's responsibility — `createMcpServer` connects
* stdio; tests can connect an {@link InMemoryTransport} instead.
*
* @internal
*/
function buildMcpServerFromContext(ctx, options) {
const server = new Server({
name: options.serverName,
version: options.serverVersion
}, { capabilities: {
tools: { listChanged: true },
resources: { listChanged: true }
} });
registerToolHandlers(server, ctx, options.exposeSharedState);
registerResourceHandlers(server, ctx, options.exposeSharedState);
const notify = (method) => {
server.notification({ method }).catch(() => {});
};
const offManifest = ctx.agent.events.on("agent:manifest:changed", () => {
notify("notifications/tools/list_changed");
notify("notifications/resources/list_changed");
});
const offKeyAdded = ctx.rpc.sharedState.onKeyAdded(() => {
notify("notifications/resources/list_changed");
});
return {
server,
dispose: () => {
offManifest();
offKeyAdded();
}
};
}
/**
* Build an MCP server over the agent surface of a devframe definition.
* Currently supports `stdio` transport only.
*
* @experimental The agent-native surface is experimental and may change
* without a major version bump until it stabilizes.
*/
async function createMcpServer(definition, options = {}) {
const transport = options.transport ?? "stdio";
if (transport !== "stdio") throw diagnostics.DF0017({
transport,
reason: "Only stdio transport is supported in this release."
});
const ctx = await createHostContext({
cwd: process.cwd(),
mode: "dev",
host: {
mountStatic: () => {},
resolveOrigin: () => "mcp://devframe",
getStorageDir: (scope) => {
if (scope === "workspace") return join(process.cwd(), ".devframe");
if (scope === "project") return join(process.cwd(), `node_modules/.${definition.id}/devframe`);
return join(homedir(), `.${definition.id}/devframe`);
}
}
});
await definition.setup(ctx);
const { server, dispose } = buildMcpServerFromContext(ctx, {
serverName: options.serverName ?? `${definition.id} (devframe)`,
serverVersion: options.serverVersion ?? definition.version ?? "0.0.0",
exposeSharedState: options.exposeSharedState ?? true
});
const { startStdioTransport } = await import("./transports-vhizgqXM.mjs");
let stop;
try {
stop = await startStdioTransport(server);
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw diagnostics.DF0017({
transport,
reason,
cause: error
});
}
options.onReady?.({ transport: "stdio" });
return { async stop() {
dispose();
await stop();
} };
}
/**
* Id of the built-in shared-state read tool — namespaced like every other
* built-in (`devframe:<area>:<fn>`). Tool-shaped access matters because many
* MCP clients only consume tools — the parallel `devframe://state/<key>`
* resource projection stays for the clients that do read resources.
*/
const READ_STATE_TOOL = "devframe:state:read";
/** Wire name of the built-in shared-state read tool: `devframe_state_read`. */
const READ_STATE_NAME = toAgentToolName(READ_STATE_TOOL);
function sharedStateFilter(exposeSharedState) {
if (exposeSharedState === false) return void 0;
return typeof exposeSharedState === "function" ? exposeSharedState : () => true;
}
function readStateToolProjection() {
return {
name: READ_STATE_NAME,
title: "Read shared state",
description: "Read this devtool's live shared state. Call without arguments to list the available keys, then with a key to get that value as JSON. Safe to call freely.",
inputSchema: {
type: "object",
properties: { key: {
type: "string",
description: "A shared-state key from the key list. Omit to list all keys."
} }
},
annotations: {
title: "Read shared state",
readOnlyHint: true,
destructiveHint: false
}
};
}
async function readStateResult(ctx, filter, key) {
const keys = ctx.rpc.sharedState.keys().filter(filter);
if (key === void 0) return { keys };
if (!keys.includes(key)) throw diagnostics.DF0048({ key });
return {
key,
value: (await ctx.rpc.sharedState.get(key)).value()
};
}
function registerToolHandlers(server, ctx, exposeSharedState) {
const stateFilter = sharedStateFilter(exposeSharedState);
const warnedCollisions = /* @__PURE__ */ new Set();
/**
* Resolve a wire tool name back to the registered {@link AgentTool}.
* Wire-name matching runs first, in manifest order — the same tool the
* list projection advertises under that name — with a raw-id fallback so
* a colon-namespaced id keeps working as a call name.
*/
const resolveTool = (name) => {
return ctx.agent.list().tools.find((tool) => toAgentToolName(tool.id) === name) ?? ctx.agent.getTool(name);
};
server.setRequestHandler("tools/list", async () => {
const byName = /* @__PURE__ */ new Map();
for (const tool of ctx.agent.list().tools) {
const name = toAgentToolName(tool.id);
const existing = byName.get(name);
if (existing) {
if (!warnedCollisions.has(`${name}|${tool.id}`)) {
warnedCollisions.add(`${name}|${tool.id}`);
diagnostics.DF0047({
name,
id: tool.id,
existing: existing.id
});
}
continue;
}
byName.set(name, tool);
}
const tools = [...byName.entries()].map(([name, tool]) => projectTool(name, tool, ctx));
if (stateFilter && !byName.has(READ_STATE_NAME)) tools.push(readStateToolProjection());
return { tools };
});
server.setRequestHandler("tools/call", async (request) => {
const { name, arguments: args } = request.params;
try {
const tool = resolveTool(name);
if (stateFilter && !tool && (name === READ_STATE_NAME || name === READ_STATE_TOOL)) {
const key = args?.key;
const result = await readStateResult(ctx, stateFilter, key);
return {
content: [{
type: "text",
text: stringifyForMcp(result)
}],
structuredContent: result
};
}
const outputSchema = tool ? usableOutputSchema(tool.outputSchema ?? computeOutputSchema(tool, ctx)) : void 0;
const result = await ctx.agent.invoke(tool?.id ?? name, args ?? {});
return {
content: [{
type: "text",
text: stringifyForMcp(result)
}],
...outputSchema ? { structuredContent: result } : {}
};
} catch (error) {
return {
isError: true,
content: [{
type: "text",
text: `Error invoking "${name}": ${formatMcpError(error)}`
}]
};
}
});
}
function registerResourceHandlers(server, ctx, exposeSharedState) {
server.setRequestHandler("resources/list", async () => {
const resources = ctx.agent.list().resources.map((resource) => ({
uri: resource.uri,
name: resource.name,
description: resource.description,
mimeType: resource.mimeType
}));
if (exposeSharedState !== false) {
const filter = typeof exposeSharedState === "function" ? exposeSharedState : () => true;
for (const key of ctx.rpc.sharedState.keys()) {
if (!filter(key)) continue;
resources.push({
uri: `devframe://state/${encodeURIComponent(key)}`,
name: key,
description: `Shared state: ${key}`,
mimeType: "application/json"
});
}
}
return { resources };
});
server.setRequestHandler("resources/read", async (request) => {
const { uri } = request.params;
const parsed = parseResourceUri(uri);
if (parsed.kind === "resource") {
const content = await ctx.agent.read(parsed.id);
return { contents: [{
uri,
mimeType: content.mimeType ?? "application/json",
text: content.text ?? stringifyForMcp(content.json)
}] };
}
if (parsed.kind === "state") return { contents: [{
uri,
mimeType: "application/json",
text: stringifyForMcp((await ctx.rpc.sharedState.get(parsed.key)).value())
}] };
throw new Error(`[devframe/mcp] unknown resource URI "${uri}"`);
});
}
/**
* MCP constrains a tool's `outputSchema` to a JSON Schema of `type:
* "object"` — clients (the SDK included) reject anything else. Non-object
* return schemas (e.g. a schema for `void` / a bare string) simply project
* no output schema; the text content still carries the result.
*/
function usableOutputSchema(schema) {
return schema && typeof schema === "object" && schema.type === "object" ? schema : void 0;
}
function projectTool(name, tool, ctx) {
const inputSchema = tool.inputSchema ?? computeInputSchema(tool, ctx);
const outputSchema = usableOutputSchema(tool.outputSchema ?? computeOutputSchema(tool, ctx));
return {
name,
title: tool.title,
description: tool.description,
inputSchema,
...outputSchema ? { outputSchema } : {},
annotations: {
title: tool.title,
readOnlyHint: tool.safety === "read",
destructiveHint: tool.safety === "destructive"
}
};
}
function computeInputSchema(tool, ctx) {
if (tool.kind === "tool") return argsToJsonSchema(tool.args).schema;
if (tool.kind !== "rpc" || !tool.rpcName) return {
type: "object",
properties: {}
};
const def = ctx.rpc.definitions.get(tool.rpcName);
if (!def) return {
type: "object",
properties: {}
};
const args = def.args;
return argsToJsonSchema(args).schema;
}
function computeOutputSchema(tool, ctx) {
if (tool.kind !== "rpc" || !tool.rpcName) return void 0;
const def = ctx.rpc.definitions.get(tool.rpcName);
if (!def) return void 0;
return returnToJsonSchema(def.returns);
}
function parseResourceUri(uri) {
const match = uri.match(/^devframe:\/\/(resource|state)\/(.+)$/);
if (!match) return { kind: "unknown" };
const [, kind, rest] = match;
const decoded = decodeURIComponent(rest);
if (kind === "resource") return {
kind: "resource",
id: decoded
};
return {
kind: "state",
key: decoded
};
}
//#endregion
//#region src/adapters/mcp/fetch.ts
/**
* Build a framework-agnostic MCP Streamable-HTTP endpoint over a devframe
* context: a web-standard `Request → Response` handler any host can mount —
* h3 (see `mountMcpHttp`), a Next.js App Router route, or any other
* fetch-shaped server.
*
* Each MCP session gets its own {@link WebStandardStreamableHTTPServerTransport}
* and MCP server (built from the shared, live `ctx` via
* `buildMcpServerFromContext`), correlated by the `Mcp-Session-Id` header: an
* `initialize` POST spins up a session; later requests route to it; a `DELETE`
* (or client disconnect) tears it down. The origin gate guards every request:
* loopback-default DNS-rebinding protection that — unlike the WS upgrade's
* `isAllowedOrigin` — also rejects `Origin`-less requests, so a route-based
* endpoint isn't reachable by an arbitrary local process.
*
* @experimental
*/
function createMcpFetchHandler(ctx, options) {
const sessions = /* @__PURE__ */ new Map();
const allowedOrigins = options.allowedOrigins;
function drop(sessionId) {
const session = sessions.get(sessionId);
if (!session) return;
sessions.delete(sessionId);
session.dispose();
}
async function createSession() {
let session;
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (id) => {
sessions.set(id, session);
},
onsessionclosed: (id) => {
drop(id);
}
});
const { server, dispose } = buildMcpServerFromContext(ctx, {
serverName: options.serverName,
serverVersion: options.serverVersion,
exposeSharedState: options.exposeSharedState
});
session = {
transport,
dispose: async () => {
dispose();
await server.close();
}
};
transport.onclose = () => {
if (transport.sessionId) drop(transport.sessionId);
};
await server.connect(transport);
return session;
}
async function handle(req) {
const origin = req.headers.get("origin") ?? void 0;
if (allowedOrigins !== false && (origin === void 0 || !isAllowedOrigin(origin, allowedOrigins ?? []))) return new Response("Forbidden: origin required", { status: 403 });
const sessionId = req.headers.get("mcp-session-id") ?? void 0;
let session = sessionId ? sessions.get(sessionId) : void 0;
if (!session && req.method === "POST") {
let body;
try {
body = await req.json();
} catch {
body = void 0;
}
if (!sessionId && isInitializeRequest(body)) session = await createSession();
else return new Response(sessionId ? "Not Found: unknown MCP session" : "Bad Request: no valid session ID and not an initialize request", { status: sessionId ? 404 : 400 });
return session.transport.handleRequest(req, { parsedBody: body });
}
if (!session) return new Response(sessionId ? "Not Found: unknown MCP session" : "Bad Request: missing MCP session ID", { status: sessionId ? 404 : 400 });
return session.transport.handleRequest(req);
}
return {
fetch: handle,
dispose: async () => {
const live = [...sessions.values()];
sessions.clear();
await Promise.all(live.map((session) => session.dispose()));
}
};
}
//#endregion
export { createMcpServer as n, createMcpFetchHandler as t };
import { t as devframeReporter } from "./diagnostics-reporter-CsIG85Q5.mjs";
import { t as diagnostics } from "./diagnostics-hwjXp_UV.mjs";
import { RpcFunctionsCollectorBase } from "./rpc/index.mjs";
import { defineRpcFunction } from "./index.mjs";
import { t as diagnostics$1 } from "./diagnostics-D7wM2cwM.mjs";
import { i as createEventEmitter, n as createSharedState, r as nanoid, t as createStorage } from "./storage-CTNVbuFB.mjs";
import { defineDiagnostics } from "nostics";
import { isatty } from "node:tty";
import { formatWithOptions, inspect } from "node:util";
import { existsSync } from "node:fs";
import { join } from "pathe";
import process$1 from "node:process";
import { homedir } from "node:os";
//#region src/node/agent-args.ts
/**
* Map the args payload an agent surface receives (MCP sends an object
* keyed `arg0`/`arg1`/…, matching the schema the adapter advertises) onto
* a handler's positional parameters. Shared by the agent host's RPC
* bridge and the hub's command-derived tools so the coercion cannot
* drift between them.
*
* - an array passes through as-is
* - `null`/`undefined` become a zero-argument call
* - with declared schemas, each schema reads its own `argN` key, in order
* - without schemas, `arg0`/`arg1`/… keys are collected when present
* - an empty object becomes a zero-argument call
* - anything else follows the {@link AgentArgsFallback}
*
* @experimental
*/
function coerceAgentPositionalArgs(args, schemas, fallback = "wrap") {
if (Array.isArray(args)) return args;
if (args === void 0 || args === null) return [];
if (typeof args === "object") {
const obj = args;
if (schemas && schemas.length) return schemas.map((_, i) => obj[`arg${i}`]);
if ("arg0" in obj) {
const out = [];
let i = 0;
while (`arg${i}` in obj) {
out.push(obj[`arg${i}`]);
i++;
}
return out;
}
if (Object.keys(obj).length === 0) return [];
}
return fallback === "drop" ? [] : [args];
}
//#endregion
//#region src/node/host-agent.ts
/**
* Framework-neutral host aggregating the agent-exposed surface of a
* devframe. Auto-discovers RPC functions with an `agent` field from
* `ctx.rpc.definitions`, and accepts plugin-registered tools /
* resources via `registerTool` / `registerResource`.
*
* @experimental
*/
var DevframeAgentHost = class {
context;
events = createEventEmitter();
tools = /* @__PURE__ */ new Map();
resources = /* @__PURE__ */ new Map();
providers = /* @__PURE__ */ new Set();
_rpcUnsubscribe;
constructor(context) {
this.context = context;
this._rpcUnsubscribe = context.rpc.onChanged(() => {
this.events.emit("agent:manifest:changed");
});
}
registerTool(input) {
this._validateToolId(input.id);
const tool = this._projectTool(input);
this.tools.set(tool.id, {
tool,
handler: input.handler
});
this.events.emit("agent:tool:registered", tool);
this.events.emit("agent:manifest:changed");
return { unregister: () => this.unregisterTool(tool.id) };
}
unregisterTool(id) {
const existed = this.tools.delete(id);
if (existed) {
this.events.emit("agent:tool:unregistered", id);
this.events.emit("agent:manifest:changed");
}
return existed;
}
registerToolProvider(provider) {
this.providers.add(provider);
this.events.emit("agent:manifest:changed");
const notifyChanged = () => {
if (this.providers.has(provider)) this.events.emit("agent:manifest:changed");
};
return {
notifyChanged,
unregister: () => {
if (this.providers.delete(provider)) this.events.emit("agent:manifest:changed");
}
};
}
registerResource(input) {
if (this.resources.has(input.id)) throw diagnostics$1.DF0016({ id: input.id });
const resource = {
id: input.id,
name: input.name,
description: input.description,
mimeType: input.mimeType ?? "application/json",
uri: input.uri ?? `devframe://resource/${encodeURIComponent(input.id)}`
};
this.resources.set(resource.id, {
resource,
read: input.read
});
this.events.emit("agent:resource:registered", resource);
this.events.emit("agent:manifest:changed");
return { unregister: () => this.unregisterResource(resource.id) };
}
unregisterResource(id) {
const existed = this.resources.delete(id);
if (existed) {
this.events.emit("agent:resource:unregistered", id);
this.events.emit("agent:manifest:changed");
}
return existed;
}
list() {
const rpcTools = this._collectRpcTools();
const plainTools = Array.from(this.tools.values()).map((t) => t.tool);
const resources = Array.from(this.resources.values()).map((r) => r.resource);
const seen = new Set([...rpcTools, ...plainTools].map((t) => t.id));
const providerTools = [];
for (const { tool } of this._collectProviderTools()) {
if (seen.has(tool.id)) continue;
seen.add(tool.id);
providerTools.push(tool);
}
return {
tools: [
...rpcTools,
...plainTools,
...providerTools
],
resources
};
}
getTool(id) {
const plain = this.tools.get(id);
if (plain) return plain.tool;
const rpc = this._collectRpcTools().find((t) => t.id === id);
if (rpc) return rpc;
return this._collectProviderTools().find((t) => t.tool.id === id)?.tool;
}
getResource(id) {
return this.resources.get(id)?.resource;
}
async invoke(id, args) {
const plain = this.tools.get(id);
if (plain?.handler) return await plain.handler(args);
const rpcDef = this._findRpcDefinition(id);
if (rpcDef) {
const positional = coerceAgentPositionalArgs(args, rpcDef.args, "wrap");
return await this.context.rpc.invokeLocal(id, ...positional);
}
const provided = this._collectProviderTools().find((t) => t.tool.id === id);
if (provided) return await provided.input.handler(args);
throw new Error(`[devframe/agent] tool "${id}" not found`);
}
async read(id) {
const entry = this.resources.get(id);
if (!entry) throw new Error(`[devframe/agent] resource "${id}" not found`);
return await entry.read();
}
/** @internal */
_dispose() {
this._rpcUnsubscribe?.();
this._rpcUnsubscribe = void 0;
}
_validateToolId(id) {
if (this.tools.has(id)) throw diagnostics$1.DF0015({ id });
if (this.context.rpc.definitions.get(id)?.agent) throw diagnostics$1.DF0015({ id });
}
_projectTool(input) {
if (!input.description || typeof input.description !== "string") throw diagnostics$1.DF0014({ name: input.id });
return {
id: input.id,
kind: "tool",
title: input.title ?? input.id,
description: input.description,
safety: input.safety ?? "action",
tags: input.tags,
args: input.args,
inputSchema: input.inputSchema,
outputSchema: input.outputSchema,
examples: input.examples
};
}
/** Query every registered provider, projecting inputs to serializable tools. */
_collectProviderTools() {
const out = [];
for (const provider of this.providers) for (const input of provider()) out.push({
input,
tool: this._projectTool(input)
});
return out;
}
_collectRpcTools() {
const out = [];
for (const [name, def] of this.context.rpc.definitions) {
const agent = def.agent;
if (!agent) continue;
if (!agent.description || typeof agent.description !== "string") throw diagnostics$1.DF0014({ name });
const type = def.type ?? "query";
const safety = agent.safety ?? inferSafety(type);
out.push({
id: name,
kind: "rpc",
title: agent.title ?? name,
description: agent.description,
safety,
tags: agent.tags,
rpcName: name,
examples: agent.examples
});
}
return out;
}
_findRpcDefinition(id) {
const def = this.context.rpc.definitions.get(id);
if (def?.agent) return def;
}
};
function inferSafety(type) {
if (type === "static" || type === "query") return "read";
return "action";
}
//#endregion
//#region src/node/host-diagnostics.ts
var DevframeDiagnosticsHost = class {
context;
_registry = {};
logger = new Proxy({}, { get: (_, code) => this._registry[code] });
defineDiagnostics = (opts) => {
return defineDiagnostics({
...opts,
reporters: [devframeReporter, ...opts.reporters ?? []]
});
};
constructor(context, initialDefinitions = []) {
this.context = context;
for (const d of initialDefinitions) this.register(d);
}
register(diagnostics) {
Object.assign(this._registry, diagnostics);
}
};
//#endregion
//#region ../../node_modules/.pnpm/obug@2.1.4/node_modules/obug/dist/core.js
/**
* Coerce `value`.
*/
function coerce(value) {
if (value instanceof Error) return value.stack || value.message;
return value;
}
/**
* Selects a color for a debug namespace
* @return An ANSI color code for the given namespace
*/
function selectColor(colors, namespace) {
let hash = 0;
for (let i = 0; i < namespace.length; i++) {
hash = (hash << 5) - hash + namespace.charCodeAt(i);
hash |= 0;
}
return colors[Math.abs(hash) % colors.length];
}
/**
* Checks if the given string matches a namespace template, honoring
* asterisks as wildcards.
*/
function matchesTemplate(search, template) {
let searchIndex = 0;
let templateIndex = 0;
let starIndex = -1;
let matchIndex = 0;
while (searchIndex < search.length) if (templateIndex < template.length && (template[templateIndex] === search[searchIndex] || template[templateIndex] === "*")) if (template[templateIndex] === "*") {
starIndex = templateIndex;
matchIndex = searchIndex;
templateIndex++;
} else {
searchIndex++;
templateIndex++;
}
else if (starIndex !== -1) {
templateIndex = starIndex + 1;
matchIndex++;
searchIndex = matchIndex;
} else return false;
while (templateIndex < template.length && template[templateIndex] === "*") templateIndex++;
return templateIndex === template.length;
}
function humanize(value) {
if (value >= 1e3) return `${(value / 1e3).toFixed(1)}s`;
return `${value}ms`;
}
let globalNamespaces = "";
function createDebug$1(namespace, options) {
let prevTime;
let enableOverride;
let namespacesCache;
let enabledCache;
const debug = (...args) => {
if (!debug.enabled) return;
const curr = Date.now();
const diff = curr - (prevTime || curr);
prevTime = curr;
args[0] = coerce(args[0]);
if (typeof args[0] !== "string") args.unshift("%O");
let index = 0;
args[0] = args[0].replace(/%([a-z%])/gi, (match, format) => {
if (match === "%%") return "%";
index++;
const formatter = options.formatters[format];
if (typeof formatter === "function") {
const value = args[index];
match = formatter.call(debug, value);
args.splice(index, 1);
index--;
}
return match;
});
options.formatArgs.call(debug, diff, args);
debug.log(...args);
};
debug.extend = function(namespace, delimiter = ":") {
return createDebug$1(this.namespace + delimiter + namespace, {
useColors: this.useColors,
color: this.color,
formatArgs: this.formatArgs,
formatters: this.formatters,
inspectOpts: this.inspectOpts,
log: this.log,
humanize: this.humanize
});
};
Object.assign(debug, options);
debug.namespace = namespace;
Object.defineProperty(debug, "enabled", {
enumerable: true,
configurable: false,
get: () => {
if (enableOverride != null) return enableOverride;
if (namespacesCache !== globalNamespaces) {
namespacesCache = globalNamespaces;
enabledCache = enabled(namespace);
}
return enabledCache;
},
set: (v) => {
enableOverride = v;
}
});
return debug;
}
let names = [];
let skips = [];
function enable(namespaces) {
globalNamespaces = namespaces;
names = [];
skips = [];
const split = globalNamespaces.trim().replace(/\s+/g, ",").split(",").filter(Boolean);
for (const ns of split) if (ns[0] === "-") skips.push(ns.slice(1));
else names.push(ns);
}
/**
* Returns true if the given mode name is enabled, false otherwise.
*/
function enabled(name) {
for (const skip of skips) if (matchesTemplate(name, skip)) return false;
for (const ns of names) if (matchesTemplate(name, ns)) return true;
return false;
}
//#endregion
//#region ../../node_modules/.pnpm/obug@2.1.4/node_modules/obug/dist/node.js
let env = {};
try {
process.env.DEBUG;
env = process.env;
} catch (_unused) {}
const colors = process.stderr.getColorDepth && process.stderr.getColorDepth(env) > 2 ? [
20,
21,
26,
27,
32,
33,
38,
39,
40,
41,
42,
43,
44,
45,
56,
57,
62,
63,
68,
69,
74,
75,
76,
77,
78,
79,
80,
81,
92,
93,
98,
99,
112,
113,
128,
129,
134,
135,
148,
149,
160,
161,
162,
163,
164,
165,
166,
167,
168,
169,
170,
171,
172,
173,
178,
179,
184,
185,
196,
197,
198,
199,
200,
201,
202,
203,
204,
205,
206,
207,
208,
209,
214,
215,
220,
221
] : [
6,
2,
3,
4,
5,
1
];
const inspectOpts = Object.keys(env).filter((key) => /^debug_/i.test(key)).reduce((obj, key) => {
const prop = key.slice(6).toLowerCase().replace(/_([a-z])/g, (_, k) => k.toUpperCase());
let value = env[key];
const lowerCase = typeof value === "string" && value.toLowerCase();
if (value === "null") value = null;
else if (lowerCase === "yes" || lowerCase === "on" || lowerCase === "true" || lowerCase === "enabled") value = true;
else if (lowerCase === "no" || lowerCase === "off" || lowerCase === "false" || lowerCase === "disabled") value = false;
else value = Number(value);
obj[prop] = value;
return obj;
}, Object.create(null));
/**
* Is stdout a TTY? Colored output is enabled when `true`.
*/
function useColors() {
return "colors" in inspectOpts ? Boolean(inspectOpts.colors) : isatty(process.stderr.fd);
}
function getDate() {
if (inspectOpts.hideDate) return "";
return `${(/* @__PURE__ */ new Date()).toISOString()} `;
}
/**
* Adds ANSI color escape codes if enabled.
*/
function formatArgs(diff, args) {
const { namespace: name, useColors } = this;
if (useColors) {
const c = this.color;
const colorCode = `\u001B[3${c < 8 ? c : `8;5;${c}`}`;
const prefix = ` ${colorCode};1m${name} \u001B[0m`;
args[0] = prefix + args[0].split("\n").join(`\n${prefix}`);
args.push(`${colorCode}m+${this.humanize(diff)}\u001B[0m`);
} else args[0] = `${getDate()}${name} ${args[0]}`;
}
function log(...args) {
process.stderr.write(`${formatWithOptions(this.inspectOpts, ...args)}\n`);
}
const defaultOptions = {
useColors: useColors(),
formatArgs,
formatters: {
/**
* Map %o to `util.inspect()`, all on a single line.
*/
o(v) {
this.inspectOpts.colors = this.useColors;
return inspect(v, this.inspectOpts).split("\n").map((str) => str.trim()).join(" ");
},
/**
* Map %O to `util.inspect()`, allowing multiple lines if needed.
*/
O(v) {
this.inspectOpts.colors = this.useColors;
return inspect(v, this.inspectOpts);
}
},
inspectOpts,
log,
humanize
};
function createDebug(namespace, options) {
var _ref;
const color = (_ref = options && options.color) !== null && _ref !== void 0 ? _ref : selectColor(colors, namespace);
return createDebug$1(namespace, Object.assign(defaultOptions, { color }, options));
}
enable(env.DEBUG || "");
//#endregion
//#region src/node/rpc-shared-state.ts
const debug$1 = createDebug("devframe:rpc:state:changed");
const debugSubscribe = createDebug("devframe:rpc:state:subscribe");
function createRpcSharedStateServerHost(rpc) {
const sharedState = /* @__PURE__ */ new Map();
const stateDisposers = /* @__PURE__ */ new Map();
const keyAddedListeners = /* @__PURE__ */ new Set();
function registerSharedState(key, state) {
const offs = [];
offs.push(state.on("updated", (fullState, patches, syncId) => {
if (patches) {
debug$1("patch", {
key,
syncId
});
rpc.broadcast({
method: "devframe:rpc:client-state:patch",
args: [
key,
patches,
syncId
],
filter: (client) => client.$meta.subscribedStates.has(key)
});
} else {
debug$1("updated", {
key,
syncId
});
rpc.broadcast({
method: "devframe:rpc:client-state:updated",
args: [
key,
fullState,
syncId
],
filter: (client) => client.$meta.subscribedStates.has(key)
});
}
}));
return () => {
for (const off of offs) off();
};
}
const host = {
get: async (key, options) => {
if (sharedState.has(key)) return sharedState.get(key);
if (options?.initialValue === void 0 && options?.sharedState === void 0) throw diagnostics$1.DF0013({ key });
debug$1("new-state", key);
const state = options.sharedState ?? createSharedState({
initialValue: options.initialValue,
enablePatches: false
});
stateDisposers.set(key, registerSharedState(key, state));
sharedState.set(key, state);
for (const fn of keyAddedListeners) fn(key);
return state;
},
keys() {
return Array.from(sharedState.keys());
},
onKeyAdded(fn) {
keyAddedListeners.add(fn);
return () => {
keyAddedListeners.delete(fn);
};
},
delete(key) {
const dispose = stateDisposers.get(key);
if (!dispose) return false;
dispose();
stateDisposers.delete(key);
sharedState.delete(key);
return true;
}
};
rpc.register({
name: "devframe:rpc:server-state:subscribe",
type: "event",
handler(key) {
const session = rpc.getCurrentRpcSession();
if (!session) return;
debugSubscribe("subscribe", {
key,
session: session.meta.id
});
session.meta.subscribedStates.add(key);
}
});
rpc.register({
name: "devframe:rpc:server-state:get",
type: "query",
handler: async (key) => {
if (!sharedState.has(key)) return void 0;
return (await host.get(key)).value();
},
dump: () => ({ inputs: host.keys().map((key) => [key]) })
});
rpc.register({
name: "devframe:rpc:server-state:set",
type: "query",
handler: async (key, value, syncId) => {
(await host.get(key, { initialValue: value })).mutate(() => value, syncId);
}
});
rpc.register({
name: "devframe:rpc:server-state:patch",
type: "query",
handler: async (key, patches, syncId) => {
if (!sharedState.has(key)) return;
(await host.get(key)).patch(patches, syncId);
}
});
return host;
}
//#endregion
//#region src/utils/streaming-channel.ts
const DEFAULT_HIGH_WATER_MARK = 256;
var StreamClosedError = class extends Error {
name = "StreamClosedError";
};
/**
* Build a server-side stream sink. RPC-agnostic — the RPC host wires
* `events.on('chunk' | 'end')` to broadcast, and reads `buffer` to replay
* for late or reconnecting subscribers.
*/
function createStreamSink(options = {}) {
const id = options.id ?? nanoid();
const replayWindow = Math.max(0, options.replayWindow ?? 0);
const events = createEventEmitter();
const controller = new AbortController();
const buffer = [];
let closed = false;
let lastSeq = 0;
function write(chunk) {
if (closed) throw new StreamClosedError(`Cannot write to a closed stream "${id}"`);
lastSeq += 1;
if (replayWindow > 0) {
buffer.push({
seq: lastSeq,
chunk
});
if (buffer.length > replayWindow) buffer.splice(0, buffer.length - replayWindow);
}
events.emit("chunk", lastSeq, chunk);
}
function error(reason) {
if (closed) return;
closed = true;
const payload = toErrorPayload(reason);
controller.abort(reason);
events.emit("end", payload);
}
function close() {
if (closed) return;
closed = true;
if (!controller.signal.aborted) controller.abort("stream closed");
events.emit("end", void 0);
}
function abort(reason) {
if (closed) return;
if (!controller.signal.aborted) controller.abort(reason ?? "aborted");
}
const writable = new WritableStream({
write(chunk) {
write(chunk);
},
close() {
close();
},
abort(reason) {
error(reason);
}
});
return {
id,
signal: controller.signal,
get closed() {
return closed;
},
get lastSeq() {
return lastSeq;
},
write,
error,
close,
abort,
writable,
events,
buffer
};
}
/**
* Build a client-side stream reader. RPC-agnostic — the RPC host calls
* `_push(seq, chunk)` on each incoming chunk and `_end(error?)` on the
* terminal frame. Consumers iterate with `for await` or pipe `readable`.
*/
function createStreamReader(options = {}) {
const id = options.id ?? nanoid();
const highWaterMark = Math.max(1, options.highWaterMark ?? DEFAULT_HIGH_WATER_MARK);
const queue = [];
let lastSeenSeq = 0;
let done = false;
let cancelled = false;
let endError;
let pending;
let pullController;
let readableInstance;
function drainNext() {
if (!pending) return;
if (queue.length > 0) {
const value = queue.shift();
const r = pending;
pending = void 0;
r.resolve({
value,
done: false
});
return;
}
if (done) {
const r = pending;
pending = void 0;
if (endError) {
const err = new Error(endError.message);
err.name = endError.name;
r.reject(err);
} else r.resolve({
value: void 0,
done: true
});
}
}
function feedReadable() {
if (!pullController) return;
while (queue.length > 0) {
const v = queue.shift();
try {
pullController.enqueue(v);
} catch {
break;
}
}
if (done && pullController) {
try {
if (endError) {
const err = new Error(endError.message);
err.name = endError.name;
pullController.error(err);
} else pullController.close();
} catch {}
pullController = void 0;
}
}
function push(seq, chunk) {
if (done || cancelled) return;
if (seq <= lastSeenSeq) return;
lastSeenSeq = seq;
queue.push(chunk);
if (queue.length > highWaterMark) {
const overflow = queue.length - highWaterMark;
queue.splice(0, overflow);
options.onOverflow?.(overflow);
}
drainNext();
if (readableInstance) feedReadable();
}
function end(error) {
if (done) return;
done = true;
endError = error;
drainNext();
if (readableInstance) feedReadable();
}
function cancel() {
if (cancelled || done) return;
cancelled = true;
options.onCancel?.();
end(void 0);
}
function getReadable() {
if (readableInstance) return readableInstance;
readableInstance = new ReadableStream({
start(controller) {
pullController = controller;
feedReadable();
},
cancel() {
cancel();
}
});
return readableInstance;
}
return {
id,
get cancelled() {
return cancelled;
},
get done() {
return done;
},
get lastSeenSeq() {
return lastSeenSeq;
},
get readable() {
return getReadable();
},
cancel,
_push: push,
_end: end,
[Symbol.asyncIterator]() {
return {
next() {
if (queue.length > 0) return Promise.resolve({
value: queue.shift(),
done: false
});
if (done) {
if (endError) {
const err = new Error(endError.message);
err.name = endError.name;
return Promise.reject(err);
}
return Promise.resolve({
value: void 0,
done: true
});
}
return new Promise((resolve, reject) => {
pending = {
resolve,
reject
};
});
},
return() {
cancel();
return Promise.resolve({
value: void 0,
done: true
});
}
};
}
};
}
function toErrorPayload(reason) {
if (reason instanceof Error) return {
name: reason.name || "Error",
message: reason.message
};
if (typeof reason === "string") return {
name: "Error",
message: reason
};
try {
return {
name: "Error",
message: JSON.stringify(reason)
};
} catch {
return {
name: "Error",
message: String(reason)
};
}
}
//#endregion
//#region src/node/rpc-streaming.ts
const debug = createDebug("devframe:rpc:streaming");
const STREAM_KEY_SEPARATOR = "";
function streamKey(channel, id) {
return `${channel}${STREAM_KEY_SEPARATOR}${id}`;
}
/**
* Build the server-side streaming host. Mirrors the layout of
* `createRpcSharedStateServerHost` — registers a fixed set of internal
* RPC methods (`subscribe` / `unsubscribe` / `cancel`) once, then per-channel
* state lives in a `Map<channelName, ChannelState>`.
*/
function createRpcStreamingServerHost(rpc) {
const channels = /* @__PURE__ */ new Map();
function findStream(channelName, id) {
return channels.get(channelName)?.streams.get(id);
}
function freeStreamNow(state, id) {
const record = state.streams.get(id);
if (!record) return;
if (record.retentionTimer) {
clearTimeout(record.retentionTimer);
record.retentionTimer = void 0;
}
for (const off of record.unbinders) off();
state.streams.delete(id);
debug("freed", state.name, id);
}
function maybeFreeStream(state, id) {
const record = state.streams.get(id);
if (!record) return;
if (!record.sink.closed || record.subscribers.size > 0) return;
const retention = state.options.closedStreamRetention;
if (retention <= 0) {
freeStreamNow(state, id);
return;
}
if (record.retentionTimer) return;
record.retentionTimer = setTimeout(freeStreamNow, retention, state, id);
}
function cancelRetention(record) {
if (record.retentionTimer) {
clearTimeout(record.retentionTimer);
record.retentionTimer = void 0;
}
}
rpc.register({
name: "devframe:streaming:subscribe",
type: "event",
handler(channelName, id, opts) {
const state = channels.get(channelName);
if (!state) {
diagnostics$1.DF0030({
channel: channelName,
id
}, { method: "error" });
return;
}
const record = state.streams.get(id);
if (!record) {
diagnostics$1.DF0030({
channel: channelName,
id
}, { method: "error" });
return;
}
const session = rpc.getCurrentRpcSession();
if (!session) return;
const key = streamKey(channelName, id);
session.meta.subscribedStreams ??= /* @__PURE__ */ new Set();
session.meta.subscribedStreams.add(key);
record.subscribers.add(session.meta);
cancelRetention(record);
const afterSeq = opts?.afterSeq ?? 0;
for (const buffered of record.sink.buffer) if (buffered.seq > afterSeq) rpc.broadcast({
method: "devframe:streaming:chunk",
args: [
channelName,
id,
buffered.seq,
buffered.chunk
],
event: true,
optional: true,
filter: (client) => client.$meta === session.meta
});
if (record.sink.closed) rpc.broadcast({
method: "devframe:streaming:end",
args: [
channelName,
id,
void 0
],
event: true,
optional: true,
filter: (client) => client.$meta === session.meta
});
}
});
rpc.register({
name: "devframe:streaming:unsubscribe",
type: "event",
handler(channelName, id) {
const state = channels.get(channelName);
const record = state?.streams.get(id);
const session = rpc.getCurrentRpcSession();
if (!session) return;
session.meta.subscribedStreams?.delete(streamKey(channelName, id));
if (state && record) {
record.subscribers.delete(session.meta);
maybeFreeStream(state, id);
}
}
});
rpc.register({
name: "devframe:streaming:cancel",
type: "event",
handler(channelName, id) {
const record = findStream(channelName, id);
if (!record) return;
const session = rpc.getCurrentRpcSession();
if (!session) return;
record.subscribers.delete(session.meta);
session.meta.subscribedStreams?.delete(streamKey(channelName, id));
if (record.subscribers.size === 0) record.sink.abort("cancelled by client");
}
});
rpc.register({
name: "devframe:streaming:upload-chunk",
type: "event",
handler(channelName, id, seq, chunk) {
const record = channels.get(channelName)?.inbound.get(id);
if (!record) {
diagnostics$1.DF0030({
channel: channelName,
id
}, { method: "error" });
return;
}
if (!record.uploaderMeta) {
const session = rpc.getCurrentRpcSession();
if (session) {
record.uploaderMeta = session.meta;
session.meta.uploadingStreams ??= /* @__PURE__ */ new Set();
session.meta.uploadingStreams.add(streamKey(channelName, id));
}
}
record.reader._push(seq, chunk);
}
});
rpc.register({
name: "devframe:streaming:upload-end",
type: "event",
handler(channelName, id, error) {
const state = channels.get(channelName);
const record = state?.inbound.get(id);
if (!record) return;
record.reader._end(error);
if (record.uploaderMeta) record.uploaderMeta.uploadingStreams?.delete(streamKey(channelName, id));
state?.inbound.delete(id);
}
});
function createChannel(name, opts = {}) {
if (channels.has(name)) throw diagnostics$1.DF0032({ channel: name });
const replayWindow = opts.replayWindow ?? 0;
const state = {
name,
options: {
replayWindow,
closedStreamRetention: opts.closedStreamRetention ?? (replayWindow > 0 ? 3e4 : 0)
},
streams: /* @__PURE__ */ new Map(),
inbound: /* @__PURE__ */ new Map()
};
channels.set(name, state);
function start(startOpts = {}) {
const sink = createStreamSink({
id: startOpts.id,
replayWindow: state.options.replayWindow
});
const record = {
sink,
subscribers: /* @__PURE__ */ new Set(),
unbinders: []
};
state.streams.set(sink.id, record);
record.unbinders.push(sink.events.on("chunk", (seq, chunk) => {
rpc.broadcast({
method: "devframe:streaming:chunk",
args: [
name,
sink.id,
seq,
chunk
],
event: true,
optional: true,
filter: (client) => record.subscribers.has(client.$meta)
});
}));
record.unbinders.push(sink.events.on("end", (error) => {
rpc.broadcast({
method: "devframe:streaming:end",
args: [
name,
sink.id,
error
],
event: true,
optional: true,
filter: (client) => record.subscribers.has(client.$meta)
});
maybeFreeStream(state, sink.id);
}));
return sink;
}
async function pipeFrom(readable, startOpts = {}) {
const sink = start(startOpts);
readable.pipeTo(sink.writable, { signal: sink.signal }).catch(() => {});
return sink;
}
function get(id) {
return state.streams.get(id)?.sink;
}
function ids() {
return Array.from(state.streams.keys());
}
function openInbound(inboundOpts = {}) {
let inboundRecord;
const reader = createStreamReader({
id: inboundOpts.id,
onCancel() {
const targetMeta = inboundRecord?.uploaderMeta;
if (!targetMeta) return;
rpc.broadcast({
method: "devframe:streaming:upload-cancel",
args: [name, reader.id],
event: true,
optional: true,
filter: (client) => client.$meta === targetMeta
});
}
});
inboundRecord = { reader };
state.inbound.set(reader.id, inboundRecord);
debug("opened-inbound", name, reader.id);
return reader;
}
return {
name,
start,
pipeFrom,
get,
ids,
openInbound
};
}
function parseKey(key) {
const sepIdx = key.indexOf(STREAM_KEY_SEPARATOR);
if (sepIdx < 0) return void 0;
return {
channelName: key.slice(0, sepIdx),
id: key.slice(sepIdx + 1)
};
}
return {
create: createChannel,
_onSessionDisconnected(meta) {
if (meta.subscribedStreams) {
for (const key of meta.subscribedStreams) {
const parsed = parseKey(key);
if (!parsed) continue;
const state = channels.get(parsed.channelName);
const record = state?.streams.get(parsed.id);
if (!state || !record) continue;
record.subscribers.delete(meta);
if (record.subscribers.size === 0 && !record.sink.closed) record.sink.abort("all subscribers disconnected");
maybeFreeStream(state, parsed.id);
}
meta.subscribedStreams.clear();
}
if (meta.uploadingStreams) {
for (const key of meta.uploadingStreams) {
const parsed = parseKey(key);
if (!parsed) continue;
const state = channels.get(parsed.channelName);
const record = state?.inbound.get(parsed.id);
if (!state || !record) continue;
record.reader._end({
name: "UploadDisconnected",
message: "Uploader disconnected before completing the stream"
});
state.inbound.delete(parsed.id);
}
meta.uploadingStreams.clear();
}
}
};
}
//#endregion
//#region src/node/host-functions.ts
const debugBroadcast = createDebug("devframe:rpc:broadcast");
/**
* Concrete implementation backing `ctx.rpc`. Internal: consumers should
* depend on the structural {@link RpcFunctionsHost} type, never this class.
* Its `@internal` members (`_rpcGroup`, `_asyncStorage`,
* `_emitSessionDisconnected`) are wired by `startHttpAndWs` and must not
* widen the public surface.
*
* @internal
*/
var RpcFunctionsHostImpl = class extends RpcFunctionsCollectorBase {
/**
* @internal
*/
_rpcGroup = void 0;
_asyncStorage = void 0;
constructor(context) {
super(context);
this.sharedState = createRpcSharedStateServerHost(this);
this.streaming = createRpcStreamingServerHost(this);
}
sharedState;
streaming;
/**
* Adapters call this from their WS `onDisconnected` hook so downstream
* hosts (streaming, …) can free per-session state. Public-ish because
* tests / custom adapters may want to mirror it.
*
* @internal
*/
_emitSessionDisconnected(meta) {
this.streaming._onSessionDisconnected(meta);
}
async invokeLocal(method, ...args) {
if (!this.definitions.has(method)) throw diagnostics$1.DF0006({ name: String(method) });
const handler = await this.getHandler(method);
return await Promise.resolve(handler(...args));
}
async broadcast(options) {
if (!this._rpcGroup) return;
debugBroadcast(JSON.stringify(options.method));
await Promise.allSettled(this._rpcGroup.clients.map((client) => {
if (options.filter?.(client) === false) return void 0;
return client.$callRaw({
optional: true,
event: true,
...options
});
}));
}
getCurrentRpcSession() {
if (!this._asyncStorage) throw diagnostics$1.DF0007();
return this._asyncStorage.getStore();
}
};
//#endregion
//#region src/node/host-services.ts
/**
* Cross-plugin service registry (see `types/services.ts` for the contract).
* Values are held per context instance; `whenAvailable` subscriptions make
* the mechanism robust against setup ordering between provider and consumer.
*/
var DevframeServicesHostImpl = class {
services = /* @__PURE__ */ new Map();
listeners = /* @__PURE__ */ new Map();
provide(id, service) {
const key = id;
if (this.services.has(key)) throw diagnostics$1.DF0037({ id: key });
this.services.set(key, service);
for (const listener of this.listeners.get(key) ?? []) listener(service);
return () => {
if (this.services.get(key) === service) this.services.delete(key);
};
}
get(id) {
return this.services.get(id);
}
has(id) {
return this.services.has(id);
}
whenAvailable(id, callback) {
const key = id;
if (this.services.has(key)) callback(this.services.get(key));
let set = this.listeners.get(key);
if (!set) {
set = /* @__PURE__ */ new Set();
this.listeners.set(key, set);
}
const listener = callback;
set.add(listener);
return () => {
set.delete(listener);
};
}
keys() {
return Array.from(this.services.keys());
}
};
//#endregion
//#region src/node/host-views.ts
var DevframeViewHost = class {
context;
/**
* @internal
*/
buildStaticDirs = [];
constructor(context) {
this.context = context;
}
hostStatic(baseUrl, distDir) {
if (!existsSync(distDir)) throw diagnostics$1.DF0008({ distDir });
this.buildStaticDirs.push({
baseUrl,
distDir
});
this.context.host.mountStatic(baseUrl, distDir);
}
};
//#endregion
//#region src/node/rpc/agent-invoke-tool.ts
const agentInvokeTool = defineRpcFunction({
name: "devframe:agent:invoke-tool",
type: "action",
setup: (ctx) => {
return { async handler(id, args) {
return await ctx.agent.invoke(id, args);
} };
}
});
//#endregion
//#region src/node/rpc/agent-list-resources.ts
const agentListResources = defineRpcFunction({
name: "devframe:agent:list-resources",
type: "query",
jsonSerializable: true,
setup: (ctx) => {
return { async handler() {
return ctx.agent.list().resources;
} };
}
});
//#endregion
//#region src/node/rpc/index.ts
/**
* Built-in agent introspection RPC functions. Registered automatically
* by `createHostContext`. Not themselves agent-exposed (no `agent`
* field) — they power the MCP adapter and any future agent CLI.
*
* @experimental
*/
const BUILTIN_AGENT_RPC = [
defineRpcFunction({
name: "devframe:agent:list-tools",
type: "query",
jsonSerializable: true,
setup: (ctx) => {
return { async handler() {
return ctx.agent.list().tools;
} };
}
}),
agentInvokeTool,
agentListResources,
defineRpcFunction({
name: "devframe:agent:read-resource",
type: "query",
jsonSerializable: true,
setup: (ctx) => {
return { async handler(id) {
return await ctx.agent.read(id);
} };
}
})
];
//#endregion
//#region src/utils/scope.ts
/** Whether a name is already namespaced (contains a `:` separator). */
function isQualifiedName(name) {
return name.includes(":");
}
/**
* Prefix a bare name with `<namespace>:`. Names that already contain a
* `:` are returned unchanged, so callers can reference another scope's
* ids explicitly (e.g. `ctx.rpc.call('other-plugin:fn')`).
*/
function qualifyName(namespace, name) {
return isQualifiedName(name) ? name : `${namespace}:${name}`;
}
//#endregion
//#region src/node/settings.ts
const STORAGE_SCOPE = {
global: "global",
project: "project"
};
function createNodeSettingsStore(context, namespace, scope) {
const stateKey = `devframe:settings:${scope}:${namespace}`;
let statePromise;
function store() {
if (!statePromise) {
const filepath = join(context.host.getStorageDir(STORAGE_SCOPE[scope]), "settings", `${namespace}.json`);
statePromise = context.rpc.sharedState.get(stateKey, { sharedState: createStorage({
filepath,
initialValue: {}
}) });
}
return statePromise;
}
return {
async get(key) {
return (await store()).value()[key];
},
async set(key, value) {
(await store()).mutate((draft) => {
draft[key] = value;
});
},
async delete(key) {
(await store()).mutate((draft) => {
delete draft[key];
});
},
async all() {
return (await store()).value();
},
async onChange(fn) {
return (await store()).on("updated", (full) => fn(full));
}
};
}
/**
* Build the node-side `settings` surface for a scope namespace. `project`
* persists under the host's `workspace` storage dir, `global` under its
* `global` dir. Each is a file-backed, client-synced key-value store.
*/
function createNodeSettings(context, namespace) {
return {
global: createNodeSettingsStore(context, namespace, "global"),
project: createNodeSettingsStore(context, namespace, "project")
};
}
//#endregion
//#region src/node/scope.ts
function prefixDefinition(namespace, fn) {
if (isQualifiedName(fn.name)) throw diagnostics$1.DF0034({
namespace,
name: fn.name
});
return {
...fn,
name: `${namespace}:${fn.name}`
};
}
/**
* Build a namespace-scoped view of a {@link DevframeNodeContext}. Every
* RPC id, shared-state key, and streaming channel passed through the
* returned `rpc` surface is auto-namespaced with `<namespace>:`.
*/
function createScopedNodeContext(context, namespace) {
const base = context.rpc;
const rpc = {
namespace,
register(fn, force) {
base.register(prefixDefinition(namespace, fn), force);
},
update(fn, force) {
base.update(prefixDefinition(namespace, fn), force);
},
call: ((method, ...args) => base.invokeLocal(qualifyName(namespace, method), ...args)),
broadcast: ((options) => base.broadcast({
...options,
method: qualifyName(namespace, options.method)
})),
sharedState: ((key, options) => base.sharedState.get(qualifyName(namespace, key), options)),
streaming: { create: (name, opts) => base.streaming.create(qualifyName(namespace, name), opts) },
getCurrentRpcSession: () => base.getCurrentRpcSession()
};
return {
namespace,
base: context,
cwd: context.cwd,
workspaceRoot: context.workspaceRoot,
mode: context.mode,
host: context.host,
rpc,
settings: createNodeSettings(context, namespace),
views: context.views,
diagnostics: context.diagnostics,
agent: context.agent,
scope: context.scope
};
}
//#endregion
//#region src/node/context.ts
/**
* Framework- and build-tool-agnostic core of the Devframe node context.
* Wires the RPC host, view (HTTP file-serving) host, diagnostics, and
* agent subsystems. Host adapters can wrap this to augment `ctx` with
* extra surfaces — for example, `@vitejs/devtools-kit`'s
* `createKitContext` attaches `docks`, `terminals`, `messages`, and
* `commands` when mounted into Vite DevTools.
*/
async function createHostContext(options) {
const { cwd, workspaceRoot = cwd, mode, host, builtinRpcDeclarations = [] } = options;
const context = {
cwd,
workspaceRoot,
mode,
host,
rpc: void 0,
views: void 0,
diagnostics: void 0,
agent: void 0,
services: void 0,
scope: void 0
};
const rpcHost = new RpcFunctionsHostImpl(context);
const viewsHost = new DevframeViewHost(context);
const diagnosticsHost = new DevframeDiagnosticsHost(context, [diagnostics$1, diagnostics]);
context.rpc = rpcHost;
context.views = viewsHost;
context.diagnostics = diagnosticsHost;
context.services = new DevframeServicesHostImpl();
context.agent = new DevframeAgentHost(context);
const scopedCache = /* @__PURE__ */ new Map();
context.scope = ((namespace) => {
if (!namespace) return context;
let scoped = scopedCache.get(namespace);
if (!scoped) {
scoped = createScopedNodeContext(context, namespace);
scopedCache.set(namespace, scoped);
}
return scoped;
});
for (const fn of BUILTIN_AGENT_RPC) rpcHost.register(fn);
for (const fn of builtinRpcDeclarations) rpcHost.register(fn);
return context;
}
//#endregion
//#region src/node/host-h3.ts
/**
* h3-backed {@link DevframeHost} — used by the standalone CLI adapter.
*/
function createH3DevframeHost(options) {
const workspaceRoot = options.workspaceRoot ?? process$1.cwd();
return {
mountStatic(base, distDir) {
return options.mount?.(base, distDir);
},
resolveOrigin() {
return options.origin;
},
getStorageDir(scope) {
const namespace = `.${options.appName}/devframe`;
if (scope === "workspace") return join(workspaceRoot, ".devframe");
if (scope === "project") return join(workspaceRoot, "node_modules", namespace);
return join(homedir(), namespace);
}
};
}
//#endregion
export { DevframeViewHost as a, createRpcSharedStateServerHost as c, coerceAgentPositionalArgs as d, createNodeSettings as i, DevframeDiagnosticsHost as l, createHostContext as n, DevframeServicesHostImpl as o, createScopedNodeContext as r, createRpcStreamingServerHost as s, createH3DevframeHost as t, DevframeAgentHost as u };
import { t as createMcpFetchHandler } from "./fetch-BKaD0Vb3.mjs";
import { defineHandler } from "h3";
//#region src/adapters/mcp/http.ts
/**
* Mount an MCP Streamable-HTTP endpoint on an h3 app at `path` — the h3
* binding over {@link createMcpFetchHandler}, which owns the sessions, the
* origin gate, and the transport plumbing.
*
* The handler is web-standard — it takes the h3 event's web `Request` and
* returns a web `Response` (an SSE `ReadableStream` body for the
* server→client stream). We copy that response onto `event.res` and return
* its body rather than returning the `Response` object directly, so a
* legitimate MCP 404 (unknown session) isn't swallowed by h3's
* "Response-with-404 falls through to the next handler" rule (which would
* otherwise hand the request to the SPA static catch-all).
*
* @experimental
*/
function mountMcpHttp(app, ctx, path, options) {
const handler = createMcpFetchHandler(ctx, options);
app.use(path, defineHandler(async (event) => respond(event, await handler.fetch(event.req))));
return { dispose: handler.dispose };
}
/**
* Copy a web `Response` from the MCP transport onto the h3 event's response
* and return its body. Returning the body (a `ReadableStream` or `null`)
* rather than the `Response` object avoids h3's 404-fall-through behavior.
*/
function respond(event, response) {
event.res.status = response.status;
event.res.statusText = response.statusText;
response.headers.forEach((value, key) => {
event.res.headers.set(key, value);
});
return response.body ?? "";
}
//#endregion
export { mountMcpHttp };
import { W as DevframeNodeRpcSession, b as DevframeNodeContext, ht as SharedState } from "./devframe-mQ4qZAuo.mjs";
import { n as InternalAnonymousAuthStorage } from "./context-DoSozfvx.mjs";
//#region src/node/auth/revoke.d.ts
/**
* Flip `isTrusted` to false on any live WS clients connected with `token`
* and broadcast the `auth:revoked` event so they can react.
*
* Shared between persisted-auth revocation and remote-dock token revocation.
*/
declare function revokeActiveConnectionsForToken(context: DevframeNodeContext, token: string): Promise<void>;
/**
* Revoke an auth token: remove from storage and notify all connected clients
* using this token that they are no longer trusted.
*/
declare function revokeAuthToken(context: DevframeNodeContext, storage: SharedState<InternalAnonymousAuthStorage>, token: string): Promise<void>;
//#endregion
//#region src/node/auth/state.d.ts
/**
* The current one-time authentication code. Display this to the user (e.g. in
* the dev-server terminal) so they can type it into the browser to authenticate.
*/
declare function getTempAuthCode(): string;
/**
* Rotate the authentication code, resetting its expiry window and failed-attempt
* counter. Call this when a new authentication flow begins (e.g. when an
* untrusted client starts authenticating) so the displayed code is freshly
* valid for its full TTL.
*/
declare function refreshTempAuthCode(): string;
/**
* Build a "magic link" authentication URL that embeds a one-time code (OTP) in
* the URL **fragment**. Opening it authenticates the client without typing —
* print it on startup (devframe stays headless, so the host prints its own
* banner). Defaults to the current code; the link is subject to the same TTL.
*
* The code rides the fragment (`#devframe_otp=…`), not the query string, so it
* is never sent to the server, written to an access log, or leaked in a
* `Referer` header — the browser client reads it locally (see
* `consumeOtpFromUrl`). Any existing fragment parameters are preserved.
*/
declare function buildOtpAuthUrl(baseUrl: string, code?: string): string;
/**
* Re-authenticate a connection that presents a previously-issued bearer token.
* Returns `true` and marks the session trusted when the token is known.
*
* Used by the `anonymous:devframe:auth` handler so a client that already
* authenticated (token persisted in the browser) is trusted on reconnect
* without entering the code again.
*/
declare function verifyAuthToken(token: string, session: DevframeNodeRpcSession, storage: SharedState<InternalAnonymousAuthStorage>): boolean;
/**
* Exchange a one-time authentication code for a fresh, node-issued bearer token.
*
* On success this mints a high-entropy token, records it in the trusted store,
* marks the calling session trusted, rotates the code, and returns the token
* for the client to persist. Returns `null` on any failure.
*
* Because the code is short and human-typed, verification is hardened against
* brute force: it enforces a time-to-live, compares in constant time, and
* rotates the code after {@link TEMP_AUTH_MAX_ATTEMPTS} failed attempts so an
* attacker cannot keep guessing against the same code.
*/
declare function exchangeTempAuthCode(code: string, session: DevframeNodeRpcSession, info: {
ua: string;
origin: string;
}, storage: SharedState<InternalAnonymousAuthStorage>): string | null;
//#endregion
export { verifyAuthToken as a, refreshTempAuthCode as i, exchangeTempAuthCode as n, revokeActiveConnectionsForToken as o, getTempAuthCode as r, revokeAuthToken as s, buildOtpAuthUrl as t };
import { t as diagnostics } from "./diagnostics-D7wM2cwM.mjs";
import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { join } from "pathe";
import process from "node:process";
import { homedir } from "node:os";
//#region src/node/instance-registry.ts
/** Environment variable overriding the registry directory (tests, CI). */
const DEVFRAME_INSTANCES_DIR_ENV = "DEVFRAME_INSTANCES_DIR";
/** Environment variable disabling instance registration entirely. */
const DEVFRAME_DISABLE_INSTANCE_REGISTRY_ENV = "DEVFRAME_DISABLE_INSTANCE_REGISTRY";
/**
* Resolve the registry directory: `~/.devframe/instances/` by default —
* the framework's own global dir, deliberately outside the per-app
* `~/.<appName>/devframe/` storage convention since the registry spans apps —
* overridable via `DEVFRAME_INSTANCES_DIR`.
*/
function resolveInstancesDir(override) {
return override ?? process.env[DEVFRAME_INSTANCES_DIR_ENV] ?? join(homedir(), ".devframe", "instances");
}
function isRegistryDisabled() {
const value = process.env[DEVFRAME_DISABLE_INSTANCE_REGISTRY_ENV];
return value === "1" || value === "true";
}
/**
* Record a running devframe instance in the global instance registry so
* discovery tooling (`devframe connect`, editor integrations) can find it
* without port guessing.
*
* `createDevServer` registers automatically; custom hosts that serve a
* devframe in-process (e.g. `@devframes/next`'s host inside a Next dev
* server) call this explicitly with the origin they are reachable at.
*
* The record is written atomically to `<dir>/<pid>-<port>.json` and removed
* by {@link DevframeInstanceRegistration.unregister}. Records surviving a
* crash are pruned by readers whose liveness probe fails. Registration never
* throws — a write failure degrades to a coded warning (`DF0045`), since a
* dev server must not die over discovery metadata.
*
* @experimental
*/
function registerDevframeInstance(record, options = {}) {
const dir = resolveInstancesDir(options.instancesDir);
const file = join(dir, `${record.pid}-${record.port}.json`);
if (!isRegistryDisabled()) try {
mkdirSync(dir, { recursive: true });
const tmp = join(dir, `.${record.pid}-${record.port}.${Date.now()}.tmp`);
writeFileSync(tmp, `${JSON.stringify(record, null, 2)}\n`);
renameSync(tmp, file);
} catch (error) {
diagnostics.DF0045({
file,
reason: error instanceof Error ? error.message : String(error),
cause: error
});
}
return {
file,
unregister: () => {
try {
rmSync(file, { force: true });
} catch (error) {
diagnostics.DF0045({
file,
reason: error instanceof Error ? error.message : String(error),
cause: error
});
}
}
};
}
/**
* Read every record in the registry directory, dropping unparseable files.
* Liveness is the caller's concern — see {@link probeDevframeInstance}.
*
* @experimental
*/
function readDevframeInstances(options = {}) {
const dir = resolveInstancesDir(options.instancesDir);
let files;
try {
files = readdirSync(dir).filter((f) => f.endsWith(".json"));
} catch {
return [];
}
const records = [];
for (const file of files) try {
const parsed = JSON.parse(readFileSync(join(dir, file), "utf8"));
if (typeof parsed?.origin === "string" && typeof parsed?.pid === "number") records.push(parsed);
} catch {}
return records;
}
/**
* Dialable-origin candidates for a recorded origin. A `localhost` bind is
* ambiguous — the server may listen on `127.0.0.1`, `::1`, or both, and
* HTTP clients differ in which family they try — so probe the explicit
* addresses too and adopt whichever answers.
*/
function originCandidates(origin) {
try {
const url = new URL(origin);
if (url.hostname !== "localhost") return [origin];
const port = url.port ? `:${url.port}` : "";
return [
origin,
`${url.protocol}//127.0.0.1${port}`,
`${url.protocol}//[::1]${port}`
];
} catch {
return [origin];
}
}
/**
* Probe `<origin><basePath>__connection.json`, trying each dialable
* candidate for the origin (see {@link originCandidates}). The single
* probe primitive behind both registry liveness checks and the
* connector's explicit `--port` probes.
*
* @internal
*/
async function probeDevframeOrigin(origin, basePath, timeoutMs) {
const base = basePath.endsWith("/") ? basePath : `${basePath}/`;
for (const candidate of originCandidates(origin)) try {
const response = await fetch(`${candidate}${base}__connection.json`, { signal: AbortSignal.timeout(timeoutMs ?? 1e3) });
if (!response.ok) continue;
return {
origin: candidate,
meta: await response.json().catch(() => ({}))
};
} catch {}
return null;
}
/**
* Probe a record's `__connection.json` to check the instance is alive.
* Returns the **dialable origin** that answered (for `localhost` records
* this may be an explicit `127.0.0.1` / `[::1]` origin), or `null` when
* unreachable.
*/
async function probeDevframeInstance(record, options = {}) {
return (await probeDevframeOrigin(record.origin, record.basePath, options.timeoutMs))?.origin ?? null;
}
/**
* Read the registry and split records into live and dead by probing each
* one's `__connection.json`, deleting dead records (prune-on-read). Live
* records carry the dialable origin the probe confirmed (a `localhost`
* record may come back as `127.0.0.1` / `[::1]`).
*
* A liveness probe only proves *something* answers on the record's port, so
* records left behind by killed processes shadow the server currently bound
* there: per `(port, basePath)` only the newest record survives, older
* ghosts are pruned with the dead.
*
* @experimental
*/
async function listLiveDevframeInstances(options = {}) {
const dir = resolveInstancesDir(options.instancesDir);
const records = readDevframeInstances({ instancesDir: dir });
const pruned = [];
const prune = (record) => {
pruned.push(record);
try {
rmSync(join(dir, `${record.pid}-${record.port}.json`), { force: true });
} catch {}
};
const newest = /* @__PURE__ */ new Map();
for (const record of records) {
const key = `${record.port}|${record.basePath}`;
const existing = newest.get(key);
if (!existing) newest.set(key, record);
else if (record.startedAt > existing.startedAt) {
prune(existing);
newest.set(key, record);
} else prune(record);
}
const live = [];
await Promise.all([...newest.values()].map(async (record) => {
const origin = await probeDevframeInstance(record, options);
if (origin) live.push(origin === record.origin ? record : {
...record,
origin
});
else prune(record);
}));
live.sort((a, b) => a.startedAt - b.startedAt);
return {
live,
pruned
};
}
//#endregion
export { probeDevframeOrigin as n, registerDevframeInstance as r, listLiveDevframeInstances as t };
import { createRpcServer } from "./rpc/server.mjs";
import { attachWsRpcTransport } from "./rpc/transports/ws-server.mjs";
import { t as diagnostics } from "./diagnostics-D7wM2cwM.mjs";
import { t as getInternalContext } from "./context-CBg8iRbQ.mjs";
import { createServer } from "node:http";
import { AsyncLocalStorage } from "node:async_hooks";
import { H3, toNodeHandler } from "h3";
import { isIP } from "node:net";
//#region src/node/utils.ts
function isObject(value) {
return Object.prototype.toString.call(value) === "[object Object]";
}
const NON_DIALABLE_HOSTS = /* @__PURE__ */ new Set([
"0.0.0.0",
"127.0.0.1",
"::",
"0000:0000:0000:0000:0000:0000:0000:0000",
""
]);
/** Map a bind host to a host a client can actually connect to. */
function toDialableHost(host) {
return NON_DIALABLE_HOSTS.has(host) ? "localhost" : host;
}
/** Format a bind host for use in a URL authority (dialable, IPv6-bracketed). */
function formatHostForUrl(host) {
const dialable = toDialableHost(host);
return isIP(dialable) === 6 ? `[${dialable}]` : dialable;
}
function normalizeHttpServerUrl(host, port) {
return `http://${formatHostForUrl(host)}:${port}`;
}
//#endregion
//#region src/node/server.ts
/**
* Compose an h3 + WebSocket server for a devframe context. The RPC
* group is bound to `context.rpc.functions`; the WS endpoint lives on
* the same port as the HTTP server.
*/
async function startHttpAndWs(options) {
const { context, port } = options;
const bindHost = options.host ?? "localhost";
const app = options.app ?? new H3();
const ownsHttpServer = !options.server;
const httpServer = options.server ?? createServer(toNodeHandler(app));
const rpcHost = context.rpc;
const asyncStorage = new AsyncLocalStorage();
const authHandler = typeof options.auth === "object" ? options.auth : void 0;
const effectiveAuthorize = options.authorize ?? authHandler?.authorize;
if (authHandler) {
for (const fn of authHandler.rpcFunctions) if (!rpcHost.definitions.has(fn.name)) rpcHost.register(fn);
}
const rpcGroup = createRpcServer(rpcHost.functions, { rpcOptions: {
onFunctionError: options.rpcOptions?.onFunctionError,
onGeneralError: options.rpcOptions?.onGeneralError,
resolver(name, fn) {
const rpc = this;
if (!fn) return void 0;
return async function(...args) {
const meta = rpc.$meta;
if (effectiveAuthorize && !effectiveAuthorize(name, {
meta,
rpc
})) throw diagnostics.DF0036({ name });
return await asyncStorage.run({
rpc,
meta
}, async () => {
return (await fn).apply(this, args);
});
};
}
} });
const separateWsPort = ownsHttpServer && options.wsPort != null && options.wsPort !== port ? options.wsPort : void 0;
const { ws, close: closeWs } = attachWsRpcTransport(rpcGroup, {
...separateWsPort != null ? {
port: separateWsPort,
host: bindHost
} : { server: httpServer },
path: options.path,
destroyUnmatched: ownsHttpServer,
allowedOrigins: options.allowedOrigins,
onConnected: authHandler || options.onPeerConnect ? (peer, meta) => {
const session = {
meta,
rpc: rpcGroup.clients.find((client) => client.$meta === meta)
};
authHandler?.onConnect(peer, session);
options.onPeerConnect?.(peer, session);
} : void 0,
onDisconnected: (peer, meta) => {
options.onPeerDisconnect?.(peer, meta);
rpcHost._emitSessionDisconnected(meta);
}
});
rpcHost._rpcGroup = rpcGroup;
rpcHost._asyncStorage = asyncStorage;
rpcHost._authDisabled = options.auth === false;
if (options.auth === false && !rpcHost.definitions.has("anonymous:devframe:auth")) rpcHost.register({
name: "anonymous:devframe:auth",
type: "action",
handler: () => {
const session = rpcHost.getCurrentRpcSession();
if (session) session.meta.isTrusted = true;
return { isTrusted: true };
}
});
if (ownsHttpServer) {
try {
await new Promise((resolve, reject) => {
const onError = (error) => reject(error);
httpServer.once("error", onError);
httpServer.listen(port, bindHost, () => {
httpServer.removeListener("error", onError);
resolve();
});
});
} catch (error) {
await closeWs().catch(() => {});
throw diagnostics.DF0052({
host: bindHost,
port,
reason: error instanceof Error ? error.message : String(error),
cause: error
});
}
if (options.onServerError) httpServer.on("error", options.onServerError);
}
const address = httpServer.address();
const resolvedPort = typeof address === "object" && address ? address.port : port;
const origin = normalizeHttpServerUrl(bindHost, resolvedPort);
const internal = getInternalContext(context);
const wsPortForUrl = separateWsPort ?? resolvedPort;
const wsUrl = `ws://${formatHostForUrl(bindHost)}:${wsPortForUrl}${options.path ?? ""}`;
internal.wsEndpoint = { url: wsUrl };
if (options.onReady) await options.onReady({
origin,
port: resolvedPort,
app
});
function connectionMeta() {
const jsonSerializableMethods = [];
for (const def of rpcHost.definitions.values()) if (def.jsonSerializable === true) jsonSerializableMethods.push(def.name);
return {
backend: "websocket",
websocket: separateWsPort != null ? {
port: separateWsPort,
path: options.path
} : { path: options.path },
jsonSerializableMethods
};
}
return {
origin,
port: resolvedPort,
app,
ws,
rpcGroup,
connectionMeta,
async close() {
await closeWs();
if (ownsHttpServer) await new Promise((r) => httpServer.close(() => r()));
if (getInternalContext(context).wsEndpoint?.url === wsUrl) getInternalContext(context).wsEndpoint = void 0;
}
};
}
//#endregion
export { toDialableHost as a, normalizeHttpServerUrl as i, formatHostForUrl as n, isObject as r, startHttpAndWs as t };
import { $ as DevframeRpcServerFunctions, Q as DevframeRpcClientFunctions, W as DevframeNodeRpcSession, _ as ConnectionMeta, b as DevframeNodeContext, p as DevframeAuthHandler } from "./devframe-mQ4qZAuo.mjs";
import { n as DevframeNodeRpcSessionMeta, r as WsOriginRegistry } from "./ws-server-uKQcClcJ.mjs";
import "./index-b64Uuhy2.mjs";
import { Peer } from "crossws";
import { BirpcGroup, EventOptions } from "birpc";
import { NodeAdapter } from "crossws/adapters/node";
import { Server } from "node:http";
import { H3 } from "h3";
//#region src/node/server.d.ts
interface StartHttpAndWsOptions {
context: DevframeNodeContext;
host?: string;
port: number;
/**
* Optional h3 app to mount on. When omitted a fresh one is created;
* when provided, callers can add their own routes (static handlers,
* auth middleware, etc.) first.
*/
app?: H3;
/**
* Bind the WS endpoint to a single upgrade route (e.g. `/__devframe_ws`) instead of
* claiming every upgrade on the port. This lets the socket share a server
* with other upgrade handlers (Vite HMR, a host framework's own sockets)
* and is what the SPA's `__connection.json` points at. When omitted, the WS
* server handles every upgrade on the port (legacy behaviour).
*/
path?: string;
/**
* Bind the WS endpoint on its own port instead of sharing the HTTP server's.
* The HTTP/SPA server still listens on `port`; the socket gets a dedicated
* `ws` server on `wsPort` (same `host`). Use this for the "different port"
* connection scenario. Ignored when a `server` is supplied.
*/
wsPort?: number;
/**
* Mount the WS endpoint onto an existing HTTP server, sharing its port,
* rather than creating and listening on a fresh one. Use this to embed
* devframe's RPC socket inside a host server (e.g. a Vite dev server) — pair
* it with `path` so it coexists with the host's routes. The caller owns the
* server's lifecycle: {@link StartedServer.close} detaches devframe's upgrade
* listener but leaves the host server running. When set, `host`/`port` are
* only used to report the resolved origin.
*/
server?: Server;
/**
* Authentication for the server:
*
* - `true` (default) — no gate; every registered method is callable
* regardless of trust (today's behavior, unchanged).
* - `false` — the RPC server is started without a trust handshake.
* Intended for single-user localhost tools where an auth round-trip
* would only get in the way. A noop `anonymous:devframe:auth` handler
* is registered so the browser client's unconditional handshake call
* succeeds and auto-trusts.
* - A {@link DevframeAuthHandler} (e.g. from
* `devframe/recipes/interactive-auth`'s `createInteractiveAuth`) —
* registers its `rpcFunctions`, wires its `authorize` as the resolver
* gate, and wires its `onConnect` on every new peer. This is the
* fully-authenticated server: an untrusted caller can only reach
* `anonymous:`-prefixed methods (see `isAnonymousRpcMethod`).
*/
auth?: boolean | DevframeAuthHandler;
/**
* Lower-level escape hatch: gate individual RPC calls by method name and
* session without a full {@link DevframeAuthHandler}. Ignored when `auth`
* is a handler object (its own `authorize` is used); combine with `auth:
* true` to layer a custom policy on top of an otherwise ungated server.
*/
authorize?: (methodName: string, session: DevframeNodeRpcSession) => boolean;
/**
* Called once per new WS connection, right after its session is created
* (before any RPC call is dispatched). Runs after the auth handler's own
* `onConnect` (when `auth` is a {@link DevframeAuthHandler}), so it can
* observe — but not override — the connect-time trust decision.
*/
onPeerConnect?: (peer: Peer, session: DevframeNodeRpcSession) => void;
/**
* Called once per closed WS connection, right after the transport's own
* disconnect bookkeeping runs. Unlike {@link onPeerConnect} this receives
* the raw session meta, not a wrapped session — by the time a peer
* disconnects there is no live RPC client left to attach.
*/
onPeerDisconnect?: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void;
/**
* Forwarded verbatim to the internal `createRpcServer`'s birpc
* `rpcOptions`, alongside the resolver `startHttpAndWs` installs for
* auth/session wiring. Use this so a host that owns its own structured
* diagnostics (e.g. a coded error reporter) keeps seeing RPC failures
* instead of them being silently absorbed by delegating to
* `startHttpAndWs`. Returning `true` from either callback suppresses
* birpc's own error response to the caller — see birpc's
* `EventOptions` for the full contract.
*/
rpcOptions?: Pick<EventOptions<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>, 'onFunctionError' | 'onGeneralError'>;
/**
* Extra origins to accept on the WS upgrade beyond the loopback default
* (`localhost`/`127.0.0.1`/`::1` and any `Origin`-less request from a
* native client). Add your LAN/tunnel origin here when reaching the tool
* from another host. Pass `false` to disable origin checking entirely
* (not recommended). Default: loopback-only.
*/
allowedOrigins?: readonly string[] | WsOriginRegistry | false;
/**
* Called once the WS server is bound so callers can mount static
* handlers whose origin depends on the resolved port, or print their
* own startup banner. Devframe does not print one itself.
*/
onReady?: (info: {
origin: string;
port: number;
app: H3;
}) => void | Promise<void>;
/**
* Called for any error the HTTP server devframe owns emits after it starts
* listening — e.g. a transient `EMFILE` while accepting a connection.
* Without it such an error has no listener and crashes the process.
*
* Applies only to a server devframe created itself. When `server` is
* supplied the caller owns that object and attaches to it directly, so
* devframe leaves its error handling alone.
*/
onServerError?: (error: Error) => void;
}
interface StartedServer {
/** Listening origin, e.g. `http://localhost:9999`. */
origin: string;
port: number;
app: H3;
/** The crossws node adapter driving the RPC socket (connected peers, pub/sub). */
ws: NodeAdapter;
rpcGroup: BirpcGroup<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>;
/**
* The {@link ConnectionMeta} descriptor for this server — the same shape
* a `__connection.json` route should serve so a devframe client's
* `resolveWsUrl` can dial back in. Reflects the `path` / `wsPort` this
* server was started with and the `jsonSerializable` methods currently
* registered on `context.rpc`.
*/
connectionMeta: () => ConnectionMeta;
close: () => Promise<void>;
}
/**
* Compose an h3 + WebSocket server for a devframe context. The RPC
* group is bound to `context.rpc.functions`; the WS endpoint lives on
* the same port as the HTTP server.
*/
declare function startHttpAndWs(options: StartHttpAndWsOptions): Promise<StartedServer>;
//#endregion
export { StartedServer as n, startHttpAndWs as r, StartHttpAndWsOptions as t };
import { t as diagnostics } from "./diagnostics-D7wM2cwM.mjs";
import fs from "node:fs";
import { dirname } from "pathe";
import process$1 from "node:process";
import { destr } from "destr";
//#region src/utils/events.ts
/**
* Create event emitter.
*/
function createEventEmitter() {
const _listeners = {};
function emit(event, ...args) {
const callbacks = _listeners[event] || [];
for (let i = 0, length = callbacks.length; i < length; i++) {
const callback = callbacks[i];
if (callback) callback(...args);
}
}
function emitOnce(event, ...args) {
emit(event, ...args);
delete _listeners[event];
}
function on(event, cb) {
(_listeners[event] ||= []).push(cb);
return () => {
_listeners[event] = _listeners[event]?.filter((i) => cb !== i);
};
}
function once(event, cb) {
const unsubscribe = on(event, ((...args) => {
unsubscribe();
return cb(...args);
}));
return unsubscribe;
}
return {
_listeners,
emit,
emitOnce,
on,
once
};
}
//#endregion
//#region ../../node_modules/.pnpm/immer@11.1.15/node_modules/immer/dist/immer.mjs
var NOTHING = Symbol.for("immer-nothing");
var DRAFTABLE = Symbol.for("immer-draftable");
var DRAFT_STATE = Symbol.for("immer-state");
var errors = process.env.NODE_ENV !== "production" ? [
function(plugin) {
return `The plugin for '${plugin}' has not been loaded into Immer. To enable the plugin, import and call \`enable${plugin}()\` when initializing your application.`;
},
function(thing) {
return `produce can only be called on things that are draftable: plain objects, arrays, Map, Set or classes that are marked with '[immerable]: true'. Got '${thing}'`;
},
"This object has been frozen and should not be mutated",
function(data) {
return "Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? " + data;
},
"An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft.",
"Immer forbids circular references",
"The first or second argument to `produce` must be a function",
"The third argument to `produce` must be a function or undefined",
"First argument to `createDraft` must be a plain object, an array, or an immerable object",
"First argument to `finishDraft` must be a draft returned by `createDraft`",
function(thing) {
return `'current' expects a draft, got: ${thing}`;
},
"Object.defineProperty() cannot be used on an Immer draft",
"Object.setPrototypeOf() cannot be used on an Immer draft",
"Immer only supports deleting array indices",
"Immer only supports setting array indices and the 'length' property",
function(thing) {
return `'original' expects a draft, got: ${thing}`;
}
] : [];
function die(error, ...args) {
if (process.env.NODE_ENV !== "production") {
const e = errors[error];
const msg = isFunction(e) ? e.apply(null, args) : e;
throw new Error(`[Immer] ${msg}`);
}
throw new Error(`[Immer] minified error nr: ${error}. Full error at: https://bit.ly/3cXEKWf`);
}
var O = Object;
var getPrototypeOf = O.getPrototypeOf;
var CONSTRUCTOR = "constructor";
var PROTOTYPE = "prototype";
var CONFIGURABLE = "configurable";
var ENUMERABLE = "enumerable";
var WRITABLE = "writable";
var VALUE = "value";
var isDraft = (value) => !!value && !!value[DRAFT_STATE];
function isDraftable(value) {
if (!value) return false;
return isPlainObject(value) || isArray(value) || !!value[DRAFTABLE] || !!value[CONSTRUCTOR]?.[DRAFTABLE] || isMap(value) || isSet(value);
}
var objectCtorString = O[PROTOTYPE][CONSTRUCTOR].toString();
var cachedCtorStrings = /* @__PURE__ */ new WeakMap();
function isPlainObject(value) {
if (!value || !isObjectish(value)) return false;
const proto = getPrototypeOf(value);
if (proto === null || proto === O[PROTOTYPE]) return true;
const Ctor = O.hasOwnProperty.call(proto, CONSTRUCTOR) && proto[CONSTRUCTOR];
if (Ctor === Object) return true;
if (!isFunction(Ctor)) return false;
let ctorString = cachedCtorStrings.get(Ctor);
if (ctorString === void 0) {
ctorString = Function.toString.call(Ctor);
cachedCtorStrings.set(Ctor, ctorString);
}
return ctorString === objectCtorString;
}
function each(obj, iter, strict = true) {
if (getArchtype(obj) === 0) (strict ? Reflect.ownKeys(obj) : O.keys(obj)).forEach((key) => {
iter(key, obj[key], obj);
});
else obj.forEach((entry, index) => iter(index, entry, obj));
}
function getArchtype(thing) {
const state = thing[DRAFT_STATE];
return state ? state.type_ : isArray(thing) ? 1 : isMap(thing) ? 2 : isSet(thing) ? 3 : 0;
}
var has = (thing, prop, type = getArchtype(thing)) => type === 2 ? thing.has(prop) : O[PROTOTYPE].hasOwnProperty.call(thing, prop);
var get = (thing, prop, type = getArchtype(thing)) => type === 2 ? thing.get(prop) : thing[prop];
var set = (thing, propOrOldValue, value, type = getArchtype(thing)) => {
if (type === 2) thing.set(propOrOldValue, value);
else if (type === 3) thing.add(value);
else thing[propOrOldValue] = value;
};
function is(x, y) {
if (x === y) return x !== 0 || 1 / x === 1 / y;
else return x !== x && y !== y;
}
var isArray = Array.isArray;
var isMap = (target) => target instanceof Map;
var isSet = (target) => target instanceof Set;
var isObjectish = (target) => typeof target === "object";
var isFunction = (target) => typeof target === "function";
var isBoolean = (target) => typeof target === "boolean";
function isArrayIndex(value) {
const n = +value;
return Number.isInteger(n) && String(n) === value;
}
var getProxyDraft = (value) => {
if (!isObjectish(value)) return null;
return value?.[DRAFT_STATE];
};
var latest = (state) => state.copy_ || state.base_;
var getFinalValue = (state) => state.modified_ ? state.copy_ : state.base_;
function shallowCopy(base, strict) {
if (isMap(base)) return new Map(base);
if (isSet(base)) return new Set(base);
if (isArray(base)) return Array[PROTOTYPE].slice.call(base);
const isPlain = isPlainObject(base);
if (strict === true || strict === "class_only" && !isPlain) {
const descriptors = O.getOwnPropertyDescriptors(base);
delete descriptors[DRAFT_STATE];
let keys = Reflect.ownKeys(descriptors);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const desc = descriptors[key];
if (desc[WRITABLE] === false) {
desc[WRITABLE] = true;
desc[CONFIGURABLE] = true;
}
if (desc.get || desc.set) descriptors[key] = {
[CONFIGURABLE]: true,
[WRITABLE]: true,
[ENUMERABLE]: desc[ENUMERABLE],
[VALUE]: base[key]
};
}
return O.create(getPrototypeOf(base), descriptors);
} else {
const proto = getPrototypeOf(base);
if (proto !== null && isPlain) return { ...base };
const obj = O.create(proto);
return O.assign(obj, base);
}
}
function freeze(obj, deep = false) {
if (isFrozen(obj) || isDraft(obj) || !isDraftable(obj)) return obj;
if (getArchtype(obj) > 1) O.defineProperties(obj, {
set: dontMutateMethodOverride,
add: dontMutateMethodOverride,
clear: dontMutateMethodOverride,
delete: dontMutateMethodOverride
});
O.freeze(obj);
if (deep) each(obj, (_key, value) => {
freeze(value, true);
}, false);
return obj;
}
function dontMutateFrozenCollections() {
die(2);
}
var dontMutateMethodOverride = { [VALUE]: dontMutateFrozenCollections };
function isFrozen(obj) {
if (obj === null || !isObjectish(obj)) return true;
return O.isFrozen(obj);
}
var PluginMapSet = "MapSet";
var PluginPatches = "Patches";
var PluginArrayMethods = "ArrayMethods";
var plugins = {};
function getPlugin(pluginKey) {
const plugin = plugins[pluginKey];
if (!plugin) die(0, pluginKey);
return plugin;
}
var isPluginLoaded = (pluginKey) => !!plugins[pluginKey];
function loadPlugin(pluginKey, implementation) {
if (!plugins[pluginKey]) plugins[pluginKey] = implementation;
}
var currentScope;
var getCurrentScope = () => currentScope;
var createScope = (parent_, immer_) => ({
drafts_: [],
parent_,
immer_,
canAutoFreeze_: true,
unfinalizedDrafts_: 0,
handledSet_: /* @__PURE__ */ new Set(),
processedForPatches_: /* @__PURE__ */ new Set(),
mapSetPlugin_: isPluginLoaded(PluginMapSet) ? getPlugin(PluginMapSet) : void 0,
arrayMethodsPlugin_: isPluginLoaded(PluginArrayMethods) ? getPlugin(PluginArrayMethods) : void 0
});
function usePatchesInScope(scope, patchListener) {
if (patchListener) {
scope.patchPlugin_ = getPlugin(PluginPatches);
scope.patches_ = [];
scope.inversePatches_ = [];
scope.patchListener_ = patchListener;
}
}
function revokeScope(scope) {
leaveScope(scope);
scope.drafts_.forEach(revokeDraft);
scope.drafts_ = null;
}
function leaveScope(scope) {
if (scope === currentScope) currentScope = scope.parent_;
}
var enterScope = (immer2) => currentScope = createScope(currentScope, immer2);
function revokeDraft(draft) {
const state = draft[DRAFT_STATE];
if (state.type_ === 0 || state.type_ === 1) state.revoke_();
else state.revoked_ = true;
}
function processResult(result, scope) {
scope.unfinalizedDrafts_ = scope.drafts_.length;
const baseDraft = scope.drafts_[0];
if (result !== void 0 && result !== baseDraft) {
if (baseDraft[DRAFT_STATE].modified_) {
revokeScope(scope);
die(4);
}
if (isDraftable(result)) result = finalize(scope, result);
const { patchPlugin_ } = scope;
if (patchPlugin_) patchPlugin_.generateReplacementPatches_(baseDraft[DRAFT_STATE].base_, result, scope);
} else result = finalize(scope, baseDraft);
maybeFreeze(scope, result, true);
revokeScope(scope);
if (scope.patches_) scope.patchListener_(scope.patches_, scope.inversePatches_);
return result !== NOTHING ? result : void 0;
}
function finalize(rootScope, value) {
if (isFrozen(value)) return value;
const state = value[DRAFT_STATE];
if (!state) return handleValue(value, rootScope.handledSet_, rootScope);
if (!isSameScope(state, rootScope)) return value;
if (!state.modified_) return state.base_;
if (!state.finalized_) {
const { callbacks_ } = state;
if (callbacks_) while (callbacks_.length > 0) callbacks_.pop()(rootScope);
generatePatchesAndFinalize(state, rootScope);
}
return state.copy_;
}
function maybeFreeze(scope, value, deep = false) {
if (!scope.parent_ && scope.immer_.autoFreeze_ && scope.canAutoFreeze_) freeze(value, deep);
}
function markStateFinalized(state) {
state.finalized_ = true;
state.scope_.unfinalizedDrafts_--;
}
var isSameScope = (state, rootScope) => state.scope_ === rootScope;
var EMPTY_LOCATIONS_RESULT = [];
function updateDraftInParent(parent, draftValue, finalizedValue, originalKey) {
const parentCopy = latest(parent);
const parentType = parent.type_;
if (originalKey !== void 0) {
if (get(parentCopy, originalKey, parentType) === draftValue) {
set(parentCopy, originalKey, finalizedValue, parentType);
return;
}
}
if (!parent.draftLocations_) {
const draftLocations = parent.draftLocations_ = /* @__PURE__ */ new Map();
each(parentCopy, (key, value) => {
if (isDraft(value)) {
const keys = draftLocations.get(value) || [];
keys.push(key);
draftLocations.set(value, keys);
}
});
}
const locations = parent.draftLocations_.get(draftValue) ?? EMPTY_LOCATIONS_RESULT;
for (const location of locations) set(parentCopy, location, finalizedValue, parentType);
}
function registerChildFinalizationCallback(parent, child, key) {
parent.callbacks_.push(function childCleanup(rootScope) {
const state = child;
if (!state || !isSameScope(state, rootScope)) return;
rootScope.mapSetPlugin_?.fixSetContents(state);
const finalizedValue = getFinalValue(state);
updateDraftInParent(parent, state.draft_ ?? state, finalizedValue, key);
generatePatchesAndFinalize(state, rootScope);
});
}
function generatePatchesAndFinalize(state, rootScope) {
if (state.modified_ && !state.finalized_ && (state.type_ === 3 || state.type_ === 1 && state.allIndicesReassigned_ || (state.assigned_?.size ?? 0) > 0)) {
const { patchPlugin_ } = rootScope;
if (patchPlugin_) {
const basePath = patchPlugin_.getPath(state);
if (basePath) patchPlugin_.generatePatches_(state, basePath, rootScope);
}
markStateFinalized(state);
}
}
function handleCrossReference(target, key, value) {
const { scope_ } = target;
if (isDraft(value)) {
const state = value[DRAFT_STATE];
if (isSameScope(state, scope_)) state.callbacks_.push(function crossReferenceCleanup() {
prepareCopy(target);
updateDraftInParent(target, value, getFinalValue(state), key);
});
} else if (isDraftable(value)) target.callbacks_.push(function nestedDraftCleanup() {
const targetCopy = latest(target);
if (target.type_ === 3) {
if (targetCopy.has(value)) handleValue(value, scope_.handledSet_, scope_);
} else if (get(targetCopy, key, target.type_) === value) {
if (scope_.drafts_.length > 1 && (target.assigned_.get(key) ?? false) === true && target.copy_) handleValue(get(target.copy_, key, target.type_), scope_.handledSet_, scope_);
}
});
}
function handleValue(target, handledSet, rootScope) {
if (!rootScope.immer_.autoFreeze_ && rootScope.unfinalizedDrafts_ < 1) return target;
if (isDraft(target) || handledSet.has(target) || !isDraftable(target) || isFrozen(target)) return target;
handledSet.add(target);
each(target, (key, value) => {
if (isDraft(value)) {
const state = value[DRAFT_STATE];
if (isSameScope(state, rootScope)) {
set(target, key, getFinalValue(state), target.type_);
markStateFinalized(state);
}
} else if (isDraftable(value)) handleValue(value, handledSet, rootScope);
});
return target;
}
function createProxyProxy(base, parent) {
const baseIsArray = isArray(base);
const state = {
type_: baseIsArray ? 1 : 0,
scope_: parent ? parent.scope_ : getCurrentScope(),
modified_: false,
finalized_: false,
assigned_: void 0,
parent_: parent,
base_: base,
draft_: null,
copy_: null,
revoke_: null,
isManual_: false,
callbacks_: void 0
};
let target = state;
let traps = objectTraps;
if (baseIsArray) {
target = [state];
traps = arrayTraps;
}
const { revoke, proxy } = Proxy.revocable(target, traps);
state.draft_ = proxy;
state.revoke_ = revoke;
return [proxy, state];
}
var objectTraps = {
get(state, prop) {
if (prop === DRAFT_STATE) return state;
let arrayPlugin = state.scope_.arrayMethodsPlugin_;
const isArrayWithStringProp = state.type_ === 1 && typeof prop === "string";
if (isArrayWithStringProp) {
if (arrayPlugin?.isArrayOperationMethod(prop)) return arrayPlugin.createMethodInterceptor(state, prop);
}
const source = latest(state);
if (!has(source, prop, state.type_)) return readPropFromProto(state, source, prop);
const value = source[prop];
if (state.finalized_ || !isDraftable(value)) return value;
if (isArrayWithStringProp && state.operationMethod && arrayPlugin?.isMutatingArrayMethod(state.operationMethod) && isArrayIndex(prop)) return value;
if (value === peek(state.base_, prop) || isRelocatedBaseRef(state, prop, value)) {
prepareCopy(state);
const childKey = state.type_ === 1 ? +prop : prop;
const childDraft = createProxy(state.scope_, value, state, childKey);
return state.copy_[childKey] = childDraft;
}
return value;
},
has(state, prop) {
return prop in latest(state);
},
ownKeys(state) {
return Reflect.ownKeys(latest(state));
},
set(state, prop, value) {
const desc = getDescriptorFromProto(latest(state), prop);
if (desc?.set) {
desc.set.call(state.draft_, value);
return true;
}
if (!state.modified_) {
const current2 = peek(latest(state), prop);
const currentState = current2?.[DRAFT_STATE];
if (currentState && currentState.base_ === value) {
state.copy_[prop] = value;
state.assigned_.set(prop, false);
return true;
}
if (is(value, current2) && (value !== void 0 || has(state.base_, prop, state.type_))) return true;
prepareCopy(state);
markChanged(state);
}
if (state.copy_[prop] === value && (value !== void 0 || has(state.copy_, prop, state.type_)) || Number.isNaN(value) && Number.isNaN(state.copy_[prop])) return true;
state.copy_[prop] = value;
state.assigned_.set(prop, true);
handleCrossReference(state, prop, value);
return true;
},
deleteProperty(state, prop) {
prepareCopy(state);
if (peek(state.base_, prop) !== void 0 || prop in state.base_) {
state.assigned_.set(prop, false);
markChanged(state);
} else state.assigned_.delete(prop);
if (state.copy_) delete state.copy_[prop];
return true;
},
getOwnPropertyDescriptor(state, prop) {
const owner = latest(state);
const desc = Reflect.getOwnPropertyDescriptor(owner, prop);
if (!desc) return desc;
return {
[WRITABLE]: true,
[CONFIGURABLE]: state.type_ !== 1 || prop !== "length",
[ENUMERABLE]: desc[ENUMERABLE],
[VALUE]: owner[prop]
};
},
defineProperty() {
die(11);
},
getPrototypeOf(state) {
return getPrototypeOf(state.base_);
},
setPrototypeOf() {
die(12);
}
};
var arrayTraps = {};
for (let key in objectTraps) {
let fn = objectTraps[key];
arrayTraps[key] = function() {
const args = arguments;
args[0] = args[0][0];
return fn.apply(this, args);
};
}
arrayTraps.deleteProperty = function(state, prop) {
if (process.env.NODE_ENV !== "production" && isNaN(parseInt(prop))) die(13);
return arrayTraps.set.call(this, state, prop, void 0);
};
arrayTraps.set = function(state, prop, value) {
if (process.env.NODE_ENV !== "production" && prop !== "length" && isNaN(parseInt(prop))) die(14);
return objectTraps.set.call(this, state[0], prop, value, state[0]);
};
function peek(draft, prop) {
const state = draft[DRAFT_STATE];
return (state ? latest(state) : draft)[prop];
}
function isRelocatedBaseRef(state, prop, value) {
if (state.type_ !== 1 || !state.allIndicesReassigned_ || state.assigned_?.get(prop) || !isDraftable(value) || value[DRAFT_STATE]) return false;
return state.baseRefs_.has(value);
}
function readPropFromProto(state, source, prop) {
const desc = getDescriptorFromProto(source, prop);
return desc ? VALUE in desc ? desc[VALUE] : desc.get?.call(state.draft_) : void 0;
}
function getDescriptorFromProto(source, prop) {
if (!(prop in source)) return void 0;
let proto = getPrototypeOf(source);
while (proto) {
const desc = Object.getOwnPropertyDescriptor(proto, prop);
if (desc) return desc;
proto = getPrototypeOf(proto);
}
}
function markChanged(state) {
if (!state.modified_) {
state.modified_ = true;
if (state.parent_) markChanged(state.parent_);
}
}
function prepareCopy(state) {
if (!state.copy_) {
state.assigned_ = /* @__PURE__ */ new Map();
state.copy_ = shallowCopy(state.base_, state.scope_.immer_.useStrictShallowCopy_);
}
}
var Immer2 = class {
constructor(config) {
this.autoFreeze_ = true;
this.useStrictShallowCopy_ = false;
this.useStrictIteration_ = false;
/**
* The `produce` function takes a value and a "recipe function" (whose
* return value often depends on the base state). The recipe function is
* free to mutate its first argument however it wants. All mutations are
* only ever applied to a __copy__ of the base state.
*
* Pass only a function to create a "curried producer" which relieves you
* from passing the recipe function every time.
*
* Only plain objects and arrays are made mutable. All other objects are
* considered uncopyable.
*
* Note: This function is __bound__ to its `Immer` instance.
*
* @param {any} base - the initial state
* @param {Function} recipe - function that receives a proxy of the base state as first argument and which can be freely modified
* @param {Function} patchListener - optional function that will be called with all the patches produced here
* @returns {any} a new state, or the initial state if nothing was modified
*/
this.produce = (base, recipe, patchListener) => {
if (isFunction(base) && !isFunction(recipe)) {
const defaultBase = recipe;
recipe = base;
const self = this;
return function curriedProduce(base2 = defaultBase, ...args) {
return self.produce(base2, (draft) => recipe.call(this, draft, ...args));
};
}
if (!isFunction(recipe)) die(6);
if (patchListener !== void 0 && !isFunction(patchListener)) die(7);
let result;
if (isDraftable(base)) {
const scope = enterScope(this);
const proxy = createProxy(scope, base, void 0);
let hasError = true;
try {
result = recipe(proxy);
hasError = false;
} finally {
if (hasError) revokeScope(scope);
else leaveScope(scope);
}
usePatchesInScope(scope, patchListener);
return processResult(result, scope);
} else if (!base || !isObjectish(base)) {
result = recipe(base);
if (result === void 0) result = base;
if (result === NOTHING) result = void 0;
if (this.autoFreeze_) freeze(result, true);
if (patchListener) {
const p = [];
const ip = [];
getPlugin(PluginPatches).generateReplacementPatches_(base, result, {
patches_: p,
inversePatches_: ip
});
patchListener(p, ip);
}
return result;
} else die(1, base);
};
this.produceWithPatches = (base, recipe) => {
if (isFunction(base)) return (state, ...args) => this.produceWithPatches(state, (draft) => base(draft, ...args));
let patches, inversePatches;
return [
this.produce(base, recipe, (p, ip) => {
patches = p;
inversePatches = ip;
}),
patches,
inversePatches
];
};
if (isBoolean(config?.autoFreeze)) this.setAutoFreeze(config.autoFreeze);
if (isBoolean(config?.useStrictShallowCopy)) this.setUseStrictShallowCopy(config.useStrictShallowCopy);
if (isBoolean(config?.useStrictIteration)) this.setUseStrictIteration(config.useStrictIteration);
}
createDraft(base) {
if (!isDraftable(base)) die(8);
if (isDraft(base)) base = current(base);
const scope = enterScope(this);
const proxy = createProxy(scope, base, void 0);
proxy[DRAFT_STATE].isManual_ = true;
leaveScope(scope);
return proxy;
}
finishDraft(draft, patchListener) {
const state = draft && draft[DRAFT_STATE];
if (!state || !state.isManual_) die(9);
const { scope_: scope } = state;
usePatchesInScope(scope, patchListener);
return processResult(void 0, scope);
}
/**
* Pass true to automatically freeze all copies created by Immer.
*
* By default, auto-freezing is enabled.
*/
setAutoFreeze(value) {
this.autoFreeze_ = value;
}
/**
* Pass true to enable strict shallow copy.
*
* By default, immer does not copy the object descriptors such as getter, setter and non-enumrable properties.
*/
setUseStrictShallowCopy(value) {
this.useStrictShallowCopy_ = value;
}
/**
* Pass false to use faster iteration that skips non-enumerable properties
* but still handles symbols for compatibility.
*
* By default, strict iteration is enabled (includes all own properties).
*/
setUseStrictIteration(value) {
this.useStrictIteration_ = value;
}
shouldUseStrictIteration() {
return this.useStrictIteration_;
}
applyPatches(base, patches) {
let i;
for (i = patches.length - 1; i >= 0; i--) {
const patch = patches[i];
if (patch.path.length === 0 && patch.op === "replace") {
base = patch.value;
break;
}
}
if (i > -1) patches = patches.slice(i + 1);
const applyPatchesImpl = getPlugin(PluginPatches).applyPatches_;
if (isDraft(base)) return applyPatchesImpl(base, patches);
return this.produce(base, (draft) => applyPatchesImpl(draft, patches));
}
};
function createProxy(rootScope, value, parent, key) {
const [draft, state] = isMap(value) ? getPlugin(PluginMapSet).proxyMap_(value, parent) : isSet(value) ? getPlugin(PluginMapSet).proxySet_(value, parent) : createProxyProxy(value, parent);
(parent?.scope_ ?? getCurrentScope()).drafts_.push(draft);
state.callbacks_ = parent?.callbacks_ ?? [];
state.key_ = key;
if (parent && key !== void 0) registerChildFinalizationCallback(parent, state, key);
else state.callbacks_.push(function rootDraftCleanup(rootScope2) {
rootScope2.mapSetPlugin_?.fixSetContents(state);
const { patchPlugin_ } = rootScope2;
if (state.modified_ && patchPlugin_) patchPlugin_.generatePatches_(state, [], rootScope2);
});
return draft;
}
function current(value) {
if (!isDraft(value)) die(10, value);
return currentImpl(value);
}
function currentImpl(value) {
if (!isDraftable(value) || isFrozen(value)) return value;
const state = value[DRAFT_STATE];
let copy;
let strict = true;
if (state) {
if (!state.modified_) return state.base_;
state.finalized_ = true;
copy = shallowCopy(value, state.scope_.immer_.useStrictShallowCopy_);
strict = state.scope_.immer_.shouldUseStrictIteration();
} else copy = shallowCopy(value, true);
each(copy, (key, childValue) => {
set(copy, key, currentImpl(childValue));
}, strict);
if (state) state.finalized_ = false;
return copy;
}
function enablePatches() {
const errorOffset = 16;
if (process.env.NODE_ENV !== "production") errors.push("Sets cannot have \"replace\" patches.", function(op) {
return "Unsupported patch operation: " + op;
}, function(path) {
return "Cannot apply patch, path doesn't resolve: " + path;
}, "Patching reserved attributes like __proto__, prototype and constructor is not allowed");
function getPath(state, path = []) {
if (state.key_ !== void 0) {
const parentCopy = state.parent_.copy_ ?? state.parent_.base_;
const proxyDraft = getProxyDraft(get(parentCopy, state.key_));
const valueAtKey = get(parentCopy, state.key_);
if (valueAtKey === void 0) return null;
if (valueAtKey !== state.draft_ && valueAtKey !== state.base_ && valueAtKey !== state.copy_) return null;
if (proxyDraft != null && proxyDraft.base_ !== state.base_) return null;
const isSet2 = state.parent_.type_ === 3;
let key;
if (isSet2) {
const setParent = state.parent_;
key = Array.from(setParent.drafts_.keys()).indexOf(state.key_);
} else key = state.key_;
if (!(isSet2 && parentCopy.size > key || has(parentCopy, key))) return null;
path.push(key);
}
if (state.parent_) return getPath(state.parent_, path);
path.reverse();
try {
resolvePath(state.copy_, path);
} catch (e) {
return null;
}
return path;
}
function resolvePath(base, path) {
let current2 = base;
for (let i = 0; i < path.length - 1; i++) {
const key = path[i];
current2 = get(current2, key);
if (!isObjectish(current2) || current2 === null) throw new Error(`Cannot resolve path at '${path.join("/")}'`);
}
return current2;
}
const REPLACE = "replace";
const ADD = "add";
const REMOVE = "remove";
function generatePatches_(state, basePath, scope) {
if (state.scope_.processedForPatches_.has(state)) return;
state.scope_.processedForPatches_.add(state);
const { patches_, inversePatches_ } = scope;
switch (state.type_) {
case 0:
case 2: return generatePatchesFromAssigned(state, basePath, patches_, inversePatches_);
case 1: return generateArrayPatches(state, basePath, patches_, inversePatches_);
case 3: return generateSetPatches(state, basePath, patches_, inversePatches_);
}
}
function generateArrayPatches(state, basePath, patches, inversePatches) {
let { base_, assigned_ } = state;
let copy_ = state.copy_;
if (copy_.length < base_.length) {
[base_, copy_] = [copy_, base_];
[patches, inversePatches] = [inversePatches, patches];
}
const allReassigned = state.allIndicesReassigned_ === true;
for (let i = 0; i < base_.length; i++) {
const copiedItem = copy_[i];
const baseItem = base_[i];
if ((allReassigned || assigned_?.get(i.toString())) && copiedItem !== baseItem) {
const childState = copiedItem?.[DRAFT_STATE];
if (childState && childState.modified_) continue;
const path = basePath.concat([i]);
patches.push({
op: REPLACE,
path,
value: clonePatchValueIfNeeded(copiedItem)
});
inversePatches.push({
op: REPLACE,
path,
value: clonePatchValueIfNeeded(baseItem)
});
}
}
for (let i = base_.length; i < copy_.length; i++) {
const path = basePath.concat([i]);
patches.push({
op: ADD,
path,
value: clonePatchValueIfNeeded(copy_[i])
});
}
for (let i = copy_.length - 1; base_.length <= i; --i) {
const path = basePath.concat([i]);
inversePatches.push({
op: REMOVE,
path
});
}
}
function generatePatchesFromAssigned(state, basePath, patches, inversePatches) {
const { base_, copy_, type_ } = state;
each(state.assigned_, (key, assignedValue) => {
const origValue = get(base_, key, type_);
const value = get(copy_, key, type_);
const op = !assignedValue ? REMOVE : has(base_, key) ? REPLACE : ADD;
if (origValue === value && op === REPLACE) return;
const path = basePath.concat(key);
patches.push(op === REMOVE ? {
op,
path
} : {
op,
path,
value: clonePatchValueIfNeeded(value)
});
inversePatches.push(op === ADD ? {
op: REMOVE,
path
} : op === REMOVE ? {
op: ADD,
path,
value: clonePatchValueIfNeeded(origValue)
} : {
op: REPLACE,
path,
value: clonePatchValueIfNeeded(origValue)
});
});
}
function generateSetPatches(state, basePath, patches, inversePatches) {
let { base_, copy_ } = state;
let i = 0;
base_.forEach((value) => {
if (!copy_.has(value)) {
const path = basePath.concat([i]);
patches.push({
op: REMOVE,
path,
value
});
inversePatches.unshift({
op: ADD,
path,
value
});
}
i++;
});
i = 0;
copy_.forEach((value) => {
if (!base_.has(value)) {
const path = basePath.concat([i]);
patches.push({
op: ADD,
path,
value
});
inversePatches.unshift({
op: REMOVE,
path,
value
});
}
i++;
});
}
function generateReplacementPatches_(baseValue, replacement, scope) {
const { patches_, inversePatches_ } = scope;
patches_.push({
op: REPLACE,
path: [],
value: replacement === NOTHING ? void 0 : replacement
});
inversePatches_.push({
op: REPLACE,
path: [],
value: baseValue
});
}
function applyPatches_(draft, patches) {
patches.forEach((patch) => {
const { path, op } = patch;
let base = draft;
for (let i = 0; i < path.length - 1; i++) {
const parentType = getArchtype(base);
let p = path[i];
if (typeof p !== "string" && typeof p !== "number") p = "" + p;
if ((parentType === 0 || parentType === 1) && (p === "__proto__" || p === CONSTRUCTOR)) die(19);
if (isFunction(base) && p === PROTOTYPE) die(19);
base = get(base, p);
if (base === null || !isObjectish(base)) die(18, path.join("/"));
}
const type = getArchtype(base);
const value = deepClonePatchValue(patch.value);
const key = path[path.length - 1];
switch (op) {
case REPLACE: switch (type) {
case 2: return base.set(key, value);
case 3: die(errorOffset);
default: return base[key] = value;
}
case ADD: switch (type) {
case 1: return key === "-" ? base.push(value) : base.splice(key, 0, value);
case 2: return base.set(key, value);
case 3: return base.add(value);
default: return base[key] = value;
}
case REMOVE: switch (type) {
case 1: return base.splice(key, 1);
case 2: return base.delete(key);
case 3: return base.delete(patch.value);
default: return delete base[key];
}
default: die(17, op);
}
});
return draft;
}
function deepClonePatchValue(obj) {
if (!isDraftable(obj)) return obj;
if (isArray(obj)) return obj.map(deepClonePatchValue);
if (isMap(obj)) return new Map(Array.from(obj.entries()).map(([k, v]) => [k, deepClonePatchValue(v)]));
if (isSet(obj)) return new Set(Array.from(obj).map(deepClonePatchValue));
const cloned = Object.create(getPrototypeOf(obj));
for (const key in obj) cloned[key] = deepClonePatchValue(obj[key]);
if (has(obj, DRAFTABLE)) cloned[DRAFTABLE] = obj[DRAFTABLE];
return cloned;
}
function clonePatchValueIfNeeded(obj) {
if (isDraft(obj)) return deepClonePatchValue(obj);
else return obj;
}
loadPlugin(PluginPatches, {
applyPatches_,
generatePatches_,
generateReplacementPatches_,
getPath
});
}
var immer = new Immer2();
var produce = immer.produce;
var produceWithPatches = /* @__PURE__ */ immer.produceWithPatches.bind(immer);
var applyPatches = /* @__PURE__ */ immer.applyPatches.bind(immer);
//#endregion
//#region src/utils/nanoid.ts
const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
function nanoid(size = 21) {
let id = "";
let i = size;
while (i--) id += urlAlphabet[Math.random() * 64 | 0];
return id;
}
//#endregion
//#region src/utils/shared-state.ts
/**
* Upper bound on retained syncIds. Loop echoes arrive near-immediately, so a
* generous window preserves de-dup while capping memory on long-lived,
* frequently-mutated states (e.g. a 1s terminal poll).
*/
const MAX_SYNC_IDS = 1e3;
function rememberSyncId(syncIds, syncId) {
syncIds.add(syncId);
if (syncIds.size > MAX_SYNC_IDS) {
const oldest = syncIds.values().next().value;
if (oldest !== void 0) syncIds.delete(oldest);
}
}
function createSharedState(options) {
const { enablePatches: enablePatches$1 = false } = options;
if (enablePatches$1) enablePatches();
const events = createEventEmitter();
let state = options.initialValue;
const syncIds = /* @__PURE__ */ new Set();
return {
on: events.on,
value: () => state,
patch: (patches, syncId = nanoid()) => {
if (syncIds.has(syncId)) return;
enablePatches();
state = applyPatches(state, patches);
rememberSyncId(syncIds, syncId);
events.emit("updated", state, void 0, syncId);
},
mutate: (fn, syncId = nanoid()) => {
if (syncIds.has(syncId)) return;
rememberSyncId(syncIds, syncId);
if (enablePatches$1) {
const [newState, patches] = produceWithPatches(state, fn);
state = newState;
events.emit("updated", state, patches, syncId);
} else {
state = produce(state, fn);
events.emit("updated", state, void 0, syncId);
}
},
syncIds
};
}
//#endregion
//#region ../../node_modules/.pnpm/perfect-debounce@2.1.0/node_modules/perfect-debounce/dist/index.mjs
const DEBOUNCE_DEFAULTS = { trailing: true };
/**
Debounce functions
@param fn - Promise-returning/async function to debounce.
@param wait - Milliseconds to wait before calling `fn`. Default value is 25ms
@returns A function that delays calling `fn` until after `wait` milliseconds have elapsed since the last time it was called.
@example
```
import { debounce } from 'perfect-debounce';
const expensiveCall = async input => input;
const debouncedFn = debounce(expensiveCall, 200);
for (const number of [1, 2, 3]) {
console.log(await debouncedFn(number));
}
//=> 1
//=> 2
//=> 3
```
*/
function debounce(fn, wait = 25, options = {}) {
options = {
...DEBOUNCE_DEFAULTS,
...options
};
if (!Number.isFinite(wait)) throw new TypeError("Expected `wait` to be a finite number");
let leadingValue;
let timeout;
let resolveList = [];
let currentPromise;
let trailingArgs;
const applyFn = (_this, args) => {
currentPromise = _applyPromised(fn, _this, args);
currentPromise.finally(() => {
currentPromise = null;
if (options.trailing && trailingArgs && !timeout) {
const promise = applyFn(_this, trailingArgs);
trailingArgs = null;
return promise;
}
});
return currentPromise;
};
const debounced = function(...args) {
if (options.trailing) trailingArgs = args;
if (currentPromise) return currentPromise;
return new Promise((resolve) => {
const shouldCallNow = !timeout && options.leading;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
const promise = options.leading ? leadingValue : applyFn(this, args);
trailingArgs = null;
for (const _resolve of resolveList) _resolve(promise);
resolveList = [];
}, wait);
if (shouldCallNow) {
leadingValue = applyFn(this, args);
resolve(leadingValue);
} else resolveList.push(resolve);
});
};
const _clearTimeout = (timer) => {
if (timer) {
clearTimeout(timer);
timeout = null;
}
};
debounced.isPending = () => !!timeout;
debounced.cancel = () => {
_clearTimeout(timeout);
resolveList = [];
trailingArgs = null;
};
debounced.flush = () => {
_clearTimeout(timeout);
if (!trailingArgs || currentPromise) return;
const args = trailingArgs;
trailingArgs = null;
return applyFn(this, args);
};
return debounced;
}
async function _applyPromised(fn, _this, args) {
return await fn.apply(_this, args);
}
//#endregion
//#region src/node/storage.ts
function createStorage(options) {
const { mergeInitialValue = (initialValue, savedValue) => ({
...initialValue,
...savedValue
}), debounce: debounceTime = 100 } = options;
let initialValue = options.initialValue;
if (fs.existsSync(options.filepath)) try {
const savedValue = destr(fs.readFileSync(options.filepath, "utf-8"), { strict: true });
initialValue = mergeInitialValue ? mergeInitialValue(options.initialValue, savedValue) : savedValue;
} catch (error) {
diagnostics.DF0012({
filepath: options.filepath,
cause: error
}, { method: "warn" });
initialValue = options.initialValue;
}
const state = createSharedState({
initialValue,
enablePatches: false
});
state.on("updated", debounce((newState) => {
try {
const dir = dirname(options.filepath);
fs.mkdirSync(dir, { recursive: true });
const tmp = `${options.filepath}.${process$1.pid}.tmp`;
fs.writeFileSync(tmp, `${JSON.stringify(newState, null, 2)}\n`);
fs.renameSync(tmp, options.filepath);
} catch (error) {
diagnostics.DF0035({
filepath: options.filepath,
cause: error
}, { method: "error" });
}
}, debounceTime));
return state;
}
//#endregion
export { createEventEmitter as i, createSharedState as n, nanoid as r, createStorage as t };
import { v as RpcFunctionDefinitionAny } from "./types-CnJSgRVa.mjs";
import { Peer } from "crossws";
import { BirpcGroup, ChannelOptions } from "birpc";
import { NodeAdapter } from "crossws/adapters/node";
import { Server } from "node:http";
import { Server as Server$1, ServerOptions } from "node:https";
import { AddressInfo } from "node:net";
//#region src/rpc/transports/ws-server.d.ts
interface DevframeNodeRpcSessionMeta {
id: number;
/** The crossws peer backing this session's socket. */
peer?: Peer;
clientAuthToken?: string;
isTrusted?: boolean;
subscribedStates: Set<string>;
/**
* Streams this session has subscribed to via
* `rpc.streaming.subscribe(channel, id)`. Tracked here for O(1) cleanup
* on disconnect; the wire format is `${channel}\x1F${id}`.
*/
subscribedStreams?: Set<string>;
/**
* Inbound streams this session is currently uploading to (via
* `rpc.streaming.upload(channel, id)`). Tracked for cleanup on
* disconnect; same wire format as `subscribedStreams`.
*/
uploadingStreams?: Set<string>;
}
interface WsRpcTransportOptions {
/**
* Attach to an existing HTTP(S) server, sharing its port. Combine with
* `path` to bind the WS endpoint to a single route so it coexists with
* other upgrade handlers on the same server (e.g. a Vite dev server's HMR
* socket). The shared server's lifecycle is owned by the caller — closing
* this transport detaches the upgrade listener without closing the server.
*/
server?: Server | Server$1;
/**
* Port for the standalone WebSocket server. Defaults to `0`, which lets the
* operating system assign an available port.
*/
port?: number;
/** Host for the standalone WebSocket server. Defaults to `localhost`. */
host?: string;
/**
* Restrict the WS endpoint to a single upgrade route (e.g. `/__devframe_ws`). When
* sharing a `server`, non-matching upgrade requests are left untouched for
* other listeners to handle, so devframe's socket can sit alongside
* framework sockets (Vite HMR, etc.).
*/
path?: string;
/**
* Destroy upgrade requests that don't match `path` instead of leaving them
* for other listeners. Enable this when devframe owns the shared server
* outright (nothing else handles its upgrades), so an off-route client is
* rejected promptly rather than left hanging. Default: `false`
* (coexist-friendly); servers this transport creates itself always
* destroy unmatched upgrades.
*/
destroyUnmatched?: boolean;
/** When set, a new https.Server is created and the WS endpoint is attached to it. */
https?: ServerOptions;
/**
* Extra origins to accept on the WS upgrade beyond the loopback default.
* Add your LAN/tunnel origin here when reaching the tool from another host.
* Pass `false` to disable origin checking entirely (not recommended).
* Default: loopback-only.
*/
allowedOrigins?: readonly string[] | WsOriginRegistry | false;
/**
* RPC function definitions, used by the per-call wire serializer to
* dispatch between strict-JSON and structured-clone encoding based
* on each function's `jsonSerializable` flag.
*
* When omitted, all messages fall back to structured-clone — safe but
* loses dev-time validation for `jsonSerializable: true` declarations.
*/
definitions?: ReadonlyMap<string, Pick<RpcFunctionDefinitionAny, 'jsonSerializable'>>;
onConnected?: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void;
onDisconnected?: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void;
/** Override the default per-call serializer. Most callers should leave this unset. */
serialize?: ChannelOptions['serialize'];
/** Override the default per-call deserializer. Most callers should leave this unset. */
deserialize?: ChannelOptions['deserialize'];
}
interface CreateWsOriginRegistryOptions {
/** Origins allowed before any external viewers are registered. */
allowedOrigins?: readonly string[];
/** Additional validation to run after the registration token is verified. */
validateOrigin?: (origin: string) => boolean;
}
interface WsOriginRegistry {
/** Registration token to include in connection metadata. */
readonly token: string;
/** Read and register an origin from a connection bootstrap URL. */
registerFromUrl: (url: string) => string | undefined;
/** Check whether an origin is currently allowed. */
isAllowed: (origin: string | undefined) => boolean;
}
/**
* Create a live, token-protected origin allowlist for external browser
* viewers. Pass it to {@link WsRpcTransportOptions.allowedOrigins}, then use
* `registerFromUrl()` in the connection metadata handler to authorize a
* viewer without sharing a mutable array or disabling DNS-rebinding protection.
*/
declare function createWsOriginRegistry(options?: CreateWsOriginRegistryOptions): WsOriginRegistry;
interface WsRpcTransport {
/**
* The crossws node adapter driving the socket — exposes the connected
* `peers` and pub/sub. See https://crossws.h3.dev.
*/
ws: NodeAdapter;
/** Resolves when the transport-owned server is listening. */
ready: Promise<void>;
/** Returns the bound address, or `null` when the server is not listening. */
address: () => AddressInfo | string | null;
/** Remove the upgrade listener from a shared `server` (a no-op otherwise). */
detach: () => void;
/**
* Tear the transport down deterministically: detach from a shared server,
* force-terminate every connected peer, and close any server this
* transport created itself (`port` / `https` modes).
*/
close: () => Promise<void>;
}
declare function isLoopbackHostname(hostname: string): boolean;
/**
* Default origin policy for a localhost dev tool: allow requests with no
* `Origin` header (native, non-browser clients), allow any loopback origin
* (so cross-port localhost dev setups keep working), and allow explicitly
* configured origins. Everything else — a real remote page in the dev's
* browser — is rejected.
*/
declare function isAllowedOrigin(origin: string | undefined, allowedOrigins: readonly string[]): boolean;
/**
* Attach a WebSocket transport to an existing RPC group, powered by
* [crossws](https://crossws.h3.dev). Either attach to an existing HTTP(S)
* `server` (sharing its port, optionally scoped to a `path`), or let this
* helper create a standalone server from `port` / `host` / `https`.
*
* Returns the crossws node adapter, standalone-server readiness/address
* accessors, `detach` (remove the upgrade listener from a shared `server`),
* and `close` (full deterministic teardown).
*/
declare function attachWsRpcTransport<ClientFunctions extends object, ServerFunctions extends object>(rpcGroup: BirpcGroup<ClientFunctions, ServerFunctions, false>, options?: WsRpcTransportOptions): WsRpcTransport;
//#endregion
export { WsRpcTransportOptions as a, isAllowedOrigin as c, WsRpcTransport as i, isLoopbackHostname as l, DevframeNodeRpcSessionMeta as n, attachWsRpcTransport as o, WsOriginRegistry as r, createWsOriginRegistry as s, CreateWsOriginRegistryOptions as t };
+1
-1

@@ -1,2 +0,2 @@

import { r as DevframeDefinition } from "../devframe-qgCKL683.mjs";
import { r as DevframeDefinition } from "../devframe-mQ4qZAuo.mjs";
//#region src/adapters/build.d.ts

@@ -3,0 +3,0 @@ interface CreateBuildOptions {

@@ -6,4 +6,4 @@ import { t as collectStaticRpcDump } from "../dump-CgZShDRB.mjs";

import { n as structuredCloneStringify } from "../structured-clone-CbAV5rFI.mjs";
import { n as createHostContext, t as createH3DevframeHost } from "../host-h3-Kz7t5Xab.mjs";
import { t as diagnostics } from "../diagnostics-B5-qHeqD.mjs";
import { n as createHostContext, t as createH3DevframeHost } from "../host-h3-D2hYZvDK.mjs";
import { t as diagnostics } from "../diagnostics-D7wM2cwM.mjs";
import { n as resolveBasePath } from "../_shared-bWRzeSa0.mjs";

@@ -10,0 +10,0 @@ import { existsSync } from "node:fs";

@@ -1,2 +0,2 @@

import { Ft as InferCliFlags, It as defineCliFlags, Lt as parseCliFlags, Pt as CliFlagsSchema, r as DevframeDefinition } from "../devframe-qgCKL683.mjs";
import { Ft as InferCliFlags, It as defineCliFlags, Lt as parseCliFlags, Pt as CliFlagsSchema, r as DevframeDefinition } from "../devframe-mQ4qZAuo.mjs";
import { CAC } from "cac";

@@ -3,0 +3,0 @@ import { H3 } from "h3";

@@ -1,2 +0,2 @@

import { n as defineCliFlags, r as parseCliFlags, t as createCac } from "../cac-B1LM3zit.mjs";
import { n as defineCliFlags, r as parseCliFlags, t as createCac } from "../cac-D4_9ok0z.mjs";
export { createCac, defineCliFlags, parseCliFlags };

@@ -1,2 +0,2 @@

import { Ft as InferCliFlags, It as defineCliFlags, Lt as parseCliFlags, Pt as CliFlagsSchema } from "../devframe-qgCKL683.mjs";
import { Ft as InferCliFlags, It as defineCliFlags, Lt as parseCliFlags, Pt as CliFlagsSchema } from "../devframe-mQ4qZAuo.mjs";
import { CacHandle, CreateCacOptions, createCac } from "./cac.mjs";

@@ -3,0 +3,0 @@ //#region src/adapters/cli.d.ts

@@ -1,2 +0,2 @@

import { n as defineCliFlags, r as parseCliFlags, t as createCac } from "../cac-B1LM3zit.mjs";
import { n as defineCliFlags, r as parseCliFlags, t as createCac } from "../cac-D4_9ok0z.mjs";
//#region src/adapters/cli.ts

@@ -3,0 +3,0 @@ /** @deprecated Use `createCac` from `devframe/adapters/cac` instead. */

@@ -1,3 +0,5 @@

import { _ as ConnectionMeta, d as McpRouteOptions, p as DevframeAuthHandler, r as DevframeDefinition, u as DevframeWsOptions } from "../devframe-qgCKL683.mjs";
import { n as StartedServer } from "../server-E9z4ZWbd.mjs";
import { W as DevframeNodeRpcSession, _ as ConnectionMeta, d as McpRouteOptions, p as DevframeAuthHandler, r as DevframeDefinition, u as DevframeWsOptions } from "../devframe-mQ4qZAuo.mjs";
import { n as DevframeNodeRpcSessionMeta } from "../ws-server-uKQcClcJ.mjs";
import { n as StartedServer } from "../server-UHWae43Q.mjs";
import { Peer } from "crossws";
import { H3 } from "h3";

@@ -72,2 +74,13 @@ //#region src/adapters/dev.d.ts

/**
* Called once per new WS connection, right after its session is created.
* Forwarded verbatim to the underlying `startHttpAndWs`.
*/
onPeerConnect?: (peer: Peer, session: DevframeNodeRpcSession) => void;
/**
* Called once per closed WS connection, right after its session's
* disconnect bookkeeping runs. Forwarded verbatim to the underlying
* `startHttpAndWs`.
*/
onPeerDisconnect?: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void;
/**
* Called once the WS server is bound. Devframe stays headless

@@ -74,0 +87,0 @@ * otherwise — wire this if you want a startup banner.

@@ -1,2 +0,2 @@

import { n as resolveDevServerPort, r as resolveMcpConnectionMeta, t as createDevServer } from "../dev-BSHFZGZr.mjs";
import { n as resolveDevServerPort, r as resolveMcpConnectionMeta, t as createDevServer } from "../dev-Bc49u3qg.mjs";
export { createDevServer, resolveDevServerPort, resolveMcpConnectionMeta };

@@ -1,2 +0,2 @@

import { b as DevframeNodeContext, r as DevframeDefinition } from "../devframe-qgCKL683.mjs";
import { b as DevframeNodeContext, r as DevframeDefinition } from "../devframe-mQ4qZAuo.mjs";
//#region src/adapters/embedded.d.ts

@@ -3,0 +3,0 @@ interface CreateEmbeddedOptions {

@@ -1,2 +0,2 @@

import { b as DevframeNodeContext, r as DevframeDefinition } from "../devframe-qgCKL683.mjs";
import { b as DevframeNodeContext, r as DevframeDefinition } from "../devframe-mQ4qZAuo.mjs";
import "@modelcontextprotocol/server";

@@ -3,0 +3,0 @@ //#region src/adapters/mcp/build-server.d.ts

@@ -1,2 +0,2 @@

import { n as createMcpServer, t as createMcpFetchHandler } from "../fetch-CK0S253E.mjs";
import { n as createMcpServer, t as createMcpFetchHandler } from "../fetch-BKaD0Vb3.mjs";
export { createMcpFetchHandler, createMcpServer };

@@ -1,3 +0,3 @@

import { t as diagnostics } from "../diagnostics-B5-qHeqD.mjs";
import { n as probeDevframeOrigin, t as listLiveDevframeInstances } from "../instance-registry-D6fxYu36.mjs";
import { t as diagnostics } from "../diagnostics-D7wM2cwM.mjs";
import { n as probeDevframeOrigin, t as listLiveDevframeInstances } from "../instance-registry-BnnA2Eoh.mjs";
import { t as toAgentToolName } from "../agent-tool-name-C3b5vEwJ.mjs";

@@ -4,0 +4,0 @@ import { Diagnostic } from "nostics";

@@ -1,2 +0,2 @@

import { $ as DevframeRpcServerFunctions, F as ScopedSharedStates, I as SettingsForNamespace, J as RpcSharedStateHost, N as ScopedRpcFn, O as DevframeSettings, P as ScopedServerFunctions, Q as DevframeRpcClientFunctions, _ as ConnectionMeta, at as StreamReader, ht as SharedState, jt as EventEmitter, ot as StreamSink, q as RpcSharedStateGetOptions } from "../devframe-qgCKL683.mjs";
import { $ as DevframeRpcServerFunctions, F as ScopedSharedStates, I as SettingsForNamespace, J as RpcSharedStateHost, N as ScopedRpcFn, O as DevframeSettings, P as ScopedServerFunctions, Q as DevframeRpcClientFunctions, _ as ConnectionMeta, at as StreamReader, ht as SharedState, jt as EventEmitter, ot as StreamSink, q as RpcSharedStateGetOptions } from "../devframe-mQ4qZAuo.mjs";
import { _ as RpcFunctionDefinition, w as RpcFunctionsCollector } from "../types-CnJSgRVa.mjs";

@@ -3,0 +3,0 @@ import { E as RpcCacheOptions, T as RpcCacheManager } from "../index-Dbw1p5ch.mjs";

@@ -1,2 +0,2 @@

import { d as McpRouteOptions, p as DevframeAuthHandler, r as DevframeDefinition } from "../devframe-qgCKL683.mjs";
import { d as McpRouteOptions, p as DevframeAuthHandler, r as DevframeDefinition } from "../devframe-mQ4qZAuo.mjs";
//#region src/helpers/vite.d.ts

@@ -3,0 +3,0 @@ interface ViteDevBridgeOptions {

import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_WS_ROUTE } from "../constants.mjs";
import { t as diagnostics } from "../diagnostics-B5-qHeqD.mjs";
import { t as diagnostics } from "../diagnostics-D7wM2cwM.mjs";
import { n as resolveBasePath, t as normalizeBasePath } from "../_shared-bWRzeSa0.mjs";
import { serveStaticNodeMiddleware } from "../utils/serve-static.mjs";
import { n as resolveDevServerPort, r as resolveMcpConnectionMeta, t as createDevServer } from "../dev-BSHFZGZr.mjs";
import { n as resolveDevServerPort, r as resolveMcpConnectionMeta, t as createDevServer } from "../dev-Bc49u3qg.mjs";
import { resolve } from "pathe";

@@ -7,0 +7,0 @@ //#region src/helpers/vite.ts

@@ -1,5 +0,5 @@

import { $ as DevframeRpcServerFunctions, A as DevframeSettingsStore, At as DevframeAgentHostEvents, B as DevframeDefineDiagnosticsOptions, C as DevframeServicesHost, Ct as AgentResourceContent, D as DevframeScopedStreamingHost, Dt as AgentToolProvider, E as DevframeScopedNodeRpc, Et as AgentToolInput, F as ScopedSharedStates, G as RpcBroadcastOptions, H as DevframeDiagnosticsHost, I as SettingsForNamespace, J as RpcSharedStateHost, K as RpcFunctionsHost, L as DevframeViewHost, M as ScopedClientFunctions, Mt as EventUnsubscribe, N as ScopedRpcFn, Nt as EventsMap, O as DevframeSettings, Ot as AgentToolProviderHandle, P as ScopedServerFunctions, Q as DevframeRpcClientFunctions, R as DevframeHost, S as DevframeServiceOf, St as AgentResource, T as DevframeScopedNodeContext, Tt as AgentTool, U as DevframeDiagnosticsLogger, V as DevframeDiagnosticsDefinition, W as DevframeNodeRpcSession, X as RpcStreamingChannelOptions, Y as RpcStreamingChannel, Z as RpcStreamingHost, _ as ConnectionMeta, a as DevframeDockDefaults, b as DevframeNodeContext, bt as AgentHandle, c as DevframeSetupInfo, d as McpRouteOptions, et as DevframeRpcSharedStates, f as defineDevframe, g as Thenable, h as PartialWithoutId, i as DevframeDeploymentKind, j as ScopedBroadcastOptions, jt as EventEmitter, k as DevframeSettingsRegistry, kt as DevframeAgentHost, l as DevframeSpaOptions, m as EntriesToObject, n as DevframeCliOptions, o as DevframeDuplicationStrategy, q as RpcSharedStateGetOptions, r as DevframeDefinition, s as DevframeRuntime, t as DevframeBrowserContext, u as DevframeWsOptions, v as ConnectionMetaWebsocket, w as DevframeServicesRegistry, wt as AgentResourceInput, x as DevframeServiceId, xt as AgentManifest, y as DevframeCapabilities, z as DevframeStorageScope } from "./devframe-qgCKL683.mjs";
import { $ as DevframeRpcServerFunctions, A as DevframeSettingsStore, At as DevframeAgentHostEvents, B as DevframeDefineDiagnosticsOptions, C as DevframeServicesHost, Ct as AgentResourceContent, D as DevframeScopedStreamingHost, Dt as AgentToolProvider, E as DevframeScopedNodeRpc, Et as AgentToolInput, F as ScopedSharedStates, G as RpcBroadcastOptions, H as DevframeDiagnosticsHost, I as SettingsForNamespace, J as RpcSharedStateHost, K as RpcFunctionsHost, L as DevframeViewHost, M as ScopedClientFunctions, Mt as EventUnsubscribe, N as ScopedRpcFn, Nt as EventsMap, O as DevframeSettings, Ot as AgentToolProviderHandle, P as ScopedServerFunctions, Q as DevframeRpcClientFunctions, R as DevframeHost, S as DevframeServiceOf, St as AgentResource, T as DevframeScopedNodeContext, Tt as AgentTool, U as DevframeDiagnosticsLogger, V as DevframeDiagnosticsDefinition, W as DevframeNodeRpcSession, X as RpcStreamingChannelOptions, Y as RpcStreamingChannel, Z as RpcStreamingHost, _ as ConnectionMeta, a as DevframeDockDefaults, b as DevframeNodeContext, bt as AgentHandle, c as DevframeSetupInfo, d as McpRouteOptions, et as DevframeRpcSharedStates, f as defineDevframe, g as Thenable, h as PartialWithoutId, i as DevframeDeploymentKind, j as ScopedBroadcastOptions, jt as EventEmitter, k as DevframeSettingsRegistry, kt as DevframeAgentHost, l as DevframeSpaOptions, m as EntriesToObject, n as DevframeCliOptions, o as DevframeDuplicationStrategy, q as RpcSharedStateGetOptions, r as DevframeDefinition, s as DevframeRuntime, t as DevframeBrowserContext, u as DevframeWsOptions, v as ConnectionMetaWebsocket, w as DevframeServicesRegistry, wt as AgentResourceInput, x as DevframeServiceId, xt as AgentManifest, y as DevframeCapabilities, z as DevframeStorageScope } from "./devframe-mQ4qZAuo.mjs";
import { C as RpcFunctionType, T as RpcReturnSchema, _ as RpcFunctionDefinition, g as RpcFunctionAgentOptions, i as RpcArgsSchema } from "./types-CnJSgRVa.mjs";
import "./index-Dbw1p5ch.mjs";
import { n as DevframeNodeRpcSessionMeta } from "./ws-server-J-zkiOyp.mjs";
import { n as DevframeNodeRpcSessionMeta } from "./ws-server-uKQcClcJ.mjs";
//#region src/define.d.ts

@@ -6,0 +6,0 @@ declare const defineRpcFunction: <NAME extends string, TYPE extends RpcFunctionType, ARGS extends any[], RETURN = void, const AS extends RpcArgsSchema | undefined = undefined, const RS extends RpcReturnSchema | undefined = undefined>(definition: RpcFunctionDefinition<NAME, TYPE, ARGS, RETURN, AS, RS, DevframeNodeContext>) => RpcFunctionDefinition<NAME, TYPE, ARGS, RETURN, AS, RS, DevframeNodeContext>;

@@ -1,3 +0,3 @@

import { p as DevframeAuthHandler } from "../devframe-qgCKL683.mjs";
import { a as verifyAuthToken, i as refreshTempAuthCode, n as exchangeTempAuthCode, o as revokeActiveConnectionsForToken, r as getTempAuthCode, s as revokeAuthToken, t as buildOtpAuthUrl } from "../index-B7RgLw--.mjs";
import { p as DevframeAuthHandler } from "../devframe-mQ4qZAuo.mjs";
import { a as verifyAuthToken, i as refreshTempAuthCode, n as exchangeTempAuthCode, o as revokeActiveConnectionsForToken, r as getTempAuthCode, s as revokeAuthToken, t as buildOtpAuthUrl } from "../index-b64Uuhy2.mjs";
export { DevframeAuthHandler, buildOtpAuthUrl, exchangeTempAuthCode, getTempAuthCode, refreshTempAuthCode, revokeActiveConnectionsForToken, revokeAuthToken, verifyAuthToken };

@@ -1,3 +0,3 @@

import { i as DevframeDeploymentKind, r as DevframeDefinition } from "../devframe-qgCKL683.mjs";
import { a as internalContextMap, i as getInternalContext, n as InternalAnonymousAuthStorage, r as RemoteTokenRecord, t as DevframeInternalContext } from "../context-DFzmxCLa.mjs";
import { i as DevframeDeploymentKind, r as DevframeDefinition } from "../devframe-mQ4qZAuo.mjs";
import { a as internalContextMap, i as getInternalContext, n as InternalAnonymousAuthStorage, r as RemoteTokenRecord, t as DevframeInternalContext } from "../context-DoSozfvx.mjs";
//#region src/adapters/_shared.d.ts

@@ -4,0 +4,0 @@ /**

@@ -1,3 +0,3 @@

import { n as internalContextMap, t as getInternalContext } from "../context-CQefP2jR.mjs";
import { n as internalContextMap, t as getInternalContext } from "../context-CBg8iRbQ.mjs";
import { n as resolveBasePath, t as normalizeBasePath } from "../_shared-bWRzeSa0.mjs";
export { getInternalContext, internalContextMap, normalizeBasePath, resolveBasePath };

@@ -1,5 +0,5 @@

import { At as DevframeAgentHostEvents, C as DevframeServicesHost, Ct as AgentResourceContent, Dt as AgentToolProvider, Et as AgentToolInput, H as DevframeDiagnosticsHost$1, J as RpcSharedStateHost, K as RpcFunctionsHost, L as DevframeViewHost$1, O as DevframeSettings, Ot as AgentToolProviderHandle, R as DevframeHost, S as DevframeServiceOf, St as AgentResource, T as DevframeScopedNodeContext, Tt as AgentTool, U as DevframeDiagnosticsLogger, Z as RpcStreamingHost, b as DevframeNodeContext, bt as AgentHandle, ht as SharedState, jt as EventEmitter, kt as DevframeAgentHost$1, wt as AgentResourceInput, x as DevframeServiceId, xt as AgentManifest } from "../devframe-qgCKL683.mjs";
import { At as DevframeAgentHostEvents, C as DevframeServicesHost, Ct as AgentResourceContent, Dt as AgentToolProvider, Et as AgentToolInput, H as DevframeDiagnosticsHost$1, J as RpcSharedStateHost, K as RpcFunctionsHost, L as DevframeViewHost$1, O as DevframeSettings, Ot as AgentToolProviderHandle, R as DevframeHost, S as DevframeServiceOf, St as AgentResource, T as DevframeScopedNodeContext, Tt as AgentTool, U as DevframeDiagnosticsLogger, Z as RpcStreamingHost, b as DevframeNodeContext, bt as AgentHandle, ht as SharedState, jt as EventEmitter, kt as DevframeAgentHost$1, wt as AgentResourceInput, x as DevframeServiceId, xt as AgentManifest } from "../devframe-mQ4qZAuo.mjs";
import { v as RpcFunctionDefinitionAny } from "../types-CnJSgRVa.mjs";
import "../index-Dbw1p5ch.mjs";
import { n as StartedServer, r as startHttpAndWs, t as StartHttpAndWsOptions } from "../server-E9z4ZWbd.mjs";
import { n as StartedServer, r as startHttpAndWs, t as StartHttpAndWsOptions } from "../server-UHWae43Q.mjs";
import { BirpcGroup } from "birpc";

@@ -6,0 +6,0 @@ //#region src/node/agent-args.d.ts

@@ -1,5 +0,5 @@

import { a as DevframeViewHost, c as createRpcSharedStateServerHost, d as coerceAgentPositionalArgs, i as createNodeSettings, l as DevframeDiagnosticsHost, n as createHostContext, o as DevframeServicesHostImpl, r as createScopedNodeContext, s as createRpcStreamingServerHost, t as createH3DevframeHost, u as DevframeAgentHost } from "../host-h3-Kz7t5Xab.mjs";
import { t as createStorage } from "../storage-Dzoc3NVC.mjs";
import { r as registerDevframeInstance, t as listLiveDevframeInstances } from "../instance-registry-D6fxYu36.mjs";
import { a as toDialableHost, i as normalizeHttpServerUrl, n as formatHostForUrl, r as isObject, t as startHttpAndWs } from "../server-CXaDDUl0.mjs";
import { a as DevframeViewHost, c as createRpcSharedStateServerHost, d as coerceAgentPositionalArgs, i as createNodeSettings, l as DevframeDiagnosticsHost, n as createHostContext, o as DevframeServicesHostImpl, r as createScopedNodeContext, s as createRpcStreamingServerHost, t as createH3DevframeHost, u as DevframeAgentHost } from "../host-h3-D2hYZvDK.mjs";
import { t as createStorage } from "../storage-CTNVbuFB.mjs";
import { r as registerDevframeInstance, t as listLiveDevframeInstances } from "../instance-registry-BnnA2Eoh.mjs";
import { a as toDialableHost, i as normalizeHttpServerUrl, n as formatHostForUrl, r as isObject, t as startHttpAndWs } from "../server-CD8XmBXl.mjs";
export { DevframeAgentHost, DevframeDiagnosticsHost, DevframeServicesHostImpl, DevframeViewHost, coerceAgentPositionalArgs, createH3DevframeHost, createHostContext, createNodeSettings, createRpcSharedStateServerHost, createRpcStreamingServerHost, createScopedNodeContext, createStorage, formatHostForUrl, isObject, listLiveDevframeInstances, normalizeHttpServerUrl, registerDevframeInstance, startHttpAndWs, toDialableHost };

@@ -1,2 +0,2 @@

import "../devframe-qgCKL683.mjs";
import "../devframe-mQ4qZAuo.mjs";
import { E as Thenable, S as RpcFunctionSetupResult, c as RpcDump, g as RpcFunctionAgentOptions } from "../types-CnJSgRVa.mjs";

@@ -3,0 +3,0 @@ import "../index-Dbw1p5ch.mjs";

@@ -1,3 +0,3 @@

import { b as DevframeNodeContext, p as DevframeAuthHandler } from "../devframe-qgCKL683.mjs";
import "../index-B7RgLw--.mjs";
import { b as DevframeNodeContext, p as DevframeAuthHandler } from "../devframe-mQ4qZAuo.mjs";
import "../index-b64Uuhy2.mjs";
//#region src/recipes/interactive-auth.d.ts

@@ -4,0 +4,0 @@ interface CreateInteractiveAuthOptions {

import { n as colors } from "../diagnostics-reporter-CsIG85Q5.mjs";
import { isAnonymousRpcMethod } from "../constants.mjs";
import { n as defineRpcFunction } from "../define-BLWPsH6y.mjs";
import { t as getInternalContext } from "../context-CQefP2jR.mjs";
import { t as getInternalContext } from "../context-CBg8iRbQ.mjs";
import { a as verifyAuthToken, n as exchangeTempAuthCode, r as getTempAuthCode, t as buildOtpAuthUrl } from "../state-BeUHDDjk.mjs";

@@ -6,0 +6,0 @@ import { t as s } from "../simple-schema-DQPZrAaZ.mjs";

@@ -1,2 +0,2 @@

import { a as WsRpcTransportOptions, c as isAllowedOrigin, i as WsRpcTransport, l as isLoopbackHostname, n as DevframeNodeRpcSessionMeta, o as attachWsRpcTransport, r as WsOriginRegistry, s as createWsOriginRegistry, t as CreateWsOriginRegistryOptions } from "../../ws-server-J-zkiOyp.mjs";
import { a as WsRpcTransportOptions, c as isAllowedOrigin, i as WsRpcTransport, l as isLoopbackHostname, n as DevframeNodeRpcSessionMeta, o as attachWsRpcTransport, r as WsOriginRegistry, s as createWsOriginRegistry, t as CreateWsOriginRegistryOptions } from "../../ws-server-uKQcClcJ.mjs";
export { CreateWsOriginRegistryOptions, DevframeNodeRpcSessionMeta, WsOriginRegistry, WsRpcTransport, WsRpcTransportOptions, attachWsRpcTransport, createWsOriginRegistry, isAllowedOrigin, isLoopbackHostname };

@@ -53,2 +53,17 @@ import "../../constants.mjs";

function NOOP() {}
function listen(server, port, host) {
return new Promise((resolve, reject) => {
const onError = (error) => reject(error);
server.once("error", onError);
try {
server.listen(port, host, () => {
server.off("error", onError);
resolve();
});
} catch (error) {
server.off("error", onError);
reject(error);
}
});
}
/** Compare two URL paths ignoring a trailing slash. */

@@ -122,5 +137,5 @@ function pathMatches(a, b) {

*
* Returns the crossws node adapter plus `detach` (remove the upgrade
* listener from a shared `server`) and `close` (full deterministic
* teardown).
* Returns the crossws node adapter, standalone-server readiness/address
* accessors, `detach` (remove the upgrade listener from a shared `server`),
* and `close` (full deterministic teardown).
*/

@@ -188,2 +203,3 @@ function attachWsRpcTransport(rpcGroup, options = {}) {

let detach = NOOP;
let ready = Promise.resolve();
let ownedServer;

@@ -194,3 +210,3 @@ if (server) detach = routeUpgrades(server, ws, path, destroyUnmatched, allowedOrigins);

detach = routeUpgrades(ownedServer, ws, path, true, allowedOrigins);
ownedServer.listen(port, host);
ready = listen(ownedServer, port ?? 0, host);
} else {

@@ -202,6 +218,9 @@ ownedServer = createServer((_req, res) => {

detach = routeUpgrades(ownedServer, ws, path, true, allowedOrigins);
ownedServer.listen(port, host);
ready = listen(ownedServer, port ?? 0, host);
}
const activeServer = server ?? ownedServer;
return {
ws,
ready,
address: () => activeServer?.address() ?? null,
detach,

@@ -213,2 +232,4 @@ async close() {

const srv = ownedServer;
await ready.catch(() => {});
if (!srv.listening) return;
await new Promise((r) => srv.close(() => r()));

@@ -215,0 +236,0 @@ }

@@ -1,4 +0,4 @@

import { $ as DevframeRpcServerFunctions, A as DevframeSettingsStore, At as DevframeAgentHostEvents, B as DevframeDefineDiagnosticsOptions, C as DevframeServicesHost, Ct as AgentResourceContent, D as DevframeScopedStreamingHost, Dt as AgentToolProvider, E as DevframeScopedNodeRpc, Et as AgentToolInput, F as ScopedSharedStates, G as RpcBroadcastOptions, H as DevframeDiagnosticsHost, I as SettingsForNamespace, J as RpcSharedStateHost, K as RpcFunctionsHost, L as DevframeViewHost, M as ScopedClientFunctions, Mt as EventUnsubscribe, N as ScopedRpcFn, Nt as EventsMap, O as DevframeSettings, Ot as AgentToolProviderHandle, P as ScopedServerFunctions, Q as DevframeRpcClientFunctions, R as DevframeHost, S as DevframeServiceOf, St as AgentResource, T as DevframeScopedNodeContext, Tt as AgentTool, U as DevframeDiagnosticsLogger, V as DevframeDiagnosticsDefinition, W as DevframeNodeRpcSession, X as RpcStreamingChannelOptions, Y as RpcStreamingChannel, Z as RpcStreamingHost, _ as ConnectionMeta, a as DevframeDockDefaults, b as DevframeNodeContext, bt as AgentHandle, c as DevframeSetupInfo, d as McpRouteOptions, et as DevframeRpcSharedStates, f as defineDevframe, g as Thenable, h as PartialWithoutId, i as DevframeDeploymentKind, j as ScopedBroadcastOptions, jt as EventEmitter, k as DevframeSettingsRegistry, kt as DevframeAgentHost, l as DevframeSpaOptions, m as EntriesToObject, n as DevframeCliOptions, o as DevframeDuplicationStrategy, q as RpcSharedStateGetOptions, r as DevframeDefinition, s as DevframeRuntime, t as DevframeBrowserContext, u as DevframeWsOptions, v as ConnectionMetaWebsocket, w as DevframeServicesRegistry, wt as AgentResourceInput, x as DevframeServiceId, xt as AgentManifest, y as DevframeCapabilities, z as DevframeStorageScope } from "../devframe-qgCKL683.mjs";
import { $ as DevframeRpcServerFunctions, A as DevframeSettingsStore, At as DevframeAgentHostEvents, B as DevframeDefineDiagnosticsOptions, C as DevframeServicesHost, Ct as AgentResourceContent, D as DevframeScopedStreamingHost, Dt as AgentToolProvider, E as DevframeScopedNodeRpc, Et as AgentToolInput, F as ScopedSharedStates, G as RpcBroadcastOptions, H as DevframeDiagnosticsHost, I as SettingsForNamespace, J as RpcSharedStateHost, K as RpcFunctionsHost, L as DevframeViewHost, M as ScopedClientFunctions, Mt as EventUnsubscribe, N as ScopedRpcFn, Nt as EventsMap, O as DevframeSettings, Ot as AgentToolProviderHandle, P as ScopedServerFunctions, Q as DevframeRpcClientFunctions, R as DevframeHost, S as DevframeServiceOf, St as AgentResource, T as DevframeScopedNodeContext, Tt as AgentTool, U as DevframeDiagnosticsLogger, V as DevframeDiagnosticsDefinition, W as DevframeNodeRpcSession, X as RpcStreamingChannelOptions, Y as RpcStreamingChannel, Z as RpcStreamingHost, _ as ConnectionMeta, a as DevframeDockDefaults, b as DevframeNodeContext, bt as AgentHandle, c as DevframeSetupInfo, d as McpRouteOptions, et as DevframeRpcSharedStates, f as defineDevframe, g as Thenable, h as PartialWithoutId, i as DevframeDeploymentKind, j as ScopedBroadcastOptions, jt as EventEmitter, k as DevframeSettingsRegistry, kt as DevframeAgentHost, l as DevframeSpaOptions, m as EntriesToObject, n as DevframeCliOptions, o as DevframeDuplicationStrategy, q as RpcSharedStateGetOptions, r as DevframeDefinition, s as DevframeRuntime, t as DevframeBrowserContext, u as DevframeWsOptions, v as ConnectionMetaWebsocket, w as DevframeServicesRegistry, wt as AgentResourceInput, x as DevframeServiceId, xt as AgentManifest, y as DevframeCapabilities, z as DevframeStorageScope } from "../devframe-mQ4qZAuo.mjs";
import { g as RpcFunctionAgentOptions } from "../types-CnJSgRVa.mjs";
import { n as DevframeNodeRpcSessionMeta } from "../ws-server-J-zkiOyp.mjs";
import { n as DevframeNodeRpcSessionMeta } from "../ws-server-uKQcClcJ.mjs";
export { AgentHandle, AgentManifest, AgentResource, AgentResourceContent, AgentResourceInput, AgentTool, AgentToolInput, AgentToolProvider, AgentToolProviderHandle, ConnectionMeta, ConnectionMetaWebsocket, DevframeAgentHost, DevframeAgentHostEvents, DevframeBrowserContext, DevframeCapabilities, DevframeCliOptions, DevframeDefineDiagnosticsOptions, DevframeDefinition, DevframeDeploymentKind, DevframeDiagnosticsDefinition, DevframeDiagnosticsHost, DevframeDiagnosticsLogger, DevframeDockDefaults, DevframeDuplicationStrategy, DevframeHost, DevframeNodeContext, DevframeNodeRpcSession, type DevframeNodeRpcSessionMeta, DevframeRpcClientFunctions, DevframeRpcServerFunctions, DevframeRpcSharedStates, DevframeRuntime, DevframeScopedNodeContext, DevframeScopedNodeRpc, DevframeScopedStreamingHost, DevframeServiceId, DevframeServiceOf, DevframeServicesHost, DevframeServicesRegistry, DevframeSettings, DevframeSettingsRegistry, DevframeSettingsStore, DevframeSetupInfo, DevframeSpaOptions, DevframeStorageScope, DevframeViewHost, DevframeWsOptions, EntriesToObject, EventEmitter, EventUnsubscribe, EventsMap, McpRouteOptions, PartialWithoutId, RpcBroadcastOptions, type RpcFunctionAgentOptions, RpcFunctionsHost, RpcSharedStateGetOptions, RpcSharedStateHost, RpcStreamingChannel, RpcStreamingChannelOptions, RpcStreamingHost, ScopedBroadcastOptions, ScopedClientFunctions, ScopedRpcFn, ScopedServerFunctions, ScopedSharedStates, SettingsForNamespace, Thenable, defineDevframe };

@@ -1,2 +0,2 @@

import { Nt as EventsMap, jt as EventEmitter } from "../devframe-qgCKL683.mjs";
import { Nt as EventsMap, jt as EventEmitter } from "../devframe-mQ4qZAuo.mjs";
//#region src/utils/events.d.ts

@@ -3,0 +3,0 @@ /**

@@ -1,2 +0,2 @@

import { _t as SharedStateOptions, dt as ImmutableArray, ft as ImmutableMap, gt as SharedStateEvents, ht as SharedState, mt as ImmutableSet, pt as ImmutableObject, ut as Immutable, vt as SharedStatePatch, yt as createSharedState } from "../devframe-qgCKL683.mjs";
import { _t as SharedStateOptions, dt as ImmutableArray, ft as ImmutableMap, gt as SharedStateEvents, ht as SharedState, mt as ImmutableSet, pt as ImmutableObject, ut as Immutable, vt as SharedStatePatch, yt as createSharedState } from "../devframe-mQ4qZAuo.mjs";
export { Immutable, ImmutableArray, ImmutableMap, ImmutableObject, ImmutableSet, SharedState, SharedStateEvents, SharedStateOptions, SharedStatePatch, createSharedState };

@@ -1,2 +0,2 @@

import { at as StreamReader, ct as createStreamReader, it as StreamErrorPayload, lt as createStreamSink, nt as CreateStreamReaderOptions, ot as StreamSink, rt as CreateStreamSinkOptions, st as StreamSinkEvents, tt as BufferedChunk } from "../devframe-qgCKL683.mjs";
import { at as StreamReader, ct as createStreamReader, it as StreamErrorPayload, lt as createStreamSink, nt as CreateStreamReaderOptions, ot as StreamSink, rt as CreateStreamSinkOptions, st as StreamSinkEvents, tt as BufferedChunk } from "../devframe-mQ4qZAuo.mjs";
export { BufferedChunk, CreateStreamReaderOptions, CreateStreamSinkOptions, StreamErrorPayload, StreamReader, StreamSink, StreamSinkEvents, createStreamReader, createStreamSink };
{
"name": "devframe",
"type": "module",
"version": "0.8.1",
"version": "0.8.2",
"description": "Framework for building generic devframes",

@@ -118,3 +118,3 @@ "author": "Anthony Fu <anthonyfu117@hotmail.com>",

"whenexpr": "^0.1.2",
"ws": "^8.21.1"
"ws": "^8.21.2"
},

@@ -121,0 +121,0 @@ "scripts": {

import { n as colors } from "./diagnostics-reporter-CsIG85Q5.mjs";
import { createBuild } from "./adapters/build.mjs";
import { n as resolveDevServerPort, t as createDevServer } from "./dev-BSHFZGZr.mjs";
import process from "node:process";
import cac$1 from "cac";
//#region src/adapters/flags.ts
/**
* Identity helper that preserves the literal schema-map type — use this
* so `InferCliFlags<typeof myFlags>` resolves to the right object shape.
*
* ```ts
* const appFlags = defineCliFlags({
* depth: v.pipe(v.number(), v.integer()),
* config: v.optional(v.string()),
* })
*
* defineDevframe({
* cli: { flags: appFlags },
* setup(ctx, info) {
* const flags = info.flags as InferCliFlags<typeof appFlags>
* flags.depth // number
* flags.config // string | undefined
* },
* })
* ```
*/
function defineCliFlags(flags) {
return flags;
}
/**
* Best-effort, dependency-free probe of a schema to decide whether the
* corresponding CAC option takes a value. Duck-types the `type` /
* `wrapped` / `inner` / `pipe` fields exposed by valibot and by devframe's
* built-in `s` builder, unwrapping `optional` / `nullable` / `nullish` /
* `pipe` wrappers then matching on the inner kind. Validators that don't
* expose these fields (e.g. zod) fall through to a value-taking option.
*/
function getSchemaKind(schema) {
let current = schema;
while (current) {
const kind = current.type;
if (kind === "optional" || kind === "nullable" || kind === "nullish" || kind === "undefined") {
current = current.wrapped ?? current.inner;
continue;
}
if (kind === "pipe" && Array.isArray(current.pipe) && current.pipe.length > 0) {
current = current.pipe[0];
continue;
}
return kind ?? "unknown";
}
return "unknown";
}
/** Whether the CAC option for this schema should be a boolean flag. */
function isBooleanFlag(schema) {
return getSchemaKind(schema) === "boolean";
}
/** Validate the raw cac-parsed bag against a {@link CliFlagsSchema}. */
function parseCliFlags(schema, raw) {
const flags = {};
const issues = [];
for (const [key, fieldSchema] of Object.entries(schema)) {
const result = fieldSchema["~standard"].validate(raw[key]);
if (result instanceof Promise) {
issues.push(`--${toKebab(key)}: async flag validation is not supported`);
continue;
}
if (result.issues) issues.push(`--${toKebab(key)}: ${result.issues.map((i) => i.message).join(", ")}`);
else flags[key] = result.value;
}
for (const [key, value] of Object.entries(raw)) if (!(key in schema) && !(key in flags)) flags[key] = value;
return issues.length ? {
flags,
issues
} : { flags };
}
function toKebab(camel) {
return camel.replaceAll(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
}
/** Kebab-case a schema key for CAC option registration. */
function flagKeyToOption(camel) {
return toKebab(camel);
}
//#endregion
//#region src/adapters/cac.ts
/**
* Wrap a {@link DevframeDefinition} in a `cac`-powered command-line
* interface exposing `dev` / `build` / `mcp` subcommands.
*
* Requires the optional `cac` peer dependency.
*/
function createCac(d, options = {}) {
const defaultPort = options.defaultPort ?? d.cli?.port ?? 9999;
const defaultHost = d.cli?.host ?? "localhost";
const cli = cac$1(d.cli?.command ?? d.id);
const devCommand = cli.command("[...args]", "Start a local dev server").option("--port <port>", "Port to listen on").option("--host <host>", "Host to bind to", { default: defaultHost }).option("--open", "Open the browser on start").option("--no-open", "Do not open the browser").option("--no-auth", "Disable the interactive authentication gate").option("--mcp", "Expose an MCP server over HTTP at /__mcp (use --no-mcp to disable) [experimental]");
if (d.cli?.flags) for (const [key, schema] of Object.entries(d.cli.flags)) {
const optionName = flagKeyToOption(key);
const description = schema.description ?? "";
if (isBooleanFlag(schema)) devCommand.option(`--${optionName}`, description);
else devCommand.option(`--${optionName} <value>`, description);
}
devCommand.action(async (_args, rawFlags) => {
const flags = resolveTypedFlags(d, rawFlags);
const host = flags.host ?? defaultHost;
const port = flags.port ?? await resolveDevServerPort(d, {
host,
defaultPort
});
const mcp = flags.mcp;
await createDevServer(d, {
host,
port,
flags,
mcp,
onReady: options.onReady
});
});
if (d.capabilities?.build !== false) cli.command("build", "Build a self-contained static deploy of the devframe").option("--out-dir <outDir>", "Output directory", { default: "dist-static" }).option("--base <base>", "URL base", { default: "/" }).option("--pretty", "Pretty-print dump JSON (larger on disk)").action(async (flags) => {
await createBuild(d, {
outDir: flags.outDir,
base: flags.base,
pretty: flags.pretty
});
});
cli.command("mcp", "Start an MCP server exposing agent-facing tools (stdio) [experimental]").action(async () => {
const { createMcpServer } = await import("./adapters/mcp.mjs");
await createMcpServer(d, {
transport: "stdio",
onReady: ({ transport }) => {
console.error(`[devframe] "${d.id}" MCP server ready (${transport})`);
}
});
});
d.cli?.configure?.(cli);
options.configureCli?.(cli);
cli.help();
cli.version("0.0.0");
return {
cli,
async parse(argv = process.argv) {
cli.parse(argv, { run: false });
await cli.runMatchedCommand();
}
};
}
function resolveTypedFlags(d, raw) {
if (!d.cli?.flags) return raw;
const { flags, issues } = parseCliFlags(d.cli.flags, raw);
if (issues?.length) {
for (const issue of issues) console.error(colors.red`[devframe] invalid flag — ${issue}`);
process.exit(1);
}
return flags;
}
//#endregion
export { defineCliFlags as n, parseCliFlags as r, createCac as t };
import { n as randomToken } from "./crypto-token-XCqTSMg9.mjs";
import { t as createStorage } from "./storage-Dzoc3NVC.mjs";
import { n as revokeAuthToken, t as revokeActiveConnectionsForToken } from "./revoke-DZbJ7Cl0.mjs";
import { join } from "pathe";
//#region src/node/hub-internals/context.ts
const internalContextMap = /* @__PURE__ */ new WeakMap();
function getInternalContext(context) {
if (!internalContextMap.has(context)) {
const storage = createStorage({
filepath: join(context.host.getStorageDir("global"), "auth.json"),
initialValue: { trusted: {} }
});
const remoteTokens = /* @__PURE__ */ new Map();
function revokeRemoteToken(token) {
if (!remoteTokens.delete(token)) return;
revokeActiveConnectionsForToken(context, token);
}
const internalContext = {
storage: { auth: storage },
revokeAuthToken: (token) => revokeAuthToken(context, storage, token),
remoteTokens,
allocateRemoteToken(dockId, origin, originLock) {
const token = randomToken();
remoteTokens.set(token, {
dockId,
origin,
originLock
});
return token;
},
revokeRemoteToken,
revokeRemoteTokensForDock(dockId) {
const tokensToRevoke = [];
for (const [token, record] of remoteTokens) if (record.dockId === dockId) tokensToRevoke.push(token);
for (const token of tokensToRevoke) revokeRemoteToken(token);
},
isRemoteTokenTrusted(token, requestOrigin) {
const record = remoteTokens.get(token);
if (!record) return false;
if (!record.originLock) return true;
return !!requestOrigin && record.origin === requestOrigin;
}
};
internalContextMap.set(context, internalContext);
}
return internalContextMap.get(context);
}
//#endregion
export { internalContextMap as n, getInternalContext as t };
import { b as DevframeNodeContext, ht as SharedState } from "./devframe-qgCKL683.mjs";
//#region src/node/hub-internals/context.d.ts
interface InternalAnonymousAuthStorage {
trusted: Record<string, {
authToken: string;
ua: string;
origin: string;
timestamp: number;
} | undefined>;
}
interface RemoteTokenRecord {
dockId: string;
/** Dock URL origin — matched against WS handshake `Origin` header when `originLock` is on. */
origin: string;
originLock: boolean;
}
interface DevframeInternalContext {
storage: {
auth: SharedState<InternalAnonymousAuthStorage>;
};
/**
* Revoke an auth token: remove from storage and notify all connected clients
* using this token that they are no longer trusted.
*/
revokeAuthToken: (token: string) => Promise<void>;
/**
* Session-only tokens issued to remote-UI iframe docks. Not persisted —
* regenerated on every dev-server restart.
*/
remoteTokens: Map<string, RemoteTokenRecord>;
allocateRemoteToken: (dockId: string, origin: string, originLock: boolean) => string;
revokeRemoteToken: (token: string) => void;
revokeRemoteTokensForDock: (dockId: string) => void;
/**
* Returns true if `token` is a valid remote token and, when `originLock` is
* on, `requestOrigin` matches the recorded dock origin.
*/
isRemoteTokenTrusted: (token: string, requestOrigin?: string) => boolean;
/**
* Populated by `createWsServer` once the WS port is bound. Consumed by the
* docks host when enriching remote iframe URLs with a connection descriptor.
*/
wsEndpoint?: {
/** Full `ws://` or `wss://` URL with host and port. */
url: string;
};
}
declare const internalContextMap: WeakMap<DevframeNodeContext, DevframeInternalContext>;
declare function getInternalContext(context: DevframeNodeContext): DevframeInternalContext;
//#endregion
export { internalContextMap as a, getInternalContext as i, InternalAnonymousAuthStorage as n, RemoteTokenRecord as r, DevframeInternalContext as t };
import { DEVFRAME_CONNECTION_META_FILENAME } from "./constants.mjs";
import { n as createHostContext, t as createH3DevframeHost } from "./host-h3-Kz7t5Xab.mjs";
import { t as diagnostics } from "./diagnostics-B5-qHeqD.mjs";
import { r as registerDevframeInstance } from "./instance-registry-D6fxYu36.mjs";
import { i as normalizeHttpServerUrl, t as startHttpAndWs } from "./server-CXaDDUl0.mjs";
import { n as resolveBasePath, t as normalizeBasePath } from "./_shared-bWRzeSa0.mjs";
import { t as open } from "./open-Deb5xmIT.mjs";
import { mountStaticHandler } from "./utils/serve-static.mjs";
import { createInteractiveAuth } from "./recipes/interactive-auth.mjs";
import { resolve } from "pathe";
import process$1 from "node:process";
import { networkInterfaces } from "node:os";
import { H3 } from "h3";
import { createServer } from "node:net";
import { joinURL, withBase, withLeadingSlash, withoutLeadingSlash } from "ufo";
//#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
//#region src/adapters/dev.ts
const DEFAULT_PORT = 9999;
/**
* Resolve the listening port for {@link createDevServer}, honoring the
* definition's `cli.port` / `cli.portRange` / `cli.random` settings.
* Exposed separately so authors who run their own argv parsing can
* resolve a port up-front (to print it, log it, etc.) before starting
* the server.
*/
async function resolveDevServerPort(def, options = {}) {
const host = options.host ?? def.cli?.host ?? "localhost";
const portOptions = {
port: options.defaultPort ?? def.cli?.port ?? DEFAULT_PORT,
host
};
if (def.cli?.portRange) portOptions.portRange = def.cli.portRange;
if (def.cli?.random) portOptions.random = def.cli.random;
return getPort(portOptions);
}
/**
* Start a devframe dev server for a {@link DevframeDefinition} —
* h3 + WebSocket RPC + (optionally) the author's SPA mounted at the
* resolved base path.
*
* When `distDir` is omitted (and `def.cli?.distDir` is unset) the
* server runs in **bridge mode**: only `__connection.json` and the WS
* endpoint are mounted, with no SPA mount. The SPA is expected to be
* hosted elsewhere (e.g. by a parent Vite/Nuxt dev server) — see
* `viteDevBridge({ devMiddleware })`.
*
* Returns the underlying {@link StartedServer} handle so callers can
* close it gracefully (SIGINT, hot-reload, test teardown).
*
* Use this directly when integrating devframe into an existing CLI
* framework (commander, yargs, hand-rolled CAC). For the all-in-one
* `dev` / `build` / `mcp` shell, reach for {@link createCac} instead.
*/
async function createDevServer(def, options = {}) {
const distDir = options.distDir ?? def.cli?.distDir;
const host = options.host ?? def.cli?.host ?? "localhost";
const port = options.port ?? await resolveDevServerPort(def, { host });
const flags = options.flags ?? {};
const basePath = options.basePath ? normalizeBasePath(options.basePath) : resolveBasePath(def, "standalone");
const app = options.app ?? new H3();
const h3Host = createH3DevframeHost({
origin: normalizeHttpServerUrl(host, port),
appName: def.id,
mount: (base, dir) => {
mountStaticHandler(app, base, dir);
}
});
const ctx = await createHostContext({
cwd: process$1.cwd(),
mode: "dev",
host: h3Host
});
const setupInfo = { flags };
await def.setup(ctx, setupInfo);
const mcpConfig = resolveMcpConfig(options.mcp ?? def.cli?.mcp);
let mcpDispose;
let mcpMeta;
if (mcpConfig) {
const mcpRoute = withoutLeadingSlash(mcpConfig.path ?? "__mcp");
const mcpPath = joinURL(basePath, mcpRoute);
let mountMcpHttp;
try {
({mountMcpHttp} = await import("./http-Dvgxujcu.mjs"));
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw diagnostics.DF0017({
transport: "http",
reason,
cause: error
});
}
mcpDispose = mountMcpHttp(app, ctx, mcpPath, {
serverName: `${def.id} (devframe)`,
serverVersion: def.version ?? "0.0.0",
exposeSharedState: true,
allowedOrigins: mcpConfig.allowedOrigins
}).dispose;
mcpMeta = { path: mcpRoute };
}
const { bindPath, wsPort, meta } = resolveWsConnection(def, options, basePath);
const connectionMetaPath = joinURL(basePath, DEVFRAME_CONNECTION_META_FILENAME);
app.use(connectionMetaPath, () => ({
backend: "websocket",
websocket: meta,
...mcpMeta ? { mcp: mcpMeta } : {}
}));
if (distDir) mountStaticHandler(app, basePath, resolve(distDir));
const authOption = flags.auth === false ? false : options.auth !== void 0 ? options.auth : def.cli?.auth;
let authHandler;
let resolvedAuth;
if (authOption === false) resolvedAuth = false;
else if (typeof authOption === "object") {
authHandler = authOption;
resolvedAuth = authOption;
} else {
authHandler = createInteractiveAuth(ctx);
resolvedAuth = authHandler;
}
const started = await startHttpAndWs({
context: ctx,
host,
port,
app,
path: bindPath,
wsPort,
auth: resolvedAuth,
onReady: async (info) => {
authHandler?.printBanner();
await options.onReady?.(info);
await maybeOpenBrowser(def, flags, `${info.origin}${basePath}`, options.openBrowser, authHandler);
}
});
const registration = registerDevframeInstance({
pid: process$1.pid,
port: started.port,
origin: normalizeHttpServerUrl(host, started.port),
basePath,
id: def.id,
name: def.name,
rootDir: process$1.cwd(),
mcp: mcpConfig ? { path: joinURL(basePath, withoutLeadingSlash(mcpConfig.path ?? "__mcp")) } : null,
startedAt: Date.now()
});
const closeServer = started.close;
started.close = async () => {
registration.unregister();
await mcpDispose?.();
await closeServer();
};
return started;
}
/**
* Normalize the `cli.mcp` / `mcp` option (`boolean | McpRouteOptions`) into
* concrete options, or `undefined` when the MCP route is disabled.
*/
function resolveMcpConfig(mcp) {
if (!mcp) return void 0;
return mcp === true ? {} : mcp;
}
/**
* Resolve the `mcp` entry a `__connection.json` should advertise for a dev
* server started with the given `mcp` option (falling back to `def.cli?.mcp`,
* exactly like {@link createDevServer}), or `undefined` when the route is
* disabled.
*
* Hosted bridges that hand-roll their connection meta (`viteDevBridge`,
* `@devframes/next`'s handler) pass the side-car `port`: the advertised path
* becomes absolute (the side-car mounts at `/`) and the client dials
* `<page-host>:<port><path>`. Without `port` the path stays relative, resolved
* against `__connection.json`'s own location (the same-server default).
*
* @experimental
*/
function resolveMcpConnectionMeta(def, mcp, port) {
const config = resolveMcpConfig(mcp ?? def.cli?.mcp);
if (!config) return void 0;
const route = withoutLeadingSlash(config.path ?? "__mcp");
return port != null ? {
path: withLeadingSlash(route),
port
} : { path: route };
}
/**
* Resolve the three WS connection scenarios from the definition / call-site
* config into a concrete server bind path, optional dedicated port, and the
* `__connection.json` descriptor the browser resolves.
*/
function resolveWsConnection(def, options, basePath) {
const ws = options.ws ?? def.cli?.ws ?? {};
const route = withoutLeadingSlash(ws.route ?? "__devframe_ws");
if (ws.url) return {
bindPath: joinURL(basePath, route),
wsPort: void 0,
meta: ws.url
};
if (ws.port != null) return {
bindPath: withLeadingSlash(route),
wsPort: ws.port,
meta: {
port: ws.port,
path: route
}
};
return {
bindPath: joinURL(basePath, route),
wsPort: void 0,
meta: { path: route }
};
}
async function maybeOpenBrowser(def, flags, origin, override, authHandler) {
const flagsOpen = flags.open;
const cliOpen = def.cli?.open;
const resolved = override ?? flagsOpen ?? cliOpen;
if (resolved === void 0 || resolved === false) return;
const target = typeof resolved === "string" ? withBase(resolved, origin) : origin;
const authorizedTarget = authHandler?.buildOpenUrl?.(target) ?? target;
try {
await open(authorizedTarget);
} catch {}
}
//#endregion
export { resolveDevServerPort as n, resolveMcpConnectionMeta as r, createDevServer as t };

Sorry, the diff of this file is too big to display

import { t as devframeReporter } from "./diagnostics-reporter-CsIG85Q5.mjs";
import { defineDiagnostics } from "nostics";
//#region src/node/diagnostics.ts
const diagnostics = defineDiagnostics({
docsBase: "https://devfra.me/errors",
reporters: [devframeReporter],
codes: {
DF0006: { why: (p) => `RPC function "${p.name}" is not registered` },
DF0007: { why: "AsyncLocalStorage is not set, it likely to be an internal bug of the Devframe foundation" },
DF0008: { why: (p) => `distDir ${p.distDir} does not exist` },
DF0012: { why: (p) => `Failed to parse storage file: ${p.filepath}, falling back to defaults.` },
DF0013: { why: (p) => `Shared state of "${p.key}" is not found, please provide an initial value for the first time` },
DF0014: {
why: (p) => `RPC function "${p.name}" has an invalid \`agent\` field — \`description\` must be a non-empty string.`,
fix: "Provide a short description (~1–3 sentences) explaining what the tool does and when agents should invoke it."
},
DF0015: {
why: (p) => `Agent tool "${p.id}" is already registered.`,
fix: "Tool ids must be unique across RPC functions with an `agent` field and tools registered via `ctx.agent.registerTool()`."
},
DF0016: { why: (p) => `Agent resource "${p.id}" is already registered.` },
DF0017: { why: (p) => `Failed to start MCP server (${p.transport}): ${p.reason}` },
DF0029: {
why: (p) => `Stream "${p.channel}#${p.id}" dropped ${p.dropped} chunk(s) after exceeding the client high-water mark.`,
fix: "The consumer is too slow for the producer. Raise `highWaterMark` on the subscription, slow the producer, or batch chunks."
},
DF0030: {
why: (p) => `Stream "${p.channel}#${p.id}" is unknown — no producer has called \`channel.start({ id: "${p.id}" })\`.`,
fix: "Ensure the server-side producer is running before clients subscribe, or check for typos in the stream id."
},
DF0031: {
why: (p) => `Cannot write to closed stream "${p.channel}#${p.id}".`,
fix: "Track the producer lifecycle — guard writes with the `stream.signal.aborted` flag."
},
DF0032: {
why: (p) => `Streaming channel "${p.channel}" is already registered.`,
fix: "Each channel name must be unique within a context. Pick a different name or reuse the existing channel handle."
},
DF0033: {
why: (p) => `Failed to start dev RPC bridge for "${p.id}": ${p.reason}`,
fix: "Verify the bridge port is free and the devframe setup function does not throw. Pin a port via `cli.port` / `cli.portRange` on the definition, or via `devMiddleware.port` on `viteDevBridge`."
},
DF0034: {
why: (p) => `Scoped RPC registration for namespace "${p.namespace}" received an already-namespaced function name "${p.name}".`,
fix: "A scoped context auto-namespaces ids. Pass a bare name without a \":\" separator (e.g. `register({ name: \"get-cwd\" })`), or use the unscoped `ctx.base.rpc.register` for a fully-qualified name."
},
DF0035: {
why: (p) => `Failed to persist storage file: ${p.filepath}`,
fix: "Check that the storage directory is writable and has free space."
},
DF0036: {
why: (p) => `RPC call to "${p.name}" was rejected: the caller is not authorized.`,
fix: "Complete the auth handshake (or connect with a static/pre-shared token) before calling a trusted method. Untrusted callers may only call `anonymous:`-prefixed methods — see `isAnonymousRpcMethod`."
},
DF0037: {
why: (p) => `A service is already provided under "${p.id}".`,
fix: "Service ids are unique per context. Revoke the existing provider first (the `provide()` call returns a revoke function), or namespace the id with your plugin id to avoid collisions."
},
DF0042: {
why: (p) => `"${p.id}" declares \`capabilities.build: false\` — its static export is not meaningful (writes are excluded and any live-served data won't be there).`,
fix: "Pass `{ force: true }` to `createBuild()` if the degraded export is still useful to you, or drop `capabilities.build: false` on the definition."
},
DF0045: {
why: (p) => `Failed to update the devframe instance registry at "${p.file}": ${p.reason}`,
fix: "Discovery tooling (`devframe connect`) will not see this instance. Check that the registry directory is writable, point `DEVFRAME_INSTANCES_DIR` at a writable directory, or set `DEVFRAME_DISABLE_INSTANCE_REGISTRY=1` to opt out of registration."
},
DF0046: {
why: (p) => `\`devframe connect\` requires the optional peer dependency @modelcontextprotocol/server: ${p.reason}`,
fix: "Install it next to devframe (e.g. `npm install @modelcontextprotocol/server`) and run `devframe connect` again."
},
DF0047: {
why: (p) => `Agent tool "${p.id}" is hidden from the MCP surface: its wire name "${p.name}" collides with the tool "${p.existing}".`,
fix: "Wire names derive from tool ids (characters outside [a-zA-Z0-9_-] become \"_\"). Rename one of the two ids so they sanitize to distinct names."
},
DF0048: {
why: (p) => `Unknown shared-state key "${p.key}".`,
fix: "Call the devframe_state_read tool without arguments to list the available keys, then retry with one of them."
},
DF0049: {
why: "The devframe_connect_call-tool tool requires { port: number, tool: string }.",
fix: "Call devframe_connect_list-instances to get the port and tool names, then retry."
},
DF0050: {
why: (p) => `No running devframe instance on port ${p.port}.`,
fix: "Call devframe_connect_list-instances for the current instance list — the instance may have stopped or changed port."
},
DF0051: {
why: (p) => `The devframe instance on port ${p.port} has no MCP endpoint.`,
fix: "Restart the instance with the --mcp flag (or set `cli.mcp: true` on its definition) to expose its tools, then list instances again."
}
}
});
//#endregion
export { diagnostics as t };
import { isAllowedOrigin } from "./rpc/transports/ws-server.mjs";
import { n as createHostContext } from "./host-h3-Kz7t5Xab.mjs";
import { t as diagnostics } from "./diagnostics-B5-qHeqD.mjs";
import { t as toAgentToolName } from "./agent-tool-name-C3b5vEwJ.mjs";
import { randomUUID } from "node:crypto";
import { Diagnostic } from "nostics";
import { join } from "pathe";
import process from "node:process";
import { homedir } from "node:os";
import { Server, WebStandardStreamableHTTPServerTransport, isInitializeRequest } from "@modelcontextprotocol/server";
//#region src/adapters/mcp/stringify.ts
/**
* JSON-coercing serializer for MCP text payloads.
*
* MCP carries tool results and resource reads as plain text over a
* JSON-RPC transport, so we cannot use the `s:`-prefixed structured-clone
* format the WS RPC transport falls back to for non-JSON values. Instead,
* we coerce common non-JSON types into JSON-friendly forms so the LLM
* client sees something useful instead of `[object Object]`.
*
* Coercions:
* - `BigInt` → `"123n"`
* - `Date` → ISO string (via the native `toJSON`)
* - `Map` → `{ __type: 'Map', entries: [[k, v], …] }`
* - `Set` → `{ __type: 'Set', entries: [v, …] }`
* - `Error` → `{ name, message, stack, cause? }` (cause recurses)
* - `Function` → `"[Function: name]"`
* - `Symbol` → `value.toString()`
* - cycles → `"[Circular]"`
*/
function stringifyForMcp(value) {
if (value === void 0) return "undefined";
if (typeof value === "string") return value;
const seen = /* @__PURE__ */ new WeakSet();
return JSON.stringify(value, (_key, val) => {
if (typeof val === "bigint") return `${val}n`;
if (val instanceof Error) {
const out = {
name: val.name,
message: val.message,
stack: val.stack
};
if (val.cause !== void 0) out.cause = val.cause;
return out;
}
if (val instanceof Map) return {
__type: "Map",
entries: [...val.entries()]
};
if (val instanceof Set) return {
__type: "Set",
entries: [...val]
};
if (typeof val === "function") return `[Function: ${val.name || "anonymous"}]`;
if (typeof val === "symbol") return val.toString();
if (val !== null && typeof val === "object") {
if (seen.has(val)) return "[Circular]";
seen.add(val);
}
return val;
}, 2);
}
/**
* Format a thrown value for an MCP `isError` text payload.
*
* A nostics `Diagnostic` (every coded devframe error) becomes structured
* JSON — `{ error: { code, message, fix?, docs? } }` — so an agent receives
* the actionable next step (`fix`) and the docs URL instead of a bare
* message string. Other errors surface `Error.name`/`message`, plus one
* level of `cause.message` so context isn't dropped silently.
*/
function formatMcpError(error) {
if (error instanceof Diagnostic) return JSON.stringify({ error: {
code: error.code,
message: error.message,
...error.fix ? { fix: error.fix } : {},
...error.docs ? { docs: error.docs } : {}
} }, null, 2);
if (!(error instanceof Error)) return String(error);
const cause = error.cause;
const causeText = cause instanceof Error ? ` (cause: ${cause.message})` : cause !== void 0 ? ` (cause: ${String(cause)})` : "";
return `${error.name}: ${error.message}${causeText}`;
}
//#endregion
//#region src/adapters/mcp/to-json-schema.ts
const FALLBACK_OBJECT_SCHEMA = Object.freeze({
type: "object",
additionalProperties: true
});
/**
* Convert a Standard Schema to JSON Schema for the agent/MCP surface.
*
* Devframe stays validator-neutral, so conversion uses the schema's own
* [Standard JSON Schema](https://standardschema.dev/) converter
* (`~standard.jsonSchema`) when the validator provides one — zod 4 does,
* for example. Validators without a native converter (e.g. valibot) degrade
* to a permissive object schema rather than pulling in a converter library.
*/
function safeToJsonSchema(schema) {
const standard = schema["~standard"];
if (standard.jsonSchema) try {
return standard.jsonSchema.input({ target: "draft-2020-12" });
} catch {
return FALLBACK_OBJECT_SCHEMA;
}
return FALLBACK_OBJECT_SCHEMA;
}
/**
* JSON Schema for an RPC return value on the agent/MCP surface.
* @internal
*/
function returnToJsonSchema(schema) {
if (!schema) return void 0;
return safeToJsonSchema(schema);
}
/**
* JSON Schema for an RPC function's positional args on the agent/MCP
* surface. Each positional arg is advertised under `arg0` / `arg1` / … —
* matching how the agent bridge coerces the incoming object payload back
* into positional arguments.
*
* Returns `{ type: 'object', properties: {} }` when there are no args.
* @internal
*/
function argsToJsonSchema(args) {
if (!args || args.length === 0) return {
schema: {
type: "object",
properties: {}
},
unwrapped: false
};
const properties = {};
const required = [];
for (let i = 0; i < args.length; i++) {
const key = `arg${i}`;
properties[key] = safeToJsonSchema(args[i]);
required.push(key);
}
return {
schema: {
type: "object",
properties,
required,
additionalProperties: false
},
unwrapped: false
};
}
//#endregion
//#region src/adapters/mcp/build-server.ts
/**
* Wire an MCP {@link Server} to a devframe context. Returns the server
* plus a disposal function for the subscriptions it sets up. The
* transport is the caller's responsibility — `createMcpServer` connects
* stdio; tests can connect an {@link InMemoryTransport} instead.
*
* @internal
*/
function buildMcpServerFromContext(ctx, options) {
const server = new Server({
name: options.serverName,
version: options.serverVersion
}, { capabilities: {
tools: { listChanged: true },
resources: { listChanged: true }
} });
registerToolHandlers(server, ctx, options.exposeSharedState);
registerResourceHandlers(server, ctx, options.exposeSharedState);
const notify = (method) => {
server.notification({ method }).catch(() => {});
};
const offManifest = ctx.agent.events.on("agent:manifest:changed", () => {
notify("notifications/tools/list_changed");
notify("notifications/resources/list_changed");
});
const offKeyAdded = ctx.rpc.sharedState.onKeyAdded(() => {
notify("notifications/resources/list_changed");
});
return {
server,
dispose: () => {
offManifest();
offKeyAdded();
}
};
}
/**
* Build an MCP server over the agent surface of a devframe definition.
* Currently supports `stdio` transport only.
*
* @experimental The agent-native surface is experimental and may change
* without a major version bump until it stabilizes.
*/
async function createMcpServer(definition, options = {}) {
const transport = options.transport ?? "stdio";
if (transport !== "stdio") throw diagnostics.DF0017({
transport,
reason: "Only stdio transport is supported in this release."
});
const ctx = await createHostContext({
cwd: process.cwd(),
mode: "dev",
host: {
mountStatic: () => {},
resolveOrigin: () => "mcp://devframe",
getStorageDir: (scope) => {
if (scope === "workspace") return join(process.cwd(), ".devframe");
if (scope === "project") return join(process.cwd(), `node_modules/.${definition.id}/devframe`);
return join(homedir(), `.${definition.id}/devframe`);
}
}
});
await definition.setup(ctx);
const { server, dispose } = buildMcpServerFromContext(ctx, {
serverName: options.serverName ?? `${definition.id} (devframe)`,
serverVersion: options.serverVersion ?? definition.version ?? "0.0.0",
exposeSharedState: options.exposeSharedState ?? true
});
const { startStdioTransport } = await import("./transports-vhizgqXM.mjs");
let stop;
try {
stop = await startStdioTransport(server);
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw diagnostics.DF0017({
transport,
reason,
cause: error
});
}
options.onReady?.({ transport: "stdio" });
return { async stop() {
dispose();
await stop();
} };
}
/**
* Id of the built-in shared-state read tool — namespaced like every other
* built-in (`devframe:<area>:<fn>`). Tool-shaped access matters because many
* MCP clients only consume tools — the parallel `devframe://state/<key>`
* resource projection stays for the clients that do read resources.
*/
const READ_STATE_TOOL = "devframe:state:read";
/** Wire name of the built-in shared-state read tool: `devframe_state_read`. */
const READ_STATE_NAME = toAgentToolName(READ_STATE_TOOL);
function sharedStateFilter(exposeSharedState) {
if (exposeSharedState === false) return void 0;
return typeof exposeSharedState === "function" ? exposeSharedState : () => true;
}
function readStateToolProjection() {
return {
name: READ_STATE_NAME,
title: "Read shared state",
description: "Read this devtool's live shared state. Call without arguments to list the available keys, then with a key to get that value as JSON. Safe to call freely.",
inputSchema: {
type: "object",
properties: { key: {
type: "string",
description: "A shared-state key from the key list. Omit to list all keys."
} }
},
annotations: {
title: "Read shared state",
readOnlyHint: true,
destructiveHint: false
}
};
}
async function readStateResult(ctx, filter, key) {
const keys = ctx.rpc.sharedState.keys().filter(filter);
if (key === void 0) return { keys };
if (!keys.includes(key)) throw diagnostics.DF0048({ key });
return {
key,
value: (await ctx.rpc.sharedState.get(key)).value()
};
}
function registerToolHandlers(server, ctx, exposeSharedState) {
const stateFilter = sharedStateFilter(exposeSharedState);
const warnedCollisions = /* @__PURE__ */ new Set();
/**
* Resolve a wire tool name back to the registered {@link AgentTool}.
* Wire-name matching runs first, in manifest order — the same tool the
* list projection advertises under that name — with a raw-id fallback so
* a colon-namespaced id keeps working as a call name.
*/
const resolveTool = (name) => {
return ctx.agent.list().tools.find((tool) => toAgentToolName(tool.id) === name) ?? ctx.agent.getTool(name);
};
server.setRequestHandler("tools/list", async () => {
const byName = /* @__PURE__ */ new Map();
for (const tool of ctx.agent.list().tools) {
const name = toAgentToolName(tool.id);
const existing = byName.get(name);
if (existing) {
if (!warnedCollisions.has(`${name}|${tool.id}`)) {
warnedCollisions.add(`${name}|${tool.id}`);
diagnostics.DF0047({
name,
id: tool.id,
existing: existing.id
});
}
continue;
}
byName.set(name, tool);
}
const tools = [...byName.entries()].map(([name, tool]) => projectTool(name, tool, ctx));
if (stateFilter && !byName.has(READ_STATE_NAME)) tools.push(readStateToolProjection());
return { tools };
});
server.setRequestHandler("tools/call", async (request) => {
const { name, arguments: args } = request.params;
try {
const tool = resolveTool(name);
if (stateFilter && !tool && (name === READ_STATE_NAME || name === READ_STATE_TOOL)) {
const key = args?.key;
const result = await readStateResult(ctx, stateFilter, key);
return {
content: [{
type: "text",
text: stringifyForMcp(result)
}],
structuredContent: result
};
}
const outputSchema = tool ? usableOutputSchema(tool.outputSchema ?? computeOutputSchema(tool, ctx)) : void 0;
const result = await ctx.agent.invoke(tool?.id ?? name, args ?? {});
return {
content: [{
type: "text",
text: stringifyForMcp(result)
}],
...outputSchema ? { structuredContent: result } : {}
};
} catch (error) {
return {
isError: true,
content: [{
type: "text",
text: `Error invoking "${name}": ${formatMcpError(error)}`
}]
};
}
});
}
function registerResourceHandlers(server, ctx, exposeSharedState) {
server.setRequestHandler("resources/list", async () => {
const resources = ctx.agent.list().resources.map((resource) => ({
uri: resource.uri,
name: resource.name,
description: resource.description,
mimeType: resource.mimeType
}));
if (exposeSharedState !== false) {
const filter = typeof exposeSharedState === "function" ? exposeSharedState : () => true;
for (const key of ctx.rpc.sharedState.keys()) {
if (!filter(key)) continue;
resources.push({
uri: `devframe://state/${encodeURIComponent(key)}`,
name: key,
description: `Shared state: ${key}`,
mimeType: "application/json"
});
}
}
return { resources };
});
server.setRequestHandler("resources/read", async (request) => {
const { uri } = request.params;
const parsed = parseResourceUri(uri);
if (parsed.kind === "resource") {
const content = await ctx.agent.read(parsed.id);
return { contents: [{
uri,
mimeType: content.mimeType ?? "application/json",
text: content.text ?? stringifyForMcp(content.json)
}] };
}
if (parsed.kind === "state") return { contents: [{
uri,
mimeType: "application/json",
text: stringifyForMcp((await ctx.rpc.sharedState.get(parsed.key)).value())
}] };
throw new Error(`[devframe/mcp] unknown resource URI "${uri}"`);
});
}
/**
* MCP constrains a tool's `outputSchema` to a JSON Schema of `type:
* "object"` — clients (the SDK included) reject anything else. Non-object
* return schemas (e.g. a schema for `void` / a bare string) simply project
* no output schema; the text content still carries the result.
*/
function usableOutputSchema(schema) {
return schema && typeof schema === "object" && schema.type === "object" ? schema : void 0;
}
function projectTool(name, tool, ctx) {
const inputSchema = tool.inputSchema ?? computeInputSchema(tool, ctx);
const outputSchema = usableOutputSchema(tool.outputSchema ?? computeOutputSchema(tool, ctx));
return {
name,
title: tool.title,
description: tool.description,
inputSchema,
...outputSchema ? { outputSchema } : {},
annotations: {
title: tool.title,
readOnlyHint: tool.safety === "read",
destructiveHint: tool.safety === "destructive"
}
};
}
function computeInputSchema(tool, ctx) {
if (tool.kind === "tool") return argsToJsonSchema(tool.args).schema;
if (tool.kind !== "rpc" || !tool.rpcName) return {
type: "object",
properties: {}
};
const def = ctx.rpc.definitions.get(tool.rpcName);
if (!def) return {
type: "object",
properties: {}
};
const args = def.args;
return argsToJsonSchema(args).schema;
}
function computeOutputSchema(tool, ctx) {
if (tool.kind !== "rpc" || !tool.rpcName) return void 0;
const def = ctx.rpc.definitions.get(tool.rpcName);
if (!def) return void 0;
return returnToJsonSchema(def.returns);
}
function parseResourceUri(uri) {
const match = uri.match(/^devframe:\/\/(resource|state)\/(.+)$/);
if (!match) return { kind: "unknown" };
const [, kind, rest] = match;
const decoded = decodeURIComponent(rest);
if (kind === "resource") return {
kind: "resource",
id: decoded
};
return {
kind: "state",
key: decoded
};
}
//#endregion
//#region src/adapters/mcp/fetch.ts
/**
* Build a framework-agnostic MCP Streamable-HTTP endpoint over a devframe
* context: a web-standard `Request → Response` handler any host can mount —
* h3 (see `mountMcpHttp`), a Next.js App Router route, or any other
* fetch-shaped server.
*
* Each MCP session gets its own {@link WebStandardStreamableHTTPServerTransport}
* and MCP server (built from the shared, live `ctx` via
* `buildMcpServerFromContext`), correlated by the `Mcp-Session-Id` header: an
* `initialize` POST spins up a session; later requests route to it; a `DELETE`
* (or client disconnect) tears it down. The origin gate guards every request:
* loopback-default DNS-rebinding protection that — unlike the WS upgrade's
* `isAllowedOrigin` — also rejects `Origin`-less requests, so a route-based
* endpoint isn't reachable by an arbitrary local process.
*
* @experimental
*/
function createMcpFetchHandler(ctx, options) {
const sessions = /* @__PURE__ */ new Map();
const allowedOrigins = options.allowedOrigins;
function drop(sessionId) {
const session = sessions.get(sessionId);
if (!session) return;
sessions.delete(sessionId);
session.dispose();
}
async function createSession() {
let session;
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (id) => {
sessions.set(id, session);
},
onsessionclosed: (id) => {
drop(id);
}
});
const { server, dispose } = buildMcpServerFromContext(ctx, {
serverName: options.serverName,
serverVersion: options.serverVersion,
exposeSharedState: options.exposeSharedState
});
session = {
transport,
dispose: async () => {
dispose();
await server.close();
}
};
transport.onclose = () => {
if (transport.sessionId) drop(transport.sessionId);
};
await server.connect(transport);
return session;
}
async function handle(req) {
const origin = req.headers.get("origin") ?? void 0;
if (allowedOrigins !== false && (origin === void 0 || !isAllowedOrigin(origin, allowedOrigins ?? []))) return new Response("Forbidden: origin required", { status: 403 });
const sessionId = req.headers.get("mcp-session-id") ?? void 0;
let session = sessionId ? sessions.get(sessionId) : void 0;
if (!session && req.method === "POST") {
let body;
try {
body = await req.json();
} catch {
body = void 0;
}
if (!sessionId && isInitializeRequest(body)) session = await createSession();
else return new Response(sessionId ? "Not Found: unknown MCP session" : "Bad Request: no valid session ID and not an initialize request", { status: sessionId ? 404 : 400 });
return session.transport.handleRequest(req, { parsedBody: body });
}
if (!session) return new Response(sessionId ? "Not Found: unknown MCP session" : "Bad Request: missing MCP session ID", { status: sessionId ? 404 : 400 });
return session.transport.handleRequest(req);
}
return {
fetch: handle,
dispose: async () => {
const live = [...sessions.values()];
sessions.clear();
await Promise.all(live.map((session) => session.dispose()));
}
};
}
//#endregion
export { createMcpServer as n, createMcpFetchHandler as t };
import { t as devframeReporter } from "./diagnostics-reporter-CsIG85Q5.mjs";
import { t as diagnostics } from "./diagnostics-hwjXp_UV.mjs";
import { RpcFunctionsCollectorBase } from "./rpc/index.mjs";
import { defineRpcFunction } from "./index.mjs";
import { t as diagnostics$1 } from "./diagnostics-B5-qHeqD.mjs";
import { i as createEventEmitter, n as createSharedState, r as nanoid, t as createStorage } from "./storage-Dzoc3NVC.mjs";
import { defineDiagnostics } from "nostics";
import { isatty } from "node:tty";
import { formatWithOptions, inspect } from "node:util";
import { existsSync } from "node:fs";
import { join } from "pathe";
import process$1 from "node:process";
import { homedir } from "node:os";
//#region src/node/agent-args.ts
/**
* Map the args payload an agent surface receives (MCP sends an object
* keyed `arg0`/`arg1`/…, matching the schema the adapter advertises) onto
* a handler's positional parameters. Shared by the agent host's RPC
* bridge and the hub's command-derived tools so the coercion cannot
* drift between them.
*
* - an array passes through as-is
* - `null`/`undefined` become a zero-argument call
* - with declared schemas, each schema reads its own `argN` key, in order
* - without schemas, `arg0`/`arg1`/… keys are collected when present
* - an empty object becomes a zero-argument call
* - anything else follows the {@link AgentArgsFallback}
*
* @experimental
*/
function coerceAgentPositionalArgs(args, schemas, fallback = "wrap") {
if (Array.isArray(args)) return args;
if (args === void 0 || args === null) return [];
if (typeof args === "object") {
const obj = args;
if (schemas && schemas.length) return schemas.map((_, i) => obj[`arg${i}`]);
if ("arg0" in obj) {
const out = [];
let i = 0;
while (`arg${i}` in obj) {
out.push(obj[`arg${i}`]);
i++;
}
return out;
}
if (Object.keys(obj).length === 0) return [];
}
return fallback === "drop" ? [] : [args];
}
//#endregion
//#region src/node/host-agent.ts
/**
* Framework-neutral host aggregating the agent-exposed surface of a
* devframe. Auto-discovers RPC functions with an `agent` field from
* `ctx.rpc.definitions`, and accepts plugin-registered tools /
* resources via `registerTool` / `registerResource`.
*
* @experimental
*/
var DevframeAgentHost = class {
context;
events = createEventEmitter();
tools = /* @__PURE__ */ new Map();
resources = /* @__PURE__ */ new Map();
providers = /* @__PURE__ */ new Set();
_rpcUnsubscribe;
constructor(context) {
this.context = context;
this._rpcUnsubscribe = context.rpc.onChanged(() => {
this.events.emit("agent:manifest:changed");
});
}
registerTool(input) {
this._validateToolId(input.id);
const tool = this._projectTool(input);
this.tools.set(tool.id, {
tool,
handler: input.handler
});
this.events.emit("agent:tool:registered", tool);
this.events.emit("agent:manifest:changed");
return { unregister: () => this.unregisterTool(tool.id) };
}
unregisterTool(id) {
const existed = this.tools.delete(id);
if (existed) {
this.events.emit("agent:tool:unregistered", id);
this.events.emit("agent:manifest:changed");
}
return existed;
}
registerToolProvider(provider) {
this.providers.add(provider);
this.events.emit("agent:manifest:changed");
const notifyChanged = () => {
if (this.providers.has(provider)) this.events.emit("agent:manifest:changed");
};
return {
notifyChanged,
unregister: () => {
if (this.providers.delete(provider)) this.events.emit("agent:manifest:changed");
}
};
}
registerResource(input) {
if (this.resources.has(input.id)) throw diagnostics$1.DF0016({ id: input.id });
const resource = {
id: input.id,
name: input.name,
description: input.description,
mimeType: input.mimeType ?? "application/json",
uri: input.uri ?? `devframe://resource/${encodeURIComponent(input.id)}`
};
this.resources.set(resource.id, {
resource,
read: input.read
});
this.events.emit("agent:resource:registered", resource);
this.events.emit("agent:manifest:changed");
return { unregister: () => this.unregisterResource(resource.id) };
}
unregisterResource(id) {
const existed = this.resources.delete(id);
if (existed) {
this.events.emit("agent:resource:unregistered", id);
this.events.emit("agent:manifest:changed");
}
return existed;
}
list() {
const rpcTools = this._collectRpcTools();
const plainTools = Array.from(this.tools.values()).map((t) => t.tool);
const resources = Array.from(this.resources.values()).map((r) => r.resource);
const seen = new Set([...rpcTools, ...plainTools].map((t) => t.id));
const providerTools = [];
for (const { tool } of this._collectProviderTools()) {
if (seen.has(tool.id)) continue;
seen.add(tool.id);
providerTools.push(tool);
}
return {
tools: [
...rpcTools,
...plainTools,
...providerTools
],
resources
};
}
getTool(id) {
const plain = this.tools.get(id);
if (plain) return plain.tool;
const rpc = this._collectRpcTools().find((t) => t.id === id);
if (rpc) return rpc;
return this._collectProviderTools().find((t) => t.tool.id === id)?.tool;
}
getResource(id) {
return this.resources.get(id)?.resource;
}
async invoke(id, args) {
const plain = this.tools.get(id);
if (plain?.handler) return await plain.handler(args);
const rpcDef = this._findRpcDefinition(id);
if (rpcDef) {
const positional = coerceAgentPositionalArgs(args, rpcDef.args, "wrap");
return await this.context.rpc.invokeLocal(id, ...positional);
}
const provided = this._collectProviderTools().find((t) => t.tool.id === id);
if (provided) return await provided.input.handler(args);
throw new Error(`[devframe/agent] tool "${id}" not found`);
}
async read(id) {
const entry = this.resources.get(id);
if (!entry) throw new Error(`[devframe/agent] resource "${id}" not found`);
return await entry.read();
}
/** @internal */
_dispose() {
this._rpcUnsubscribe?.();
this._rpcUnsubscribe = void 0;
}
_validateToolId(id) {
if (this.tools.has(id)) throw diagnostics$1.DF0015({ id });
if (this.context.rpc.definitions.get(id)?.agent) throw diagnostics$1.DF0015({ id });
}
_projectTool(input) {
if (!input.description || typeof input.description !== "string") throw diagnostics$1.DF0014({ name: input.id });
return {
id: input.id,
kind: "tool",
title: input.title ?? input.id,
description: input.description,
safety: input.safety ?? "action",
tags: input.tags,
args: input.args,
inputSchema: input.inputSchema,
outputSchema: input.outputSchema,
examples: input.examples
};
}
/** Query every registered provider, projecting inputs to serializable tools. */
_collectProviderTools() {
const out = [];
for (const provider of this.providers) for (const input of provider()) out.push({
input,
tool: this._projectTool(input)
});
return out;
}
_collectRpcTools() {
const out = [];
for (const [name, def] of this.context.rpc.definitions) {
const agent = def.agent;
if (!agent) continue;
if (!agent.description || typeof agent.description !== "string") throw diagnostics$1.DF0014({ name });
const type = def.type ?? "query";
const safety = agent.safety ?? inferSafety(type);
out.push({
id: name,
kind: "rpc",
title: agent.title ?? name,
description: agent.description,
safety,
tags: agent.tags,
rpcName: name,
examples: agent.examples
});
}
return out;
}
_findRpcDefinition(id) {
const def = this.context.rpc.definitions.get(id);
if (def?.agent) return def;
}
};
function inferSafety(type) {
if (type === "static" || type === "query") return "read";
return "action";
}
//#endregion
//#region src/node/host-diagnostics.ts
var DevframeDiagnosticsHost = class {
context;
_registry = {};
logger = new Proxy({}, { get: (_, code) => this._registry[code] });
defineDiagnostics = (opts) => {
return defineDiagnostics({
...opts,
reporters: [devframeReporter, ...opts.reporters ?? []]
});
};
constructor(context, initialDefinitions = []) {
this.context = context;
for (const d of initialDefinitions) this.register(d);
}
register(diagnostics) {
Object.assign(this._registry, diagnostics);
}
};
//#endregion
//#region ../../node_modules/.pnpm/obug@2.1.4/node_modules/obug/dist/core.js
/**
* Coerce `value`.
*/
function coerce(value) {
if (value instanceof Error) return value.stack || value.message;
return value;
}
/**
* Selects a color for a debug namespace
* @return An ANSI color code for the given namespace
*/
function selectColor(colors, namespace) {
let hash = 0;
for (let i = 0; i < namespace.length; i++) {
hash = (hash << 5) - hash + namespace.charCodeAt(i);
hash |= 0;
}
return colors[Math.abs(hash) % colors.length];
}
/**
* Checks if the given string matches a namespace template, honoring
* asterisks as wildcards.
*/
function matchesTemplate(search, template) {
let searchIndex = 0;
let templateIndex = 0;
let starIndex = -1;
let matchIndex = 0;
while (searchIndex < search.length) if (templateIndex < template.length && (template[templateIndex] === search[searchIndex] || template[templateIndex] === "*")) if (template[templateIndex] === "*") {
starIndex = templateIndex;
matchIndex = searchIndex;
templateIndex++;
} else {
searchIndex++;
templateIndex++;
}
else if (starIndex !== -1) {
templateIndex = starIndex + 1;
matchIndex++;
searchIndex = matchIndex;
} else return false;
while (templateIndex < template.length && template[templateIndex] === "*") templateIndex++;
return templateIndex === template.length;
}
function humanize(value) {
if (value >= 1e3) return `${(value / 1e3).toFixed(1)}s`;
return `${value}ms`;
}
let globalNamespaces = "";
function createDebug$1(namespace, options) {
let prevTime;
let enableOverride;
let namespacesCache;
let enabledCache;
const debug = (...args) => {
if (!debug.enabled) return;
const curr = Date.now();
const diff = curr - (prevTime || curr);
prevTime = curr;
args[0] = coerce(args[0]);
if (typeof args[0] !== "string") args.unshift("%O");
let index = 0;
args[0] = args[0].replace(/%([a-z%])/gi, (match, format) => {
if (match === "%%") return "%";
index++;
const formatter = options.formatters[format];
if (typeof formatter === "function") {
const value = args[index];
match = formatter.call(debug, value);
args.splice(index, 1);
index--;
}
return match;
});
options.formatArgs.call(debug, diff, args);
debug.log(...args);
};
debug.extend = function(namespace, delimiter = ":") {
return createDebug$1(this.namespace + delimiter + namespace, {
useColors: this.useColors,
color: this.color,
formatArgs: this.formatArgs,
formatters: this.formatters,
inspectOpts: this.inspectOpts,
log: this.log,
humanize: this.humanize
});
};
Object.assign(debug, options);
debug.namespace = namespace;
Object.defineProperty(debug, "enabled", {
enumerable: true,
configurable: false,
get: () => {
if (enableOverride != null) return enableOverride;
if (namespacesCache !== globalNamespaces) {
namespacesCache = globalNamespaces;
enabledCache = enabled(namespace);
}
return enabledCache;
},
set: (v) => {
enableOverride = v;
}
});
return debug;
}
let names = [];
let skips = [];
function enable(namespaces) {
globalNamespaces = namespaces;
names = [];
skips = [];
const split = globalNamespaces.trim().replace(/\s+/g, ",").split(",").filter(Boolean);
for (const ns of split) if (ns[0] === "-") skips.push(ns.slice(1));
else names.push(ns);
}
/**
* Returns true if the given mode name is enabled, false otherwise.
*/
function enabled(name) {
for (const skip of skips) if (matchesTemplate(name, skip)) return false;
for (const ns of names) if (matchesTemplate(name, ns)) return true;
return false;
}
//#endregion
//#region ../../node_modules/.pnpm/obug@2.1.4/node_modules/obug/dist/node.js
let env = {};
try {
process.env.DEBUG;
env = process.env;
} catch (_unused) {}
const colors = process.stderr.getColorDepth && process.stderr.getColorDepth(env) > 2 ? [
20,
21,
26,
27,
32,
33,
38,
39,
40,
41,
42,
43,
44,
45,
56,
57,
62,
63,
68,
69,
74,
75,
76,
77,
78,
79,
80,
81,
92,
93,
98,
99,
112,
113,
128,
129,
134,
135,
148,
149,
160,
161,
162,
163,
164,
165,
166,
167,
168,
169,
170,
171,
172,
173,
178,
179,
184,
185,
196,
197,
198,
199,
200,
201,
202,
203,
204,
205,
206,
207,
208,
209,
214,
215,
220,
221
] : [
6,
2,
3,
4,
5,
1
];
const inspectOpts = Object.keys(env).filter((key) => /^debug_/i.test(key)).reduce((obj, key) => {
const prop = key.slice(6).toLowerCase().replace(/_([a-z])/g, (_, k) => k.toUpperCase());
let value = env[key];
const lowerCase = typeof value === "string" && value.toLowerCase();
if (value === "null") value = null;
else if (lowerCase === "yes" || lowerCase === "on" || lowerCase === "true" || lowerCase === "enabled") value = true;
else if (lowerCase === "no" || lowerCase === "off" || lowerCase === "false" || lowerCase === "disabled") value = false;
else value = Number(value);
obj[prop] = value;
return obj;
}, Object.create(null));
/**
* Is stdout a TTY? Colored output is enabled when `true`.
*/
function useColors() {
return "colors" in inspectOpts ? Boolean(inspectOpts.colors) : isatty(process.stderr.fd);
}
function getDate() {
if (inspectOpts.hideDate) return "";
return `${(/* @__PURE__ */ new Date()).toISOString()} `;
}
/**
* Adds ANSI color escape codes if enabled.
*/
function formatArgs(diff, args) {
const { namespace: name, useColors } = this;
if (useColors) {
const c = this.color;
const colorCode = `\u001B[3${c < 8 ? c : `8;5;${c}`}`;
const prefix = ` ${colorCode};1m${name} \u001B[0m`;
args[0] = prefix + args[0].split("\n").join(`\n${prefix}`);
args.push(`${colorCode}m+${this.humanize(diff)}\u001B[0m`);
} else args[0] = `${getDate()}${name} ${args[0]}`;
}
function log(...args) {
process.stderr.write(`${formatWithOptions(this.inspectOpts, ...args)}\n`);
}
const defaultOptions = {
useColors: useColors(),
formatArgs,
formatters: {
/**
* Map %o to `util.inspect()`, all on a single line.
*/
o(v) {
this.inspectOpts.colors = this.useColors;
return inspect(v, this.inspectOpts).split("\n").map((str) => str.trim()).join(" ");
},
/**
* Map %O to `util.inspect()`, allowing multiple lines if needed.
*/
O(v) {
this.inspectOpts.colors = this.useColors;
return inspect(v, this.inspectOpts);
}
},
inspectOpts,
log,
humanize
};
function createDebug(namespace, options) {
var _ref;
const color = (_ref = options && options.color) !== null && _ref !== void 0 ? _ref : selectColor(colors, namespace);
return createDebug$1(namespace, Object.assign(defaultOptions, { color }, options));
}
enable(env.DEBUG || "");
//#endregion
//#region src/node/rpc-shared-state.ts
const debug$1 = createDebug("devframe:rpc:state:changed");
const debugSubscribe = createDebug("devframe:rpc:state:subscribe");
function createRpcSharedStateServerHost(rpc) {
const sharedState = /* @__PURE__ */ new Map();
const stateDisposers = /* @__PURE__ */ new Map();
const keyAddedListeners = /* @__PURE__ */ new Set();
function registerSharedState(key, state) {
const offs = [];
offs.push(state.on("updated", (fullState, patches, syncId) => {
if (patches) {
debug$1("patch", {
key,
syncId
});
rpc.broadcast({
method: "devframe:rpc:client-state:patch",
args: [
key,
patches,
syncId
],
filter: (client) => client.$meta.subscribedStates.has(key)
});
} else {
debug$1("updated", {
key,
syncId
});
rpc.broadcast({
method: "devframe:rpc:client-state:updated",
args: [
key,
fullState,
syncId
],
filter: (client) => client.$meta.subscribedStates.has(key)
});
}
}));
return () => {
for (const off of offs) off();
};
}
const host = {
get: async (key, options) => {
if (sharedState.has(key)) return sharedState.get(key);
if (options?.initialValue === void 0 && options?.sharedState === void 0) throw diagnostics$1.DF0013({ key });
debug$1("new-state", key);
const state = options.sharedState ?? createSharedState({
initialValue: options.initialValue,
enablePatches: false
});
stateDisposers.set(key, registerSharedState(key, state));
sharedState.set(key, state);
for (const fn of keyAddedListeners) fn(key);
return state;
},
keys() {
return Array.from(sharedState.keys());
},
onKeyAdded(fn) {
keyAddedListeners.add(fn);
return () => {
keyAddedListeners.delete(fn);
};
},
delete(key) {
const dispose = stateDisposers.get(key);
if (!dispose) return false;
dispose();
stateDisposers.delete(key);
sharedState.delete(key);
return true;
}
};
rpc.register({
name: "devframe:rpc:server-state:subscribe",
type: "event",
handler(key) {
const session = rpc.getCurrentRpcSession();
if (!session) return;
debugSubscribe("subscribe", {
key,
session: session.meta.id
});
session.meta.subscribedStates.add(key);
}
});
rpc.register({
name: "devframe:rpc:server-state:get",
type: "query",
handler: async (key) => {
if (!sharedState.has(key)) return void 0;
return (await host.get(key)).value();
},
dump: () => ({ inputs: host.keys().map((key) => [key]) })
});
rpc.register({
name: "devframe:rpc:server-state:set",
type: "query",
handler: async (key, value, syncId) => {
(await host.get(key, { initialValue: value })).mutate(() => value, syncId);
}
});
rpc.register({
name: "devframe:rpc:server-state:patch",
type: "query",
handler: async (key, patches, syncId) => {
if (!sharedState.has(key)) return;
(await host.get(key)).patch(patches, syncId);
}
});
return host;
}
//#endregion
//#region src/utils/streaming-channel.ts
const DEFAULT_HIGH_WATER_MARK = 256;
var StreamClosedError = class extends Error {
name = "StreamClosedError";
};
/**
* Build a server-side stream sink. RPC-agnostic — the RPC host wires
* `events.on('chunk' | 'end')` to broadcast, and reads `buffer` to replay
* for late or reconnecting subscribers.
*/
function createStreamSink(options = {}) {
const id = options.id ?? nanoid();
const replayWindow = Math.max(0, options.replayWindow ?? 0);
const events = createEventEmitter();
const controller = new AbortController();
const buffer = [];
let closed = false;
let lastSeq = 0;
function write(chunk) {
if (closed) throw new StreamClosedError(`Cannot write to a closed stream "${id}"`);
lastSeq += 1;
if (replayWindow > 0) {
buffer.push({
seq: lastSeq,
chunk
});
if (buffer.length > replayWindow) buffer.splice(0, buffer.length - replayWindow);
}
events.emit("chunk", lastSeq, chunk);
}
function error(reason) {
if (closed) return;
closed = true;
const payload = toErrorPayload(reason);
controller.abort(reason);
events.emit("end", payload);
}
function close() {
if (closed) return;
closed = true;
if (!controller.signal.aborted) controller.abort("stream closed");
events.emit("end", void 0);
}
function abort(reason) {
if (closed) return;
if (!controller.signal.aborted) controller.abort(reason ?? "aborted");
}
const writable = new WritableStream({
write(chunk) {
write(chunk);
},
close() {
close();
},
abort(reason) {
error(reason);
}
});
return {
id,
signal: controller.signal,
get closed() {
return closed;
},
get lastSeq() {
return lastSeq;
},
write,
error,
close,
abort,
writable,
events,
buffer
};
}
/**
* Build a client-side stream reader. RPC-agnostic — the RPC host calls
* `_push(seq, chunk)` on each incoming chunk and `_end(error?)` on the
* terminal frame. Consumers iterate with `for await` or pipe `readable`.
*/
function createStreamReader(options = {}) {
const id = options.id ?? nanoid();
const highWaterMark = Math.max(1, options.highWaterMark ?? DEFAULT_HIGH_WATER_MARK);
const queue = [];
let lastSeenSeq = 0;
let done = false;
let cancelled = false;
let endError;
let pending;
let pullController;
let readableInstance;
function drainNext() {
if (!pending) return;
if (queue.length > 0) {
const value = queue.shift();
const r = pending;
pending = void 0;
r.resolve({
value,
done: false
});
return;
}
if (done) {
const r = pending;
pending = void 0;
if (endError) {
const err = new Error(endError.message);
err.name = endError.name;
r.reject(err);
} else r.resolve({
value: void 0,
done: true
});
}
}
function feedReadable() {
if (!pullController) return;
while (queue.length > 0) {
const v = queue.shift();
try {
pullController.enqueue(v);
} catch {
break;
}
}
if (done && pullController) {
try {
if (endError) {
const err = new Error(endError.message);
err.name = endError.name;
pullController.error(err);
} else pullController.close();
} catch {}
pullController = void 0;
}
}
function push(seq, chunk) {
if (done || cancelled) return;
if (seq <= lastSeenSeq) return;
lastSeenSeq = seq;
queue.push(chunk);
if (queue.length > highWaterMark) {
const overflow = queue.length - highWaterMark;
queue.splice(0, overflow);
options.onOverflow?.(overflow);
}
drainNext();
if (readableInstance) feedReadable();
}
function end(error) {
if (done) return;
done = true;
endError = error;
drainNext();
if (readableInstance) feedReadable();
}
function cancel() {
if (cancelled || done) return;
cancelled = true;
options.onCancel?.();
end(void 0);
}
function getReadable() {
if (readableInstance) return readableInstance;
readableInstance = new ReadableStream({
start(controller) {
pullController = controller;
feedReadable();
},
cancel() {
cancel();
}
});
return readableInstance;
}
return {
id,
get cancelled() {
return cancelled;
},
get done() {
return done;
},
get lastSeenSeq() {
return lastSeenSeq;
},
get readable() {
return getReadable();
},
cancel,
_push: push,
_end: end,
[Symbol.asyncIterator]() {
return {
next() {
if (queue.length > 0) return Promise.resolve({
value: queue.shift(),
done: false
});
if (done) {
if (endError) {
const err = new Error(endError.message);
err.name = endError.name;
return Promise.reject(err);
}
return Promise.resolve({
value: void 0,
done: true
});
}
return new Promise((resolve, reject) => {
pending = {
resolve,
reject
};
});
},
return() {
cancel();
return Promise.resolve({
value: void 0,
done: true
});
}
};
}
};
}
function toErrorPayload(reason) {
if (reason instanceof Error) return {
name: reason.name || "Error",
message: reason.message
};
if (typeof reason === "string") return {
name: "Error",
message: reason
};
try {
return {
name: "Error",
message: JSON.stringify(reason)
};
} catch {
return {
name: "Error",
message: String(reason)
};
}
}
//#endregion
//#region src/node/rpc-streaming.ts
const debug = createDebug("devframe:rpc:streaming");
const STREAM_KEY_SEPARATOR = "";
function streamKey(channel, id) {
return `${channel}${STREAM_KEY_SEPARATOR}${id}`;
}
/**
* Build the server-side streaming host. Mirrors the layout of
* `createRpcSharedStateServerHost` — registers a fixed set of internal
* RPC methods (`subscribe` / `unsubscribe` / `cancel`) once, then per-channel
* state lives in a `Map<channelName, ChannelState>`.
*/
function createRpcStreamingServerHost(rpc) {
const channels = /* @__PURE__ */ new Map();
function findStream(channelName, id) {
return channels.get(channelName)?.streams.get(id);
}
function freeStreamNow(state, id) {
const record = state.streams.get(id);
if (!record) return;
if (record.retentionTimer) {
clearTimeout(record.retentionTimer);
record.retentionTimer = void 0;
}
for (const off of record.unbinders) off();
state.streams.delete(id);
debug("freed", state.name, id);
}
function maybeFreeStream(state, id) {
const record = state.streams.get(id);
if (!record) return;
if (!record.sink.closed || record.subscribers.size > 0) return;
const retention = state.options.closedStreamRetention;
if (retention <= 0) {
freeStreamNow(state, id);
return;
}
if (record.retentionTimer) return;
record.retentionTimer = setTimeout(freeStreamNow, retention, state, id);
}
function cancelRetention(record) {
if (record.retentionTimer) {
clearTimeout(record.retentionTimer);
record.retentionTimer = void 0;
}
}
rpc.register({
name: "devframe:streaming:subscribe",
type: "event",
handler(channelName, id, opts) {
const state = channels.get(channelName);
if (!state) {
diagnostics$1.DF0030({
channel: channelName,
id
}, { method: "error" });
return;
}
const record = state.streams.get(id);
if (!record) {
diagnostics$1.DF0030({
channel: channelName,
id
}, { method: "error" });
return;
}
const session = rpc.getCurrentRpcSession();
if (!session) return;
const key = streamKey(channelName, id);
session.meta.subscribedStreams ??= /* @__PURE__ */ new Set();
session.meta.subscribedStreams.add(key);
record.subscribers.add(session.meta);
cancelRetention(record);
const afterSeq = opts?.afterSeq ?? 0;
for (const buffered of record.sink.buffer) if (buffered.seq > afterSeq) rpc.broadcast({
method: "devframe:streaming:chunk",
args: [
channelName,
id,
buffered.seq,
buffered.chunk
],
event: true,
optional: true,
filter: (client) => client.$meta === session.meta
});
if (record.sink.closed) rpc.broadcast({
method: "devframe:streaming:end",
args: [
channelName,
id,
void 0
],
event: true,
optional: true,
filter: (client) => client.$meta === session.meta
});
}
});
rpc.register({
name: "devframe:streaming:unsubscribe",
type: "event",
handler(channelName, id) {
const state = channels.get(channelName);
const record = state?.streams.get(id);
const session = rpc.getCurrentRpcSession();
if (!session) return;
session.meta.subscribedStreams?.delete(streamKey(channelName, id));
if (state && record) {
record.subscribers.delete(session.meta);
maybeFreeStream(state, id);
}
}
});
rpc.register({
name: "devframe:streaming:cancel",
type: "event",
handler(channelName, id) {
const record = findStream(channelName, id);
if (!record) return;
const session = rpc.getCurrentRpcSession();
if (!session) return;
record.subscribers.delete(session.meta);
session.meta.subscribedStreams?.delete(streamKey(channelName, id));
if (record.subscribers.size === 0) record.sink.abort("cancelled by client");
}
});
rpc.register({
name: "devframe:streaming:upload-chunk",
type: "event",
handler(channelName, id, seq, chunk) {
const record = channels.get(channelName)?.inbound.get(id);
if (!record) {
diagnostics$1.DF0030({
channel: channelName,
id
}, { method: "error" });
return;
}
if (!record.uploaderMeta) {
const session = rpc.getCurrentRpcSession();
if (session) {
record.uploaderMeta = session.meta;
session.meta.uploadingStreams ??= /* @__PURE__ */ new Set();
session.meta.uploadingStreams.add(streamKey(channelName, id));
}
}
record.reader._push(seq, chunk);
}
});
rpc.register({
name: "devframe:streaming:upload-end",
type: "event",
handler(channelName, id, error) {
const state = channels.get(channelName);
const record = state?.inbound.get(id);
if (!record) return;
record.reader._end(error);
if (record.uploaderMeta) record.uploaderMeta.uploadingStreams?.delete(streamKey(channelName, id));
state?.inbound.delete(id);
}
});
function createChannel(name, opts = {}) {
if (channels.has(name)) throw diagnostics$1.DF0032({ channel: name });
const replayWindow = opts.replayWindow ?? 0;
const state = {
name,
options: {
replayWindow,
closedStreamRetention: opts.closedStreamRetention ?? (replayWindow > 0 ? 3e4 : 0)
},
streams: /* @__PURE__ */ new Map(),
inbound: /* @__PURE__ */ new Map()
};
channels.set(name, state);
function start(startOpts = {}) {
const sink = createStreamSink({
id: startOpts.id,
replayWindow: state.options.replayWindow
});
const record = {
sink,
subscribers: /* @__PURE__ */ new Set(),
unbinders: []
};
state.streams.set(sink.id, record);
record.unbinders.push(sink.events.on("chunk", (seq, chunk) => {
rpc.broadcast({
method: "devframe:streaming:chunk",
args: [
name,
sink.id,
seq,
chunk
],
event: true,
optional: true,
filter: (client) => record.subscribers.has(client.$meta)
});
}));
record.unbinders.push(sink.events.on("end", (error) => {
rpc.broadcast({
method: "devframe:streaming:end",
args: [
name,
sink.id,
error
],
event: true,
optional: true,
filter: (client) => record.subscribers.has(client.$meta)
});
maybeFreeStream(state, sink.id);
}));
return sink;
}
async function pipeFrom(readable, startOpts = {}) {
const sink = start(startOpts);
readable.pipeTo(sink.writable, { signal: sink.signal }).catch(() => {});
return sink;
}
function get(id) {
return state.streams.get(id)?.sink;
}
function ids() {
return Array.from(state.streams.keys());
}
function openInbound(inboundOpts = {}) {
let inboundRecord;
const reader = createStreamReader({
id: inboundOpts.id,
onCancel() {
const targetMeta = inboundRecord?.uploaderMeta;
if (!targetMeta) return;
rpc.broadcast({
method: "devframe:streaming:upload-cancel",
args: [name, reader.id],
event: true,
optional: true,
filter: (client) => client.$meta === targetMeta
});
}
});
inboundRecord = { reader };
state.inbound.set(reader.id, inboundRecord);
debug("opened-inbound", name, reader.id);
return reader;
}
return {
name,
start,
pipeFrom,
get,
ids,
openInbound
};
}
function parseKey(key) {
const sepIdx = key.indexOf(STREAM_KEY_SEPARATOR);
if (sepIdx < 0) return void 0;
return {
channelName: key.slice(0, sepIdx),
id: key.slice(sepIdx + 1)
};
}
return {
create: createChannel,
_onSessionDisconnected(meta) {
if (meta.subscribedStreams) {
for (const key of meta.subscribedStreams) {
const parsed = parseKey(key);
if (!parsed) continue;
const state = channels.get(parsed.channelName);
const record = state?.streams.get(parsed.id);
if (!state || !record) continue;
record.subscribers.delete(meta);
if (record.subscribers.size === 0 && !record.sink.closed) record.sink.abort("all subscribers disconnected");
maybeFreeStream(state, parsed.id);
}
meta.subscribedStreams.clear();
}
if (meta.uploadingStreams) {
for (const key of meta.uploadingStreams) {
const parsed = parseKey(key);
if (!parsed) continue;
const state = channels.get(parsed.channelName);
const record = state?.inbound.get(parsed.id);
if (!state || !record) continue;
record.reader._end({
name: "UploadDisconnected",
message: "Uploader disconnected before completing the stream"
});
state.inbound.delete(parsed.id);
}
meta.uploadingStreams.clear();
}
}
};
}
//#endregion
//#region src/node/host-functions.ts
const debugBroadcast = createDebug("devframe:rpc:broadcast");
/**
* Concrete implementation backing `ctx.rpc`. Internal: consumers should
* depend on the structural {@link RpcFunctionsHost} type, never this class.
* Its `@internal` members (`_rpcGroup`, `_asyncStorage`,
* `_emitSessionDisconnected`) are wired by `startHttpAndWs` and must not
* widen the public surface.
*
* @internal
*/
var RpcFunctionsHostImpl = class extends RpcFunctionsCollectorBase {
/**
* @internal
*/
_rpcGroup = void 0;
_asyncStorage = void 0;
constructor(context) {
super(context);
this.sharedState = createRpcSharedStateServerHost(this);
this.streaming = createRpcStreamingServerHost(this);
}
sharedState;
streaming;
/**
* Adapters call this from their WS `onDisconnected` hook so downstream
* hosts (streaming, …) can free per-session state. Public-ish because
* tests / custom adapters may want to mirror it.
*
* @internal
*/
_emitSessionDisconnected(meta) {
this.streaming._onSessionDisconnected(meta);
}
async invokeLocal(method, ...args) {
if (!this.definitions.has(method)) throw diagnostics$1.DF0006({ name: String(method) });
const handler = await this.getHandler(method);
return await Promise.resolve(handler(...args));
}
async broadcast(options) {
if (!this._rpcGroup) return;
debugBroadcast(JSON.stringify(options.method));
await Promise.allSettled(this._rpcGroup.clients.map((client) => {
if (options.filter?.(client) === false) return void 0;
return client.$callRaw({
optional: true,
event: true,
...options
});
}));
}
getCurrentRpcSession() {
if (!this._asyncStorage) throw diagnostics$1.DF0007();
return this._asyncStorage.getStore();
}
};
//#endregion
//#region src/node/host-services.ts
/**
* Cross-plugin service registry (see `types/services.ts` for the contract).
* Values are held per context instance; `whenAvailable` subscriptions make
* the mechanism robust against setup ordering between provider and consumer.
*/
var DevframeServicesHostImpl = class {
services = /* @__PURE__ */ new Map();
listeners = /* @__PURE__ */ new Map();
provide(id, service) {
const key = id;
if (this.services.has(key)) throw diagnostics$1.DF0037({ id: key });
this.services.set(key, service);
for (const listener of this.listeners.get(key) ?? []) listener(service);
return () => {
if (this.services.get(key) === service) this.services.delete(key);
};
}
get(id) {
return this.services.get(id);
}
has(id) {
return this.services.has(id);
}
whenAvailable(id, callback) {
const key = id;
if (this.services.has(key)) callback(this.services.get(key));
let set = this.listeners.get(key);
if (!set) {
set = /* @__PURE__ */ new Set();
this.listeners.set(key, set);
}
const listener = callback;
set.add(listener);
return () => {
set.delete(listener);
};
}
keys() {
return Array.from(this.services.keys());
}
};
//#endregion
//#region src/node/host-views.ts
var DevframeViewHost = class {
context;
/**
* @internal
*/
buildStaticDirs = [];
constructor(context) {
this.context = context;
}
hostStatic(baseUrl, distDir) {
if (!existsSync(distDir)) throw diagnostics$1.DF0008({ distDir });
this.buildStaticDirs.push({
baseUrl,
distDir
});
this.context.host.mountStatic(baseUrl, distDir);
}
};
//#endregion
//#region src/node/rpc/agent-invoke-tool.ts
const agentInvokeTool = defineRpcFunction({
name: "devframe:agent:invoke-tool",
type: "action",
setup: (ctx) => {
return { async handler(id, args) {
return await ctx.agent.invoke(id, args);
} };
}
});
//#endregion
//#region src/node/rpc/agent-list-resources.ts
const agentListResources = defineRpcFunction({
name: "devframe:agent:list-resources",
type: "query",
jsonSerializable: true,
setup: (ctx) => {
return { async handler() {
return ctx.agent.list().resources;
} };
}
});
//#endregion
//#region src/node/rpc/index.ts
/**
* Built-in agent introspection RPC functions. Registered automatically
* by `createHostContext`. Not themselves agent-exposed (no `agent`
* field) — they power the MCP adapter and any future agent CLI.
*
* @experimental
*/
const BUILTIN_AGENT_RPC = [
defineRpcFunction({
name: "devframe:agent:list-tools",
type: "query",
jsonSerializable: true,
setup: (ctx) => {
return { async handler() {
return ctx.agent.list().tools;
} };
}
}),
agentInvokeTool,
agentListResources,
defineRpcFunction({
name: "devframe:agent:read-resource",
type: "query",
jsonSerializable: true,
setup: (ctx) => {
return { async handler(id) {
return await ctx.agent.read(id);
} };
}
})
];
//#endregion
//#region src/utils/scope.ts
/** Whether a name is already namespaced (contains a `:` separator). */
function isQualifiedName(name) {
return name.includes(":");
}
/**
* Prefix a bare name with `<namespace>:`. Names that already contain a
* `:` are returned unchanged, so callers can reference another scope's
* ids explicitly (e.g. `ctx.rpc.call('other-plugin:fn')`).
*/
function qualifyName(namespace, name) {
return isQualifiedName(name) ? name : `${namespace}:${name}`;
}
//#endregion
//#region src/node/settings.ts
const STORAGE_SCOPE = {
global: "global",
project: "project"
};
function createNodeSettingsStore(context, namespace, scope) {
const stateKey = `devframe:settings:${scope}:${namespace}`;
let statePromise;
function store() {
if (!statePromise) {
const filepath = join(context.host.getStorageDir(STORAGE_SCOPE[scope]), "settings", `${namespace}.json`);
statePromise = context.rpc.sharedState.get(stateKey, { sharedState: createStorage({
filepath,
initialValue: {}
}) });
}
return statePromise;
}
return {
async get(key) {
return (await store()).value()[key];
},
async set(key, value) {
(await store()).mutate((draft) => {
draft[key] = value;
});
},
async delete(key) {
(await store()).mutate((draft) => {
delete draft[key];
});
},
async all() {
return (await store()).value();
},
async onChange(fn) {
return (await store()).on("updated", (full) => fn(full));
}
};
}
/**
* Build the node-side `settings` surface for a scope namespace. `project`
* persists under the host's `workspace` storage dir, `global` under its
* `global` dir. Each is a file-backed, client-synced key-value store.
*/
function createNodeSettings(context, namespace) {
return {
global: createNodeSettingsStore(context, namespace, "global"),
project: createNodeSettingsStore(context, namespace, "project")
};
}
//#endregion
//#region src/node/scope.ts
function prefixDefinition(namespace, fn) {
if (isQualifiedName(fn.name)) throw diagnostics$1.DF0034({
namespace,
name: fn.name
});
return {
...fn,
name: `${namespace}:${fn.name}`
};
}
/**
* Build a namespace-scoped view of a {@link DevframeNodeContext}. Every
* RPC id, shared-state key, and streaming channel passed through the
* returned `rpc` surface is auto-namespaced with `<namespace>:`.
*/
function createScopedNodeContext(context, namespace) {
const base = context.rpc;
const rpc = {
namespace,
register(fn, force) {
base.register(prefixDefinition(namespace, fn), force);
},
update(fn, force) {
base.update(prefixDefinition(namespace, fn), force);
},
call: ((method, ...args) => base.invokeLocal(qualifyName(namespace, method), ...args)),
broadcast: ((options) => base.broadcast({
...options,
method: qualifyName(namespace, options.method)
})),
sharedState: ((key, options) => base.sharedState.get(qualifyName(namespace, key), options)),
streaming: { create: (name, opts) => base.streaming.create(qualifyName(namespace, name), opts) },
getCurrentRpcSession: () => base.getCurrentRpcSession()
};
return {
namespace,
base: context,
cwd: context.cwd,
workspaceRoot: context.workspaceRoot,
mode: context.mode,
host: context.host,
rpc,
settings: createNodeSettings(context, namespace),
views: context.views,
diagnostics: context.diagnostics,
agent: context.agent,
scope: context.scope
};
}
//#endregion
//#region src/node/context.ts
/**
* Framework- and build-tool-agnostic core of the Devframe node context.
* Wires the RPC host, view (HTTP file-serving) host, diagnostics, and
* agent subsystems. Host adapters can wrap this to augment `ctx` with
* extra surfaces — for example, `@vitejs/devtools-kit`'s
* `createKitContext` attaches `docks`, `terminals`, `messages`, and
* `commands` when mounted into Vite DevTools.
*/
async function createHostContext(options) {
const { cwd, workspaceRoot = cwd, mode, host, builtinRpcDeclarations = [] } = options;
const context = {
cwd,
workspaceRoot,
mode,
host,
rpc: void 0,
views: void 0,
diagnostics: void 0,
agent: void 0,
services: void 0,
scope: void 0
};
const rpcHost = new RpcFunctionsHostImpl(context);
const viewsHost = new DevframeViewHost(context);
const diagnosticsHost = new DevframeDiagnosticsHost(context, [diagnostics$1, diagnostics]);
context.rpc = rpcHost;
context.views = viewsHost;
context.diagnostics = diagnosticsHost;
context.services = new DevframeServicesHostImpl();
context.agent = new DevframeAgentHost(context);
const scopedCache = /* @__PURE__ */ new Map();
context.scope = ((namespace) => {
if (!namespace) return context;
let scoped = scopedCache.get(namespace);
if (!scoped) {
scoped = createScopedNodeContext(context, namespace);
scopedCache.set(namespace, scoped);
}
return scoped;
});
for (const fn of BUILTIN_AGENT_RPC) rpcHost.register(fn);
for (const fn of builtinRpcDeclarations) rpcHost.register(fn);
return context;
}
//#endregion
//#region src/node/host-h3.ts
/**
* h3-backed {@link DevframeHost} — used by the standalone CLI adapter.
*/
function createH3DevframeHost(options) {
const workspaceRoot = options.workspaceRoot ?? process$1.cwd();
return {
mountStatic(base, distDir) {
return options.mount?.(base, distDir);
},
resolveOrigin() {
return options.origin;
},
getStorageDir(scope) {
const namespace = `.${options.appName}/devframe`;
if (scope === "workspace") return join(workspaceRoot, ".devframe");
if (scope === "project") return join(workspaceRoot, "node_modules", namespace);
return join(homedir(), namespace);
}
};
}
//#endregion
export { DevframeViewHost as a, createRpcSharedStateServerHost as c, coerceAgentPositionalArgs as d, createNodeSettings as i, DevframeDiagnosticsHost as l, createHostContext as n, DevframeServicesHostImpl as o, createScopedNodeContext as r, createRpcStreamingServerHost as s, createH3DevframeHost as t, DevframeAgentHost as u };
import { t as createMcpFetchHandler } from "./fetch-CK0S253E.mjs";
import { defineHandler } from "h3";
//#region src/adapters/mcp/http.ts
/**
* Mount an MCP Streamable-HTTP endpoint on an h3 app at `path` — the h3
* binding over {@link createMcpFetchHandler}, which owns the sessions, the
* origin gate, and the transport plumbing.
*
* The handler is web-standard — it takes the h3 event's web `Request` and
* returns a web `Response` (an SSE `ReadableStream` body for the
* server→client stream). We copy that response onto `event.res` and return
* its body rather than returning the `Response` object directly, so a
* legitimate MCP 404 (unknown session) isn't swallowed by h3's
* "Response-with-404 falls through to the next handler" rule (which would
* otherwise hand the request to the SPA static catch-all).
*
* @experimental
*/
function mountMcpHttp(app, ctx, path, options) {
const handler = createMcpFetchHandler(ctx, options);
app.use(path, defineHandler(async (event) => respond(event, await handler.fetch(event.req))));
return { dispose: handler.dispose };
}
/**
* Copy a web `Response` from the MCP transport onto the h3 event's response
* and return its body. Returning the body (a `ReadableStream` or `null`)
* rather than the `Response` object avoids h3's 404-fall-through behavior.
*/
function respond(event, response) {
event.res.status = response.status;
event.res.statusText = response.statusText;
response.headers.forEach((value, key) => {
event.res.headers.set(key, value);
});
return response.body ?? "";
}
//#endregion
export { mountMcpHttp };
import { W as DevframeNodeRpcSession, b as DevframeNodeContext, ht as SharedState } from "./devframe-qgCKL683.mjs";
import { n as InternalAnonymousAuthStorage } from "./context-DFzmxCLa.mjs";
//#region src/node/auth/revoke.d.ts
/**
* Flip `isTrusted` to false on any live WS clients connected with `token`
* and broadcast the `auth:revoked` event so they can react.
*
* Shared between persisted-auth revocation and remote-dock token revocation.
*/
declare function revokeActiveConnectionsForToken(context: DevframeNodeContext, token: string): Promise<void>;
/**
* Revoke an auth token: remove from storage and notify all connected clients
* using this token that they are no longer trusted.
*/
declare function revokeAuthToken(context: DevframeNodeContext, storage: SharedState<InternalAnonymousAuthStorage>, token: string): Promise<void>;
//#endregion
//#region src/node/auth/state.d.ts
/**
* The current one-time authentication code. Display this to the user (e.g. in
* the dev-server terminal) so they can type it into the browser to authenticate.
*/
declare function getTempAuthCode(): string;
/**
* Rotate the authentication code, resetting its expiry window and failed-attempt
* counter. Call this when a new authentication flow begins (e.g. when an
* untrusted client starts authenticating) so the displayed code is freshly
* valid for its full TTL.
*/
declare function refreshTempAuthCode(): string;
/**
* Build a "magic link" authentication URL that embeds a one-time code (OTP) in
* the URL **fragment**. Opening it authenticates the client without typing —
* print it on startup (devframe stays headless, so the host prints its own
* banner). Defaults to the current code; the link is subject to the same TTL.
*
* The code rides the fragment (`#devframe_otp=…`), not the query string, so it
* is never sent to the server, written to an access log, or leaked in a
* `Referer` header — the browser client reads it locally (see
* `consumeOtpFromUrl`). Any existing fragment parameters are preserved.
*/
declare function buildOtpAuthUrl(baseUrl: string, code?: string): string;
/**
* Re-authenticate a connection that presents a previously-issued bearer token.
* Returns `true` and marks the session trusted when the token is known.
*
* Used by the `anonymous:devframe:auth` handler so a client that already
* authenticated (token persisted in the browser) is trusted on reconnect
* without entering the code again.
*/
declare function verifyAuthToken(token: string, session: DevframeNodeRpcSession, storage: SharedState<InternalAnonymousAuthStorage>): boolean;
/**
* Exchange a one-time authentication code for a fresh, node-issued bearer token.
*
* On success this mints a high-entropy token, records it in the trusted store,
* marks the calling session trusted, rotates the code, and returns the token
* for the client to persist. Returns `null` on any failure.
*
* Because the code is short and human-typed, verification is hardened against
* brute force: it enforces a time-to-live, compares in constant time, and
* rotates the code after {@link TEMP_AUTH_MAX_ATTEMPTS} failed attempts so an
* attacker cannot keep guessing against the same code.
*/
declare function exchangeTempAuthCode(code: string, session: DevframeNodeRpcSession, info: {
ua: string;
origin: string;
}, storage: SharedState<InternalAnonymousAuthStorage>): string | null;
//#endregion
export { verifyAuthToken as a, refreshTempAuthCode as i, exchangeTempAuthCode as n, revokeActiveConnectionsForToken as o, getTempAuthCode as r, revokeAuthToken as s, buildOtpAuthUrl as t };
import { t as diagnostics } from "./diagnostics-B5-qHeqD.mjs";
import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { join } from "pathe";
import process from "node:process";
import { homedir } from "node:os";
//#region src/node/instance-registry.ts
/** Environment variable overriding the registry directory (tests, CI). */
const DEVFRAME_INSTANCES_DIR_ENV = "DEVFRAME_INSTANCES_DIR";
/** Environment variable disabling instance registration entirely. */
const DEVFRAME_DISABLE_INSTANCE_REGISTRY_ENV = "DEVFRAME_DISABLE_INSTANCE_REGISTRY";
/**
* Resolve the registry directory: `~/.devframe/instances/` by default —
* the framework's own global dir, deliberately outside the per-app
* `~/.<appName>/devframe/` storage convention since the registry spans apps —
* overridable via `DEVFRAME_INSTANCES_DIR`.
*/
function resolveInstancesDir(override) {
return override ?? process.env[DEVFRAME_INSTANCES_DIR_ENV] ?? join(homedir(), ".devframe", "instances");
}
function isRegistryDisabled() {
const value = process.env[DEVFRAME_DISABLE_INSTANCE_REGISTRY_ENV];
return value === "1" || value === "true";
}
/**
* Record a running devframe instance in the global instance registry so
* discovery tooling (`devframe connect`, editor integrations) can find it
* without port guessing.
*
* `createDevServer` registers automatically; custom hosts that serve a
* devframe in-process (e.g. `@devframes/next`'s host inside a Next dev
* server) call this explicitly with the origin they are reachable at.
*
* The record is written atomically to `<dir>/<pid>-<port>.json` and removed
* by {@link DevframeInstanceRegistration.unregister}. Records surviving a
* crash are pruned by readers whose liveness probe fails. Registration never
* throws — a write failure degrades to a coded warning (`DF0045`), since a
* dev server must not die over discovery metadata.
*
* @experimental
*/
function registerDevframeInstance(record, options = {}) {
const dir = resolveInstancesDir(options.instancesDir);
const file = join(dir, `${record.pid}-${record.port}.json`);
if (!isRegistryDisabled()) try {
mkdirSync(dir, { recursive: true });
const tmp = join(dir, `.${record.pid}-${record.port}.${Date.now()}.tmp`);
writeFileSync(tmp, `${JSON.stringify(record, null, 2)}\n`);
renameSync(tmp, file);
} catch (error) {
diagnostics.DF0045({
file,
reason: error instanceof Error ? error.message : String(error),
cause: error
});
}
return {
file,
unregister: () => {
try {
rmSync(file, { force: true });
} catch (error) {
diagnostics.DF0045({
file,
reason: error instanceof Error ? error.message : String(error),
cause: error
});
}
}
};
}
/**
* Read every record in the registry directory, dropping unparseable files.
* Liveness is the caller's concern — see {@link probeDevframeInstance}.
*
* @experimental
*/
function readDevframeInstances(options = {}) {
const dir = resolveInstancesDir(options.instancesDir);
let files;
try {
files = readdirSync(dir).filter((f) => f.endsWith(".json"));
} catch {
return [];
}
const records = [];
for (const file of files) try {
const parsed = JSON.parse(readFileSync(join(dir, file), "utf8"));
if (typeof parsed?.origin === "string" && typeof parsed?.pid === "number") records.push(parsed);
} catch {}
return records;
}
/**
* Dialable-origin candidates for a recorded origin. A `localhost` bind is
* ambiguous — the server may listen on `127.0.0.1`, `::1`, or both, and
* HTTP clients differ in which family they try — so probe the explicit
* addresses too and adopt whichever answers.
*/
function originCandidates(origin) {
try {
const url = new URL(origin);
if (url.hostname !== "localhost") return [origin];
const port = url.port ? `:${url.port}` : "";
return [
origin,
`${url.protocol}//127.0.0.1${port}`,
`${url.protocol}//[::1]${port}`
];
} catch {
return [origin];
}
}
/**
* Probe `<origin><basePath>__connection.json`, trying each dialable
* candidate for the origin (see {@link originCandidates}). The single
* probe primitive behind both registry liveness checks and the
* connector's explicit `--port` probes.
*
* @internal
*/
async function probeDevframeOrigin(origin, basePath, timeoutMs) {
const base = basePath.endsWith("/") ? basePath : `${basePath}/`;
for (const candidate of originCandidates(origin)) try {
const response = await fetch(`${candidate}${base}__connection.json`, { signal: AbortSignal.timeout(timeoutMs ?? 1e3) });
if (!response.ok) continue;
return {
origin: candidate,
meta: await response.json().catch(() => ({}))
};
} catch {}
return null;
}
/**
* Probe a record's `__connection.json` to check the instance is alive.
* Returns the **dialable origin** that answered (for `localhost` records
* this may be an explicit `127.0.0.1` / `[::1]` origin), or `null` when
* unreachable.
*/
async function probeDevframeInstance(record, options = {}) {
return (await probeDevframeOrigin(record.origin, record.basePath, options.timeoutMs))?.origin ?? null;
}
/**
* Read the registry and split records into live and dead by probing each
* one's `__connection.json`, deleting dead records (prune-on-read). Live
* records carry the dialable origin the probe confirmed (a `localhost`
* record may come back as `127.0.0.1` / `[::1]`).
*
* A liveness probe only proves *something* answers on the record's port, so
* records left behind by killed processes shadow the server currently bound
* there: per `(port, basePath)` only the newest record survives, older
* ghosts are pruned with the dead.
*
* @experimental
*/
async function listLiveDevframeInstances(options = {}) {
const dir = resolveInstancesDir(options.instancesDir);
const records = readDevframeInstances({ instancesDir: dir });
const pruned = [];
const prune = (record) => {
pruned.push(record);
try {
rmSync(join(dir, `${record.pid}-${record.port}.json`), { force: true });
} catch {}
};
const newest = /* @__PURE__ */ new Map();
for (const record of records) {
const key = `${record.port}|${record.basePath}`;
const existing = newest.get(key);
if (!existing) newest.set(key, record);
else if (record.startedAt > existing.startedAt) {
prune(existing);
newest.set(key, record);
} else prune(record);
}
const live = [];
await Promise.all([...newest.values()].map(async (record) => {
const origin = await probeDevframeInstance(record, options);
if (origin) live.push(origin === record.origin ? record : {
...record,
origin
});
else prune(record);
}));
live.sort((a, b) => a.startedAt - b.startedAt);
return {
live,
pruned
};
}
//#endregion
export { probeDevframeOrigin as n, registerDevframeInstance as r, listLiveDevframeInstances as t };
import { createRpcServer } from "./rpc/server.mjs";
import { attachWsRpcTransport } from "./rpc/transports/ws-server.mjs";
import { t as diagnostics } from "./diagnostics-B5-qHeqD.mjs";
import { t as getInternalContext } from "./context-CQefP2jR.mjs";
import { createServer } from "node:http";
import { AsyncLocalStorage } from "node:async_hooks";
import { H3, toNodeHandler } from "h3";
import { isIP } from "node:net";
//#region src/node/utils.ts
function isObject(value) {
return Object.prototype.toString.call(value) === "[object Object]";
}
const NON_DIALABLE_HOSTS = /* @__PURE__ */ new Set([
"0.0.0.0",
"127.0.0.1",
"::",
"0000:0000:0000:0000:0000:0000:0000:0000",
""
]);
/** Map a bind host to a host a client can actually connect to. */
function toDialableHost(host) {
return NON_DIALABLE_HOSTS.has(host) ? "localhost" : host;
}
/** Format a bind host for use in a URL authority (dialable, IPv6-bracketed). */
function formatHostForUrl(host) {
const dialable = toDialableHost(host);
return isIP(dialable) === 6 ? `[${dialable}]` : dialable;
}
function normalizeHttpServerUrl(host, port) {
return `http://${formatHostForUrl(host)}:${port}`;
}
//#endregion
//#region src/node/server.ts
/**
* Compose an h3 + WebSocket server for a devframe context. The RPC
* group is bound to `context.rpc.functions`; the WS endpoint lives on
* the same port as the HTTP server.
*/
async function startHttpAndWs(options) {
const { context, port } = options;
const bindHost = options.host ?? "localhost";
const app = options.app ?? new H3();
const ownsHttpServer = !options.server;
const httpServer = options.server ?? createServer(toNodeHandler(app));
const rpcHost = context.rpc;
const asyncStorage = new AsyncLocalStorage();
const authHandler = typeof options.auth === "object" ? options.auth : void 0;
const effectiveAuthorize = options.authorize ?? authHandler?.authorize;
if (authHandler) {
for (const fn of authHandler.rpcFunctions) if (!rpcHost.definitions.has(fn.name)) rpcHost.register(fn);
}
const rpcGroup = createRpcServer(rpcHost.functions, { rpcOptions: {
onFunctionError: options.rpcOptions?.onFunctionError,
onGeneralError: options.rpcOptions?.onGeneralError,
resolver(name, fn) {
const rpc = this;
if (!fn) return void 0;
return async function(...args) {
const meta = rpc.$meta;
if (effectiveAuthorize && !effectiveAuthorize(name, {
meta,
rpc
})) throw diagnostics.DF0036({ name });
return await asyncStorage.run({
rpc,
meta
}, async () => {
return (await fn).apply(this, args);
});
};
}
} });
const separateWsPort = ownsHttpServer && options.wsPort != null && options.wsPort !== port ? options.wsPort : void 0;
const { ws, close: closeWs } = attachWsRpcTransport(rpcGroup, {
...separateWsPort != null ? {
port: separateWsPort,
host: bindHost
} : { server: httpServer },
path: options.path,
destroyUnmatched: ownsHttpServer,
allowedOrigins: options.allowedOrigins,
onConnected: authHandler || options.onPeerConnect ? (peer, meta) => {
const session = {
meta,
rpc: rpcGroup.clients.find((client) => client.$meta === meta)
};
authHandler?.onConnect(peer, session);
options.onPeerConnect?.(peer, session);
} : void 0,
onDisconnected: (_peer, meta) => {
rpcHost._emitSessionDisconnected(meta);
}
});
rpcHost._rpcGroup = rpcGroup;
rpcHost._asyncStorage = asyncStorage;
rpcHost._authDisabled = options.auth === false;
if (options.auth === false && !rpcHost.definitions.has("anonymous:devframe:auth")) rpcHost.register({
name: "anonymous:devframe:auth",
type: "action",
handler: () => {
const session = rpcHost.getCurrentRpcSession();
if (session) session.meta.isTrusted = true;
return { isTrusted: true };
}
});
if (ownsHttpServer) await new Promise((resolveListen) => {
httpServer.listen(port, bindHost, () => resolveListen());
});
const address = httpServer.address();
const resolvedPort = typeof address === "object" && address ? address.port : port;
const origin = normalizeHttpServerUrl(bindHost, resolvedPort);
const internal = getInternalContext(context);
const wsPortForUrl = separateWsPort ?? resolvedPort;
const wsUrl = `ws://${formatHostForUrl(bindHost)}:${wsPortForUrl}${options.path ?? ""}`;
internal.wsEndpoint = { url: wsUrl };
if (options.onReady) await options.onReady({
origin,
port: resolvedPort,
app
});
function connectionMeta() {
const jsonSerializableMethods = [];
for (const def of rpcHost.definitions.values()) if (def.jsonSerializable === true) jsonSerializableMethods.push(def.name);
return {
backend: "websocket",
websocket: separateWsPort != null ? {
port: separateWsPort,
path: options.path
} : { path: options.path },
jsonSerializableMethods
};
}
return {
origin,
port: resolvedPort,
app,
ws,
rpcGroup,
connectionMeta,
async close() {
await closeWs();
if (ownsHttpServer) await new Promise((r) => httpServer.close(() => r()));
if (getInternalContext(context).wsEndpoint?.url === wsUrl) getInternalContext(context).wsEndpoint = void 0;
}
};
}
//#endregion
export { toDialableHost as a, normalizeHttpServerUrl as i, formatHostForUrl as n, isObject as r, startHttpAndWs as t };
import { $ as DevframeRpcServerFunctions, Q as DevframeRpcClientFunctions, W as DevframeNodeRpcSession, _ as ConnectionMeta, b as DevframeNodeContext, p as DevframeAuthHandler } from "./devframe-qgCKL683.mjs";
import { r as WsOriginRegistry } from "./ws-server-J-zkiOyp.mjs";
import "./index-B7RgLw--.mjs";
import { Peer } from "crossws";
import { BirpcGroup, EventOptions } from "birpc";
import { NodeAdapter } from "crossws/adapters/node";
import { Server } from "node:http";
import { H3 } from "h3";
//#region src/node/server.d.ts
interface StartHttpAndWsOptions {
context: DevframeNodeContext;
host?: string;
port: number;
/**
* Optional h3 app to mount on. When omitted a fresh one is created;
* when provided, callers can add their own routes (static handlers,
* auth middleware, etc.) first.
*/
app?: H3;
/**
* Bind the WS endpoint to a single upgrade route (e.g. `/__devframe_ws`) instead of
* claiming every upgrade on the port. This lets the socket share a server
* with other upgrade handlers (Vite HMR, a host framework's own sockets)
* and is what the SPA's `__connection.json` points at. When omitted, the WS
* server handles every upgrade on the port (legacy behaviour).
*/
path?: string;
/**
* Bind the WS endpoint on its own port instead of sharing the HTTP server's.
* The HTTP/SPA server still listens on `port`; the socket gets a dedicated
* `ws` server on `wsPort` (same `host`). Use this for the "different port"
* connection scenario. Ignored when a `server` is supplied.
*/
wsPort?: number;
/**
* Mount the WS endpoint onto an existing HTTP server, sharing its port,
* rather than creating and listening on a fresh one. Use this to embed
* devframe's RPC socket inside a host server (e.g. a Vite dev server) — pair
* it with `path` so it coexists with the host's routes. The caller owns the
* server's lifecycle: {@link StartedServer.close} detaches devframe's upgrade
* listener but leaves the host server running. When set, `host`/`port` are
* only used to report the resolved origin.
*/
server?: Server;
/**
* Authentication for the server:
*
* - `true` (default) — no gate; every registered method is callable
* regardless of trust (today's behavior, unchanged).
* - `false` — the RPC server is started without a trust handshake.
* Intended for single-user localhost tools where an auth round-trip
* would only get in the way. A noop `anonymous:devframe:auth` handler
* is registered so the browser client's unconditional handshake call
* succeeds and auto-trusts.
* - A {@link DevframeAuthHandler} (e.g. from
* `devframe/recipes/interactive-auth`'s `createInteractiveAuth`) —
* registers its `rpcFunctions`, wires its `authorize` as the resolver
* gate, and wires its `onConnect` on every new peer. This is the
* fully-authenticated server: an untrusted caller can only reach
* `anonymous:`-prefixed methods (see `isAnonymousRpcMethod`).
*/
auth?: boolean | DevframeAuthHandler;
/**
* Lower-level escape hatch: gate individual RPC calls by method name and
* session without a full {@link DevframeAuthHandler}. Ignored when `auth`
* is a handler object (its own `authorize` is used); combine with `auth:
* true` to layer a custom policy on top of an otherwise ungated server.
*/
authorize?: (methodName: string, session: DevframeNodeRpcSession) => boolean;
/**
* Called once per new WS connection, right after its session is created
* (before any RPC call is dispatched). Runs after the auth handler's own
* `onConnect` (when `auth` is a {@link DevframeAuthHandler}), so it can
* observe — but not override — the connect-time trust decision.
*/
onPeerConnect?: (peer: Peer, session: DevframeNodeRpcSession) => void;
/**
* Forwarded verbatim to the internal `createRpcServer`'s birpc
* `rpcOptions`, alongside the resolver `startHttpAndWs` installs for
* auth/session wiring. Use this so a host that owns its own structured
* diagnostics (e.g. a coded error reporter) keeps seeing RPC failures
* instead of them being silently absorbed by delegating to
* `startHttpAndWs`. Returning `true` from either callback suppresses
* birpc's own error response to the caller — see birpc's
* `EventOptions` for the full contract.
*/
rpcOptions?: Pick<EventOptions<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>, 'onFunctionError' | 'onGeneralError'>;
/**
* Extra origins to accept on the WS upgrade beyond the loopback default
* (`localhost`/`127.0.0.1`/`::1` and any `Origin`-less request from a
* native client). Add your LAN/tunnel origin here when reaching the tool
* from another host. Pass `false` to disable origin checking entirely
* (not recommended). Default: loopback-only.
*/
allowedOrigins?: readonly string[] | WsOriginRegistry | false;
/**
* Called once the WS server is bound so callers can mount static
* handlers whose origin depends on the resolved port, or print their
* own startup banner. Devframe does not print one itself.
*/
onReady?: (info: {
origin: string;
port: number;
app: H3;
}) => void | Promise<void>;
}
interface StartedServer {
/** Listening origin, e.g. `http://localhost:9999`. */
origin: string;
port: number;
app: H3;
/** The crossws node adapter driving the RPC socket (connected peers, pub/sub). */
ws: NodeAdapter;
rpcGroup: BirpcGroup<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>;
/**
* The {@link ConnectionMeta} descriptor for this server — the same shape
* a `__connection.json` route should serve so a devframe client's
* `resolveWsUrl` can dial back in. Reflects the `path` / `wsPort` this
* server was started with and the `jsonSerializable` methods currently
* registered on `context.rpc`.
*/
connectionMeta: () => ConnectionMeta;
close: () => Promise<void>;
}
/**
* Compose an h3 + WebSocket server for a devframe context. The RPC
* group is bound to `context.rpc.functions`; the WS endpoint lives on
* the same port as the HTTP server.
*/
declare function startHttpAndWs(options: StartHttpAndWsOptions): Promise<StartedServer>;
//#endregion
export { StartedServer as n, startHttpAndWs as r, StartHttpAndWsOptions as t };
import { t as diagnostics } from "./diagnostics-B5-qHeqD.mjs";
import fs from "node:fs";
import { dirname } from "pathe";
import process$1 from "node:process";
import { destr } from "destr";
//#region src/utils/events.ts
/**
* Create event emitter.
*/
function createEventEmitter() {
const _listeners = {};
function emit(event, ...args) {
const callbacks = _listeners[event] || [];
for (let i = 0, length = callbacks.length; i < length; i++) {
const callback = callbacks[i];
if (callback) callback(...args);
}
}
function emitOnce(event, ...args) {
emit(event, ...args);
delete _listeners[event];
}
function on(event, cb) {
(_listeners[event] ||= []).push(cb);
return () => {
_listeners[event] = _listeners[event]?.filter((i) => cb !== i);
};
}
function once(event, cb) {
const unsubscribe = on(event, ((...args) => {
unsubscribe();
return cb(...args);
}));
return unsubscribe;
}
return {
_listeners,
emit,
emitOnce,
on,
once
};
}
//#endregion
//#region ../../node_modules/.pnpm/immer@11.1.15/node_modules/immer/dist/immer.mjs
var NOTHING = Symbol.for("immer-nothing");
var DRAFTABLE = Symbol.for("immer-draftable");
var DRAFT_STATE = Symbol.for("immer-state");
var errors = process.env.NODE_ENV !== "production" ? [
function(plugin) {
return `The plugin for '${plugin}' has not been loaded into Immer. To enable the plugin, import and call \`enable${plugin}()\` when initializing your application.`;
},
function(thing) {
return `produce can only be called on things that are draftable: plain objects, arrays, Map, Set or classes that are marked with '[immerable]: true'. Got '${thing}'`;
},
"This object has been frozen and should not be mutated",
function(data) {
return "Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? " + data;
},
"An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft.",
"Immer forbids circular references",
"The first or second argument to `produce` must be a function",
"The third argument to `produce` must be a function or undefined",
"First argument to `createDraft` must be a plain object, an array, or an immerable object",
"First argument to `finishDraft` must be a draft returned by `createDraft`",
function(thing) {
return `'current' expects a draft, got: ${thing}`;
},
"Object.defineProperty() cannot be used on an Immer draft",
"Object.setPrototypeOf() cannot be used on an Immer draft",
"Immer only supports deleting array indices",
"Immer only supports setting array indices and the 'length' property",
function(thing) {
return `'original' expects a draft, got: ${thing}`;
}
] : [];
function die(error, ...args) {
if (process.env.NODE_ENV !== "production") {
const e = errors[error];
const msg = isFunction(e) ? e.apply(null, args) : e;
throw new Error(`[Immer] ${msg}`);
}
throw new Error(`[Immer] minified error nr: ${error}. Full error at: https://bit.ly/3cXEKWf`);
}
var O = Object;
var getPrototypeOf = O.getPrototypeOf;
var CONSTRUCTOR = "constructor";
var PROTOTYPE = "prototype";
var CONFIGURABLE = "configurable";
var ENUMERABLE = "enumerable";
var WRITABLE = "writable";
var VALUE = "value";
var isDraft = (value) => !!value && !!value[DRAFT_STATE];
function isDraftable(value) {
if (!value) return false;
return isPlainObject(value) || isArray(value) || !!value[DRAFTABLE] || !!value[CONSTRUCTOR]?.[DRAFTABLE] || isMap(value) || isSet(value);
}
var objectCtorString = O[PROTOTYPE][CONSTRUCTOR].toString();
var cachedCtorStrings = /* @__PURE__ */ new WeakMap();
function isPlainObject(value) {
if (!value || !isObjectish(value)) return false;
const proto = getPrototypeOf(value);
if (proto === null || proto === O[PROTOTYPE]) return true;
const Ctor = O.hasOwnProperty.call(proto, CONSTRUCTOR) && proto[CONSTRUCTOR];
if (Ctor === Object) return true;
if (!isFunction(Ctor)) return false;
let ctorString = cachedCtorStrings.get(Ctor);
if (ctorString === void 0) {
ctorString = Function.toString.call(Ctor);
cachedCtorStrings.set(Ctor, ctorString);
}
return ctorString === objectCtorString;
}
function each(obj, iter, strict = true) {
if (getArchtype(obj) === 0) (strict ? Reflect.ownKeys(obj) : O.keys(obj)).forEach((key) => {
iter(key, obj[key], obj);
});
else obj.forEach((entry, index) => iter(index, entry, obj));
}
function getArchtype(thing) {
const state = thing[DRAFT_STATE];
return state ? state.type_ : isArray(thing) ? 1 : isMap(thing) ? 2 : isSet(thing) ? 3 : 0;
}
var has = (thing, prop, type = getArchtype(thing)) => type === 2 ? thing.has(prop) : O[PROTOTYPE].hasOwnProperty.call(thing, prop);
var get = (thing, prop, type = getArchtype(thing)) => type === 2 ? thing.get(prop) : thing[prop];
var set = (thing, propOrOldValue, value, type = getArchtype(thing)) => {
if (type === 2) thing.set(propOrOldValue, value);
else if (type === 3) thing.add(value);
else thing[propOrOldValue] = value;
};
function is(x, y) {
if (x === y) return x !== 0 || 1 / x === 1 / y;
else return x !== x && y !== y;
}
var isArray = Array.isArray;
var isMap = (target) => target instanceof Map;
var isSet = (target) => target instanceof Set;
var isObjectish = (target) => typeof target === "object";
var isFunction = (target) => typeof target === "function";
var isBoolean = (target) => typeof target === "boolean";
function isArrayIndex(value) {
const n = +value;
return Number.isInteger(n) && String(n) === value;
}
var getProxyDraft = (value) => {
if (!isObjectish(value)) return null;
return value?.[DRAFT_STATE];
};
var latest = (state) => state.copy_ || state.base_;
var getFinalValue = (state) => state.modified_ ? state.copy_ : state.base_;
function shallowCopy(base, strict) {
if (isMap(base)) return new Map(base);
if (isSet(base)) return new Set(base);
if (isArray(base)) return Array[PROTOTYPE].slice.call(base);
const isPlain = isPlainObject(base);
if (strict === true || strict === "class_only" && !isPlain) {
const descriptors = O.getOwnPropertyDescriptors(base);
delete descriptors[DRAFT_STATE];
let keys = Reflect.ownKeys(descriptors);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const desc = descriptors[key];
if (desc[WRITABLE] === false) {
desc[WRITABLE] = true;
desc[CONFIGURABLE] = true;
}
if (desc.get || desc.set) descriptors[key] = {
[CONFIGURABLE]: true,
[WRITABLE]: true,
[ENUMERABLE]: desc[ENUMERABLE],
[VALUE]: base[key]
};
}
return O.create(getPrototypeOf(base), descriptors);
} else {
const proto = getPrototypeOf(base);
if (proto !== null && isPlain) return { ...base };
const obj = O.create(proto);
return O.assign(obj, base);
}
}
function freeze(obj, deep = false) {
if (isFrozen(obj) || isDraft(obj) || !isDraftable(obj)) return obj;
if (getArchtype(obj) > 1) O.defineProperties(obj, {
set: dontMutateMethodOverride,
add: dontMutateMethodOverride,
clear: dontMutateMethodOverride,
delete: dontMutateMethodOverride
});
O.freeze(obj);
if (deep) each(obj, (_key, value) => {
freeze(value, true);
}, false);
return obj;
}
function dontMutateFrozenCollections() {
die(2);
}
var dontMutateMethodOverride = { [VALUE]: dontMutateFrozenCollections };
function isFrozen(obj) {
if (obj === null || !isObjectish(obj)) return true;
return O.isFrozen(obj);
}
var PluginMapSet = "MapSet";
var PluginPatches = "Patches";
var PluginArrayMethods = "ArrayMethods";
var plugins = {};
function getPlugin(pluginKey) {
const plugin = plugins[pluginKey];
if (!plugin) die(0, pluginKey);
return plugin;
}
var isPluginLoaded = (pluginKey) => !!plugins[pluginKey];
function loadPlugin(pluginKey, implementation) {
if (!plugins[pluginKey]) plugins[pluginKey] = implementation;
}
var currentScope;
var getCurrentScope = () => currentScope;
var createScope = (parent_, immer_) => ({
drafts_: [],
parent_,
immer_,
canAutoFreeze_: true,
unfinalizedDrafts_: 0,
handledSet_: /* @__PURE__ */ new Set(),
processedForPatches_: /* @__PURE__ */ new Set(),
mapSetPlugin_: isPluginLoaded(PluginMapSet) ? getPlugin(PluginMapSet) : void 0,
arrayMethodsPlugin_: isPluginLoaded(PluginArrayMethods) ? getPlugin(PluginArrayMethods) : void 0
});
function usePatchesInScope(scope, patchListener) {
if (patchListener) {
scope.patchPlugin_ = getPlugin(PluginPatches);
scope.patches_ = [];
scope.inversePatches_ = [];
scope.patchListener_ = patchListener;
}
}
function revokeScope(scope) {
leaveScope(scope);
scope.drafts_.forEach(revokeDraft);
scope.drafts_ = null;
}
function leaveScope(scope) {
if (scope === currentScope) currentScope = scope.parent_;
}
var enterScope = (immer2) => currentScope = createScope(currentScope, immer2);
function revokeDraft(draft) {
const state = draft[DRAFT_STATE];
if (state.type_ === 0 || state.type_ === 1) state.revoke_();
else state.revoked_ = true;
}
function processResult(result, scope) {
scope.unfinalizedDrafts_ = scope.drafts_.length;
const baseDraft = scope.drafts_[0];
if (result !== void 0 && result !== baseDraft) {
if (baseDraft[DRAFT_STATE].modified_) {
revokeScope(scope);
die(4);
}
if (isDraftable(result)) result = finalize(scope, result);
const { patchPlugin_ } = scope;
if (patchPlugin_) patchPlugin_.generateReplacementPatches_(baseDraft[DRAFT_STATE].base_, result, scope);
} else result = finalize(scope, baseDraft);
maybeFreeze(scope, result, true);
revokeScope(scope);
if (scope.patches_) scope.patchListener_(scope.patches_, scope.inversePatches_);
return result !== NOTHING ? result : void 0;
}
function finalize(rootScope, value) {
if (isFrozen(value)) return value;
const state = value[DRAFT_STATE];
if (!state) return handleValue(value, rootScope.handledSet_, rootScope);
if (!isSameScope(state, rootScope)) return value;
if (!state.modified_) return state.base_;
if (!state.finalized_) {
const { callbacks_ } = state;
if (callbacks_) while (callbacks_.length > 0) callbacks_.pop()(rootScope);
generatePatchesAndFinalize(state, rootScope);
}
return state.copy_;
}
function maybeFreeze(scope, value, deep = false) {
if (!scope.parent_ && scope.immer_.autoFreeze_ && scope.canAutoFreeze_) freeze(value, deep);
}
function markStateFinalized(state) {
state.finalized_ = true;
state.scope_.unfinalizedDrafts_--;
}
var isSameScope = (state, rootScope) => state.scope_ === rootScope;
var EMPTY_LOCATIONS_RESULT = [];
function updateDraftInParent(parent, draftValue, finalizedValue, originalKey) {
const parentCopy = latest(parent);
const parentType = parent.type_;
if (originalKey !== void 0) {
if (get(parentCopy, originalKey, parentType) === draftValue) {
set(parentCopy, originalKey, finalizedValue, parentType);
return;
}
}
if (!parent.draftLocations_) {
const draftLocations = parent.draftLocations_ = /* @__PURE__ */ new Map();
each(parentCopy, (key, value) => {
if (isDraft(value)) {
const keys = draftLocations.get(value) || [];
keys.push(key);
draftLocations.set(value, keys);
}
});
}
const locations = parent.draftLocations_.get(draftValue) ?? EMPTY_LOCATIONS_RESULT;
for (const location of locations) set(parentCopy, location, finalizedValue, parentType);
}
function registerChildFinalizationCallback(parent, child, key) {
parent.callbacks_.push(function childCleanup(rootScope) {
const state = child;
if (!state || !isSameScope(state, rootScope)) return;
rootScope.mapSetPlugin_?.fixSetContents(state);
const finalizedValue = getFinalValue(state);
updateDraftInParent(parent, state.draft_ ?? state, finalizedValue, key);
generatePatchesAndFinalize(state, rootScope);
});
}
function generatePatchesAndFinalize(state, rootScope) {
if (state.modified_ && !state.finalized_ && (state.type_ === 3 || state.type_ === 1 && state.allIndicesReassigned_ || (state.assigned_?.size ?? 0) > 0)) {
const { patchPlugin_ } = rootScope;
if (patchPlugin_) {
const basePath = patchPlugin_.getPath(state);
if (basePath) patchPlugin_.generatePatches_(state, basePath, rootScope);
}
markStateFinalized(state);
}
}
function handleCrossReference(target, key, value) {
const { scope_ } = target;
if (isDraft(value)) {
const state = value[DRAFT_STATE];
if (isSameScope(state, scope_)) state.callbacks_.push(function crossReferenceCleanup() {
prepareCopy(target);
updateDraftInParent(target, value, getFinalValue(state), key);
});
} else if (isDraftable(value)) target.callbacks_.push(function nestedDraftCleanup() {
const targetCopy = latest(target);
if (target.type_ === 3) {
if (targetCopy.has(value)) handleValue(value, scope_.handledSet_, scope_);
} else if (get(targetCopy, key, target.type_) === value) {
if (scope_.drafts_.length > 1 && (target.assigned_.get(key) ?? false) === true && target.copy_) handleValue(get(target.copy_, key, target.type_), scope_.handledSet_, scope_);
}
});
}
function handleValue(target, handledSet, rootScope) {
if (!rootScope.immer_.autoFreeze_ && rootScope.unfinalizedDrafts_ < 1) return target;
if (isDraft(target) || handledSet.has(target) || !isDraftable(target) || isFrozen(target)) return target;
handledSet.add(target);
each(target, (key, value) => {
if (isDraft(value)) {
const state = value[DRAFT_STATE];
if (isSameScope(state, rootScope)) {
set(target, key, getFinalValue(state), target.type_);
markStateFinalized(state);
}
} else if (isDraftable(value)) handleValue(value, handledSet, rootScope);
});
return target;
}
function createProxyProxy(base, parent) {
const baseIsArray = isArray(base);
const state = {
type_: baseIsArray ? 1 : 0,
scope_: parent ? parent.scope_ : getCurrentScope(),
modified_: false,
finalized_: false,
assigned_: void 0,
parent_: parent,
base_: base,
draft_: null,
copy_: null,
revoke_: null,
isManual_: false,
callbacks_: void 0
};
let target = state;
let traps = objectTraps;
if (baseIsArray) {
target = [state];
traps = arrayTraps;
}
const { revoke, proxy } = Proxy.revocable(target, traps);
state.draft_ = proxy;
state.revoke_ = revoke;
return [proxy, state];
}
var objectTraps = {
get(state, prop) {
if (prop === DRAFT_STATE) return state;
let arrayPlugin = state.scope_.arrayMethodsPlugin_;
const isArrayWithStringProp = state.type_ === 1 && typeof prop === "string";
if (isArrayWithStringProp) {
if (arrayPlugin?.isArrayOperationMethod(prop)) return arrayPlugin.createMethodInterceptor(state, prop);
}
const source = latest(state);
if (!has(source, prop, state.type_)) return readPropFromProto(state, source, prop);
const value = source[prop];
if (state.finalized_ || !isDraftable(value)) return value;
if (isArrayWithStringProp && state.operationMethod && arrayPlugin?.isMutatingArrayMethod(state.operationMethod) && isArrayIndex(prop)) return value;
if (value === peek(state.base_, prop) || isRelocatedBaseRef(state, prop, value)) {
prepareCopy(state);
const childKey = state.type_ === 1 ? +prop : prop;
const childDraft = createProxy(state.scope_, value, state, childKey);
return state.copy_[childKey] = childDraft;
}
return value;
},
has(state, prop) {
return prop in latest(state);
},
ownKeys(state) {
return Reflect.ownKeys(latest(state));
},
set(state, prop, value) {
const desc = getDescriptorFromProto(latest(state), prop);
if (desc?.set) {
desc.set.call(state.draft_, value);
return true;
}
if (!state.modified_) {
const current2 = peek(latest(state), prop);
const currentState = current2?.[DRAFT_STATE];
if (currentState && currentState.base_ === value) {
state.copy_[prop] = value;
state.assigned_.set(prop, false);
return true;
}
if (is(value, current2) && (value !== void 0 || has(state.base_, prop, state.type_))) return true;
prepareCopy(state);
markChanged(state);
}
if (state.copy_[prop] === value && (value !== void 0 || has(state.copy_, prop, state.type_)) || Number.isNaN(value) && Number.isNaN(state.copy_[prop])) return true;
state.copy_[prop] = value;
state.assigned_.set(prop, true);
handleCrossReference(state, prop, value);
return true;
},
deleteProperty(state, prop) {
prepareCopy(state);
if (peek(state.base_, prop) !== void 0 || prop in state.base_) {
state.assigned_.set(prop, false);
markChanged(state);
} else state.assigned_.delete(prop);
if (state.copy_) delete state.copy_[prop];
return true;
},
getOwnPropertyDescriptor(state, prop) {
const owner = latest(state);
const desc = Reflect.getOwnPropertyDescriptor(owner, prop);
if (!desc) return desc;
return {
[WRITABLE]: true,
[CONFIGURABLE]: state.type_ !== 1 || prop !== "length",
[ENUMERABLE]: desc[ENUMERABLE],
[VALUE]: owner[prop]
};
},
defineProperty() {
die(11);
},
getPrototypeOf(state) {
return getPrototypeOf(state.base_);
},
setPrototypeOf() {
die(12);
}
};
var arrayTraps = {};
for (let key in objectTraps) {
let fn = objectTraps[key];
arrayTraps[key] = function() {
const args = arguments;
args[0] = args[0][0];
return fn.apply(this, args);
};
}
arrayTraps.deleteProperty = function(state, prop) {
if (process.env.NODE_ENV !== "production" && isNaN(parseInt(prop))) die(13);
return arrayTraps.set.call(this, state, prop, void 0);
};
arrayTraps.set = function(state, prop, value) {
if (process.env.NODE_ENV !== "production" && prop !== "length" && isNaN(parseInt(prop))) die(14);
return objectTraps.set.call(this, state[0], prop, value, state[0]);
};
function peek(draft, prop) {
const state = draft[DRAFT_STATE];
return (state ? latest(state) : draft)[prop];
}
function isRelocatedBaseRef(state, prop, value) {
if (state.type_ !== 1 || !state.allIndicesReassigned_ || state.assigned_?.get(prop) || !isDraftable(value) || value[DRAFT_STATE]) return false;
return state.baseRefs_.has(value);
}
function readPropFromProto(state, source, prop) {
const desc = getDescriptorFromProto(source, prop);
return desc ? VALUE in desc ? desc[VALUE] : desc.get?.call(state.draft_) : void 0;
}
function getDescriptorFromProto(source, prop) {
if (!(prop in source)) return void 0;
let proto = getPrototypeOf(source);
while (proto) {
const desc = Object.getOwnPropertyDescriptor(proto, prop);
if (desc) return desc;
proto = getPrototypeOf(proto);
}
}
function markChanged(state) {
if (!state.modified_) {
state.modified_ = true;
if (state.parent_) markChanged(state.parent_);
}
}
function prepareCopy(state) {
if (!state.copy_) {
state.assigned_ = /* @__PURE__ */ new Map();
state.copy_ = shallowCopy(state.base_, state.scope_.immer_.useStrictShallowCopy_);
}
}
var Immer2 = class {
constructor(config) {
this.autoFreeze_ = true;
this.useStrictShallowCopy_ = false;
this.useStrictIteration_ = false;
/**
* The `produce` function takes a value and a "recipe function" (whose
* return value often depends on the base state). The recipe function is
* free to mutate its first argument however it wants. All mutations are
* only ever applied to a __copy__ of the base state.
*
* Pass only a function to create a "curried producer" which relieves you
* from passing the recipe function every time.
*
* Only plain objects and arrays are made mutable. All other objects are
* considered uncopyable.
*
* Note: This function is __bound__ to its `Immer` instance.
*
* @param {any} base - the initial state
* @param {Function} recipe - function that receives a proxy of the base state as first argument and which can be freely modified
* @param {Function} patchListener - optional function that will be called with all the patches produced here
* @returns {any} a new state, or the initial state if nothing was modified
*/
this.produce = (base, recipe, patchListener) => {
if (isFunction(base) && !isFunction(recipe)) {
const defaultBase = recipe;
recipe = base;
const self = this;
return function curriedProduce(base2 = defaultBase, ...args) {
return self.produce(base2, (draft) => recipe.call(this, draft, ...args));
};
}
if (!isFunction(recipe)) die(6);
if (patchListener !== void 0 && !isFunction(patchListener)) die(7);
let result;
if (isDraftable(base)) {
const scope = enterScope(this);
const proxy = createProxy(scope, base, void 0);
let hasError = true;
try {
result = recipe(proxy);
hasError = false;
} finally {
if (hasError) revokeScope(scope);
else leaveScope(scope);
}
usePatchesInScope(scope, patchListener);
return processResult(result, scope);
} else if (!base || !isObjectish(base)) {
result = recipe(base);
if (result === void 0) result = base;
if (result === NOTHING) result = void 0;
if (this.autoFreeze_) freeze(result, true);
if (patchListener) {
const p = [];
const ip = [];
getPlugin(PluginPatches).generateReplacementPatches_(base, result, {
patches_: p,
inversePatches_: ip
});
patchListener(p, ip);
}
return result;
} else die(1, base);
};
this.produceWithPatches = (base, recipe) => {
if (isFunction(base)) return (state, ...args) => this.produceWithPatches(state, (draft) => base(draft, ...args));
let patches, inversePatches;
return [
this.produce(base, recipe, (p, ip) => {
patches = p;
inversePatches = ip;
}),
patches,
inversePatches
];
};
if (isBoolean(config?.autoFreeze)) this.setAutoFreeze(config.autoFreeze);
if (isBoolean(config?.useStrictShallowCopy)) this.setUseStrictShallowCopy(config.useStrictShallowCopy);
if (isBoolean(config?.useStrictIteration)) this.setUseStrictIteration(config.useStrictIteration);
}
createDraft(base) {
if (!isDraftable(base)) die(8);
if (isDraft(base)) base = current(base);
const scope = enterScope(this);
const proxy = createProxy(scope, base, void 0);
proxy[DRAFT_STATE].isManual_ = true;
leaveScope(scope);
return proxy;
}
finishDraft(draft, patchListener) {
const state = draft && draft[DRAFT_STATE];
if (!state || !state.isManual_) die(9);
const { scope_: scope } = state;
usePatchesInScope(scope, patchListener);
return processResult(void 0, scope);
}
/**
* Pass true to automatically freeze all copies created by Immer.
*
* By default, auto-freezing is enabled.
*/
setAutoFreeze(value) {
this.autoFreeze_ = value;
}
/**
* Pass true to enable strict shallow copy.
*
* By default, immer does not copy the object descriptors such as getter, setter and non-enumrable properties.
*/
setUseStrictShallowCopy(value) {
this.useStrictShallowCopy_ = value;
}
/**
* Pass false to use faster iteration that skips non-enumerable properties
* but still handles symbols for compatibility.
*
* By default, strict iteration is enabled (includes all own properties).
*/
setUseStrictIteration(value) {
this.useStrictIteration_ = value;
}
shouldUseStrictIteration() {
return this.useStrictIteration_;
}
applyPatches(base, patches) {
let i;
for (i = patches.length - 1; i >= 0; i--) {
const patch = patches[i];
if (patch.path.length === 0 && patch.op === "replace") {
base = patch.value;
break;
}
}
if (i > -1) patches = patches.slice(i + 1);
const applyPatchesImpl = getPlugin(PluginPatches).applyPatches_;
if (isDraft(base)) return applyPatchesImpl(base, patches);
return this.produce(base, (draft) => applyPatchesImpl(draft, patches));
}
};
function createProxy(rootScope, value, parent, key) {
const [draft, state] = isMap(value) ? getPlugin(PluginMapSet).proxyMap_(value, parent) : isSet(value) ? getPlugin(PluginMapSet).proxySet_(value, parent) : createProxyProxy(value, parent);
(parent?.scope_ ?? getCurrentScope()).drafts_.push(draft);
state.callbacks_ = parent?.callbacks_ ?? [];
state.key_ = key;
if (parent && key !== void 0) registerChildFinalizationCallback(parent, state, key);
else state.callbacks_.push(function rootDraftCleanup(rootScope2) {
rootScope2.mapSetPlugin_?.fixSetContents(state);
const { patchPlugin_ } = rootScope2;
if (state.modified_ && patchPlugin_) patchPlugin_.generatePatches_(state, [], rootScope2);
});
return draft;
}
function current(value) {
if (!isDraft(value)) die(10, value);
return currentImpl(value);
}
function currentImpl(value) {
if (!isDraftable(value) || isFrozen(value)) return value;
const state = value[DRAFT_STATE];
let copy;
let strict = true;
if (state) {
if (!state.modified_) return state.base_;
state.finalized_ = true;
copy = shallowCopy(value, state.scope_.immer_.useStrictShallowCopy_);
strict = state.scope_.immer_.shouldUseStrictIteration();
} else copy = shallowCopy(value, true);
each(copy, (key, childValue) => {
set(copy, key, currentImpl(childValue));
}, strict);
if (state) state.finalized_ = false;
return copy;
}
function enablePatches() {
const errorOffset = 16;
if (process.env.NODE_ENV !== "production") errors.push("Sets cannot have \"replace\" patches.", function(op) {
return "Unsupported patch operation: " + op;
}, function(path) {
return "Cannot apply patch, path doesn't resolve: " + path;
}, "Patching reserved attributes like __proto__, prototype and constructor is not allowed");
function getPath(state, path = []) {
if (state.key_ !== void 0) {
const parentCopy = state.parent_.copy_ ?? state.parent_.base_;
const proxyDraft = getProxyDraft(get(parentCopy, state.key_));
const valueAtKey = get(parentCopy, state.key_);
if (valueAtKey === void 0) return null;
if (valueAtKey !== state.draft_ && valueAtKey !== state.base_ && valueAtKey !== state.copy_) return null;
if (proxyDraft != null && proxyDraft.base_ !== state.base_) return null;
const isSet2 = state.parent_.type_ === 3;
let key;
if (isSet2) {
const setParent = state.parent_;
key = Array.from(setParent.drafts_.keys()).indexOf(state.key_);
} else key = state.key_;
if (!(isSet2 && parentCopy.size > key || has(parentCopy, key))) return null;
path.push(key);
}
if (state.parent_) return getPath(state.parent_, path);
path.reverse();
try {
resolvePath(state.copy_, path);
} catch (e) {
return null;
}
return path;
}
function resolvePath(base, path) {
let current2 = base;
for (let i = 0; i < path.length - 1; i++) {
const key = path[i];
current2 = get(current2, key);
if (!isObjectish(current2) || current2 === null) throw new Error(`Cannot resolve path at '${path.join("/")}'`);
}
return current2;
}
const REPLACE = "replace";
const ADD = "add";
const REMOVE = "remove";
function generatePatches_(state, basePath, scope) {
if (state.scope_.processedForPatches_.has(state)) return;
state.scope_.processedForPatches_.add(state);
const { patches_, inversePatches_ } = scope;
switch (state.type_) {
case 0:
case 2: return generatePatchesFromAssigned(state, basePath, patches_, inversePatches_);
case 1: return generateArrayPatches(state, basePath, patches_, inversePatches_);
case 3: return generateSetPatches(state, basePath, patches_, inversePatches_);
}
}
function generateArrayPatches(state, basePath, patches, inversePatches) {
let { base_, assigned_ } = state;
let copy_ = state.copy_;
if (copy_.length < base_.length) {
[base_, copy_] = [copy_, base_];
[patches, inversePatches] = [inversePatches, patches];
}
const allReassigned = state.allIndicesReassigned_ === true;
for (let i = 0; i < base_.length; i++) {
const copiedItem = copy_[i];
const baseItem = base_[i];
if ((allReassigned || assigned_?.get(i.toString())) && copiedItem !== baseItem) {
const childState = copiedItem?.[DRAFT_STATE];
if (childState && childState.modified_) continue;
const path = basePath.concat([i]);
patches.push({
op: REPLACE,
path,
value: clonePatchValueIfNeeded(copiedItem)
});
inversePatches.push({
op: REPLACE,
path,
value: clonePatchValueIfNeeded(baseItem)
});
}
}
for (let i = base_.length; i < copy_.length; i++) {
const path = basePath.concat([i]);
patches.push({
op: ADD,
path,
value: clonePatchValueIfNeeded(copy_[i])
});
}
for (let i = copy_.length - 1; base_.length <= i; --i) {
const path = basePath.concat([i]);
inversePatches.push({
op: REMOVE,
path
});
}
}
function generatePatchesFromAssigned(state, basePath, patches, inversePatches) {
const { base_, copy_, type_ } = state;
each(state.assigned_, (key, assignedValue) => {
const origValue = get(base_, key, type_);
const value = get(copy_, key, type_);
const op = !assignedValue ? REMOVE : has(base_, key) ? REPLACE : ADD;
if (origValue === value && op === REPLACE) return;
const path = basePath.concat(key);
patches.push(op === REMOVE ? {
op,
path
} : {
op,
path,
value: clonePatchValueIfNeeded(value)
});
inversePatches.push(op === ADD ? {
op: REMOVE,
path
} : op === REMOVE ? {
op: ADD,
path,
value: clonePatchValueIfNeeded(origValue)
} : {
op: REPLACE,
path,
value: clonePatchValueIfNeeded(origValue)
});
});
}
function generateSetPatches(state, basePath, patches, inversePatches) {
let { base_, copy_ } = state;
let i = 0;
base_.forEach((value) => {
if (!copy_.has(value)) {
const path = basePath.concat([i]);
patches.push({
op: REMOVE,
path,
value
});
inversePatches.unshift({
op: ADD,
path,
value
});
}
i++;
});
i = 0;
copy_.forEach((value) => {
if (!base_.has(value)) {
const path = basePath.concat([i]);
patches.push({
op: ADD,
path,
value
});
inversePatches.unshift({
op: REMOVE,
path,
value
});
}
i++;
});
}
function generateReplacementPatches_(baseValue, replacement, scope) {
const { patches_, inversePatches_ } = scope;
patches_.push({
op: REPLACE,
path: [],
value: replacement === NOTHING ? void 0 : replacement
});
inversePatches_.push({
op: REPLACE,
path: [],
value: baseValue
});
}
function applyPatches_(draft, patches) {
patches.forEach((patch) => {
const { path, op } = patch;
let base = draft;
for (let i = 0; i < path.length - 1; i++) {
const parentType = getArchtype(base);
let p = path[i];
if (typeof p !== "string" && typeof p !== "number") p = "" + p;
if ((parentType === 0 || parentType === 1) && (p === "__proto__" || p === CONSTRUCTOR)) die(19);
if (isFunction(base) && p === PROTOTYPE) die(19);
base = get(base, p);
if (base === null || !isObjectish(base)) die(18, path.join("/"));
}
const type = getArchtype(base);
const value = deepClonePatchValue(patch.value);
const key = path[path.length - 1];
switch (op) {
case REPLACE: switch (type) {
case 2: return base.set(key, value);
case 3: die(errorOffset);
default: return base[key] = value;
}
case ADD: switch (type) {
case 1: return key === "-" ? base.push(value) : base.splice(key, 0, value);
case 2: return base.set(key, value);
case 3: return base.add(value);
default: return base[key] = value;
}
case REMOVE: switch (type) {
case 1: return base.splice(key, 1);
case 2: return base.delete(key);
case 3: return base.delete(patch.value);
default: return delete base[key];
}
default: die(17, op);
}
});
return draft;
}
function deepClonePatchValue(obj) {
if (!isDraftable(obj)) return obj;
if (isArray(obj)) return obj.map(deepClonePatchValue);
if (isMap(obj)) return new Map(Array.from(obj.entries()).map(([k, v]) => [k, deepClonePatchValue(v)]));
if (isSet(obj)) return new Set(Array.from(obj).map(deepClonePatchValue));
const cloned = Object.create(getPrototypeOf(obj));
for (const key in obj) cloned[key] = deepClonePatchValue(obj[key]);
if (has(obj, DRAFTABLE)) cloned[DRAFTABLE] = obj[DRAFTABLE];
return cloned;
}
function clonePatchValueIfNeeded(obj) {
if (isDraft(obj)) return deepClonePatchValue(obj);
else return obj;
}
loadPlugin(PluginPatches, {
applyPatches_,
generatePatches_,
generateReplacementPatches_,
getPath
});
}
var immer = new Immer2();
var produce = immer.produce;
var produceWithPatches = /* @__PURE__ */ immer.produceWithPatches.bind(immer);
var applyPatches = /* @__PURE__ */ immer.applyPatches.bind(immer);
//#endregion
//#region src/utils/nanoid.ts
const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
function nanoid(size = 21) {
let id = "";
let i = size;
while (i--) id += urlAlphabet[Math.random() * 64 | 0];
return id;
}
//#endregion
//#region src/utils/shared-state.ts
/**
* Upper bound on retained syncIds. Loop echoes arrive near-immediately, so a
* generous window preserves de-dup while capping memory on long-lived,
* frequently-mutated states (e.g. a 1s terminal poll).
*/
const MAX_SYNC_IDS = 1e3;
function rememberSyncId(syncIds, syncId) {
syncIds.add(syncId);
if (syncIds.size > MAX_SYNC_IDS) {
const oldest = syncIds.values().next().value;
if (oldest !== void 0) syncIds.delete(oldest);
}
}
function createSharedState(options) {
const { enablePatches: enablePatches$1 = false } = options;
if (enablePatches$1) enablePatches();
const events = createEventEmitter();
let state = options.initialValue;
const syncIds = /* @__PURE__ */ new Set();
return {
on: events.on,
value: () => state,
patch: (patches, syncId = nanoid()) => {
if (syncIds.has(syncId)) return;
enablePatches();
state = applyPatches(state, patches);
rememberSyncId(syncIds, syncId);
events.emit("updated", state, void 0, syncId);
},
mutate: (fn, syncId = nanoid()) => {
if (syncIds.has(syncId)) return;
rememberSyncId(syncIds, syncId);
if (enablePatches$1) {
const [newState, patches] = produceWithPatches(state, fn);
state = newState;
events.emit("updated", state, patches, syncId);
} else {
state = produce(state, fn);
events.emit("updated", state, void 0, syncId);
}
},
syncIds
};
}
//#endregion
//#region ../../node_modules/.pnpm/perfect-debounce@2.1.0/node_modules/perfect-debounce/dist/index.mjs
const DEBOUNCE_DEFAULTS = { trailing: true };
/**
Debounce functions
@param fn - Promise-returning/async function to debounce.
@param wait - Milliseconds to wait before calling `fn`. Default value is 25ms
@returns A function that delays calling `fn` until after `wait` milliseconds have elapsed since the last time it was called.
@example
```
import { debounce } from 'perfect-debounce';
const expensiveCall = async input => input;
const debouncedFn = debounce(expensiveCall, 200);
for (const number of [1, 2, 3]) {
console.log(await debouncedFn(number));
}
//=> 1
//=> 2
//=> 3
```
*/
function debounce(fn, wait = 25, options = {}) {
options = {
...DEBOUNCE_DEFAULTS,
...options
};
if (!Number.isFinite(wait)) throw new TypeError("Expected `wait` to be a finite number");
let leadingValue;
let timeout;
let resolveList = [];
let currentPromise;
let trailingArgs;
const applyFn = (_this, args) => {
currentPromise = _applyPromised(fn, _this, args);
currentPromise.finally(() => {
currentPromise = null;
if (options.trailing && trailingArgs && !timeout) {
const promise = applyFn(_this, trailingArgs);
trailingArgs = null;
return promise;
}
});
return currentPromise;
};
const debounced = function(...args) {
if (options.trailing) trailingArgs = args;
if (currentPromise) return currentPromise;
return new Promise((resolve) => {
const shouldCallNow = !timeout && options.leading;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
const promise = options.leading ? leadingValue : applyFn(this, args);
trailingArgs = null;
for (const _resolve of resolveList) _resolve(promise);
resolveList = [];
}, wait);
if (shouldCallNow) {
leadingValue = applyFn(this, args);
resolve(leadingValue);
} else resolveList.push(resolve);
});
};
const _clearTimeout = (timer) => {
if (timer) {
clearTimeout(timer);
timeout = null;
}
};
debounced.isPending = () => !!timeout;
debounced.cancel = () => {
_clearTimeout(timeout);
resolveList = [];
trailingArgs = null;
};
debounced.flush = () => {
_clearTimeout(timeout);
if (!trailingArgs || currentPromise) return;
const args = trailingArgs;
trailingArgs = null;
return applyFn(this, args);
};
return debounced;
}
async function _applyPromised(fn, _this, args) {
return await fn.apply(_this, args);
}
//#endregion
//#region src/node/storage.ts
function createStorage(options) {
const { mergeInitialValue = (initialValue, savedValue) => ({
...initialValue,
...savedValue
}), debounce: debounceTime = 100 } = options;
let initialValue = options.initialValue;
if (fs.existsSync(options.filepath)) try {
const savedValue = destr(fs.readFileSync(options.filepath, "utf-8"), { strict: true });
initialValue = mergeInitialValue ? mergeInitialValue(options.initialValue, savedValue) : savedValue;
} catch (error) {
diagnostics.DF0012({
filepath: options.filepath,
cause: error
}, { method: "warn" });
initialValue = options.initialValue;
}
const state = createSharedState({
initialValue,
enablePatches: false
});
state.on("updated", debounce((newState) => {
try {
const dir = dirname(options.filepath);
fs.mkdirSync(dir, { recursive: true });
const tmp = `${options.filepath}.${process$1.pid}.tmp`;
fs.writeFileSync(tmp, `${JSON.stringify(newState, null, 2)}\n`);
fs.renameSync(tmp, options.filepath);
} catch (error) {
diagnostics.DF0035({
filepath: options.filepath,
cause: error
}, { method: "error" });
}
}, debounceTime));
return state;
}
//#endregion
export { createEventEmitter as i, createSharedState as n, nanoid as r, createStorage as t };
import { v as RpcFunctionDefinitionAny } from "./types-CnJSgRVa.mjs";
import { Peer } from "crossws";
import { BirpcGroup, ChannelOptions } from "birpc";
import { NodeAdapter } from "crossws/adapters/node";
import { Server } from "node:http";
import { Server as Server$1, ServerOptions } from "node:https";
//#region src/rpc/transports/ws-server.d.ts
interface DevframeNodeRpcSessionMeta {
id: number;
/** The crossws peer backing this session's socket. */
peer?: Peer;
clientAuthToken?: string;
isTrusted?: boolean;
subscribedStates: Set<string>;
/**
* Streams this session has subscribed to via
* `rpc.streaming.subscribe(channel, id)`. Tracked here for O(1) cleanup
* on disconnect; the wire format is `${channel}\x1F${id}`.
*/
subscribedStreams?: Set<string>;
/**
* Inbound streams this session is currently uploading to (via
* `rpc.streaming.upload(channel, id)`). Tracked for cleanup on
* disconnect; same wire format as `subscribedStreams`.
*/
uploadingStreams?: Set<string>;
}
interface WsRpcTransportOptions {
/**
* Attach to an existing HTTP(S) server, sharing its port. Combine with
* `path` to bind the WS endpoint to a single route so it coexists with
* other upgrade handlers on the same server (e.g. a Vite dev server's HMR
* socket). The shared server's lifecycle is owned by the caller — closing
* this transport detaches the upgrade listener without closing the server.
*/
server?: Server | Server$1;
/** Port for a newly-created standalone WS server. */
port?: number;
/** Host for a newly-created standalone WS server. Defaults to `localhost`. */
host?: string;
/**
* Restrict the WS endpoint to a single upgrade route (e.g. `/__devframe_ws`). When
* sharing a `server`, non-matching upgrade requests are left untouched for
* other listeners to handle, so devframe's socket can sit alongside
* framework sockets (Vite HMR, etc.).
*/
path?: string;
/**
* Destroy upgrade requests that don't match `path` instead of leaving them
* for other listeners. Enable this when devframe owns the shared server
* outright (nothing else handles its upgrades), so an off-route client is
* rejected promptly rather than left hanging. Default: `false`
* (coexist-friendly); servers this transport creates itself always
* destroy unmatched upgrades.
*/
destroyUnmatched?: boolean;
/** When set, a new https.Server is created and the WS endpoint is attached to it. */
https?: ServerOptions;
/**
* Extra origins to accept on the WS upgrade beyond the loopback default.
* Add your LAN/tunnel origin here when reaching the tool from another host.
* Pass `false` to disable origin checking entirely (not recommended).
* Default: loopback-only.
*/
allowedOrigins?: readonly string[] | WsOriginRegistry | false;
/**
* RPC function definitions, used by the per-call wire serializer to
* dispatch between strict-JSON and structured-clone encoding based
* on each function's `jsonSerializable` flag.
*
* When omitted, all messages fall back to structured-clone — safe but
* loses dev-time validation for `jsonSerializable: true` declarations.
*/
definitions?: ReadonlyMap<string, Pick<RpcFunctionDefinitionAny, 'jsonSerializable'>>;
onConnected?: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void;
onDisconnected?: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void;
/** Override the default per-call serializer. Most callers should leave this unset. */
serialize?: ChannelOptions['serialize'];
/** Override the default per-call deserializer. Most callers should leave this unset. */
deserialize?: ChannelOptions['deserialize'];
}
interface CreateWsOriginRegistryOptions {
/** Origins allowed before any external viewers are registered. */
allowedOrigins?: readonly string[];
/** Additional validation to run after the registration token is verified. */
validateOrigin?: (origin: string) => boolean;
}
interface WsOriginRegistry {
/** Registration token to include in connection metadata. */
readonly token: string;
/** Read and register an origin from a connection bootstrap URL. */
registerFromUrl: (url: string) => string | undefined;
/** Check whether an origin is currently allowed. */
isAllowed: (origin: string | undefined) => boolean;
}
/**
* Create a live, token-protected origin allowlist for external browser
* viewers. Pass it to {@link WsRpcTransportOptions.allowedOrigins}, then use
* `registerFromUrl()` in the connection metadata handler to authorize a
* viewer without sharing a mutable array or disabling DNS-rebinding protection.
*/
declare function createWsOriginRegistry(options?: CreateWsOriginRegistryOptions): WsOriginRegistry;
interface WsRpcTransport {
/**
* The crossws node adapter driving the socket — exposes the connected
* `peers` and pub/sub. See https://crossws.h3.dev.
*/
ws: NodeAdapter;
/** Remove the upgrade listener from a shared `server` (a no-op otherwise). */
detach: () => void;
/**
* Tear the transport down deterministically: detach from a shared server,
* force-terminate every connected peer, and close any server this
* transport created itself (`port` / `https` modes).
*/
close: () => Promise<void>;
}
declare function isLoopbackHostname(hostname: string): boolean;
/**
* Default origin policy for a localhost dev tool: allow requests with no
* `Origin` header (native, non-browser clients), allow any loopback origin
* (so cross-port localhost dev setups keep working), and allow explicitly
* configured origins. Everything else — a real remote page in the dev's
* browser — is rejected.
*/
declare function isAllowedOrigin(origin: string | undefined, allowedOrigins: readonly string[]): boolean;
/**
* Attach a WebSocket transport to an existing RPC group, powered by
* [crossws](https://crossws.h3.dev). Either attach to an existing HTTP(S)
* `server` (sharing its port, optionally scoped to a `path`), or let this
* helper create a standalone server from `port` / `host` / `https`.
*
* Returns the crossws node adapter plus `detach` (remove the upgrade
* listener from a shared `server`) and `close` (full deterministic
* teardown).
*/
declare function attachWsRpcTransport<ClientFunctions extends object, ServerFunctions extends object>(rpcGroup: BirpcGroup<ClientFunctions, ServerFunctions, false>, options?: WsRpcTransportOptions): WsRpcTransport;
//#endregion
export { WsRpcTransportOptions as a, isAllowedOrigin as c, WsRpcTransport as i, isLoopbackHostname as l, DevframeNodeRpcSessionMeta as n, attachWsRpcTransport as o, WsOriginRegistry as r, createWsOriginRegistry as s, CreateWsOriginRegistryOptions as t };