🎩 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
40
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.7.10
to
0.7.11
+152
dist/cac-Drj5sjnU.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-BdegDQKt.mjs";
import process from "node:process";
import cac from "cac";
import { safeParse } from "valibot";
//#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 probe of a valibot schema to decide whether the
* corresponding CAC option takes a value. Unwraps `optional` / `nullable`
* / `nullish` / `default` / `pipe` wrappers then matches on the inner
* type's kind.
*/
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;
}
return "unknown";
}
/** Whether the CAC option for this schema should be a boolean flag. */
function isBooleanFlag(schema) {
return getSchemaKind(schema) === "boolean";
}
/** Validate and coerce 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 = safeParse(fieldSchema, raw[key]);
if (result.success) flags[key] = result.output;
else issues.push(`--${toKebab(key)}: ${result.issues.map((i) => i.message).join(", ")}`);
}
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(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
});
});
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 { DEVFRAME_CONNECTION_META_FILENAME } from "./constants.mjs";
import { a as diagnostics } from "./storage-D_Xy9v1l.mjs";
import { n as createHostContext, t as createH3DevframeHost } from "./host-h3-SjwRTwkE.mjs";
import { i as normalizeHttpServerUrl, t as startHttpAndWs } from "./server-BFsuZI9e.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-BaCZYhIR.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);
}
});
if (mcpDispose) {
const closeServer = started.close;
started.close = async () => {
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 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, createDevServer as t };
+1
-1

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

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

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

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

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

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

import { d as McpRouteOptions, r as DevframeDefinition, u as DevframeWsOptions } from "../devframe-C18zEiex.mjs";
import { d as McpRouteOptions, p as DevframeAuthHandler, r as DevframeDefinition, u as DevframeWsOptions } from "../devframe-C18zEiex.mjs";
import { n as StartedServer } from "../server-BLaM21gv.mjs";

@@ -54,2 +54,13 @@ import { H3 } from "h3";

/**
* Override how authentication resolves, taking precedence over
* `def.cli?.auth`. Pass `false` to skip the gate entirely (the standard
* choice for a **hosted** deployment where the host manages auth — see
* {@link viteDevBridge}); a {@link DevframeAuthHandler} to install a custom
* scheme; or `true` to force devframe's interactive OTP gate on. When
* omitted, auth resolves from `flags.auth` / `def.cli?.auth` (the standalone
* default: gated). The `--no-auth` flag (`flags.auth === false`) still forces
* the gate off regardless of this option.
*/
auth?: boolean | DevframeAuthHandler;
/**
* Expose a route-based MCP server on the dev server (Streamable-HTTP).

@@ -56,0 +67,0 @@ * Overrides `def.cli?.mcp`; `undefined` falls through to it. `false`

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

import { n as resolveDevServerPort, t as createDevServer } from "../dev-DmdXbLyJ.mjs";
import { n as resolveDevServerPort, t as createDevServer } from "../dev-BdegDQKt.mjs";
export { createDevServer, resolveDevServerPort };

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

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

@@ -33,2 +33,15 @@ interface ViteDevBridgeOptions {

};
/**
* Whether the bridged devframe runs its own auth gate. This is a **hosted**
* adapter — the devframe shares the host app's origin and the host owns
* authentication — so it defaults to `false`: the plugin's own gate never
* fires and its `cli.auth` default is ignored (matching devframe's
* hosted-deployment contract). Pass `true` to force devframe's interactive
* OTP gate on, or a {@link DevframeAuthHandler} to install a custom scheme.
* Only applies in bridge mode (`devMiddleware`); the static-mount mode
* starts no RPC server.
*
* @default false
*/
auth?: boolean | DevframeAuthHandler;
}

@@ -62,2 +75,8 @@ interface DevframeVitePlugin {

*
* As a hosted adapter the bridge defers authentication to the host: its
* side-car RPC server runs with the plugin's own auth gate **off** by
* default (ignoring `def.cli?.auth`), so a plugin mounted this way never
* triggers its standalone OTP prompt. Opt back in per-mount with
* `options.auth` (`true` for devframe's interactive gate, or a handler).
*
* Use bridge mode when integrating with frameworks that own the SPA

@@ -64,0 +83,0 @@ * (Nuxt, Astro, SolidStart, plain Vite apps). For the all-in-one

@@ -5,3 +5,3 @@ import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_WS_ROUTE } from "../constants.mjs";

import { serveStaticNodeMiddleware } from "../utils/serve-static.mjs";
import { n as resolveDevServerPort, t as createDevServer } from "../dev-DmdXbLyJ.mjs";
import { n as resolveDevServerPort, t as createDevServer } from "../dev-BdegDQKt.mjs";
import { resolve } from "pathe";

@@ -23,2 +23,8 @@ //#region src/helpers/vite.ts

*
* As a hosted adapter the bridge defers authentication to the host: its
* side-car RPC server runs with the plugin's own auth gate **off** by
* default (ignoring `def.cli?.auth`), so a plugin mounted this way never
* triggers its standalone OTP prompt. Opt back in per-mount with
* `options.auth` (`true` for devframe's interactive gate, or a handler).
*
* Use bridge mode when integrating with frameworks that own the SPA

@@ -56,3 +62,4 @@ * (Nuxt, Astro, SolidStart, plain Vite apps). For the all-in-one

flags: mw.flags,
openBrowser: false
openBrowser: false,
auth: options.auth ?? false
});

@@ -59,0 +66,0 @@ } catch (e) {

{
"name": "devframe",
"type": "module",
"version": "0.7.10",
"version": "0.7.11",
"description": "Framework for building generic devframes",

@@ -6,0 +6,0 @@ "author": "Anthony Fu <anthonyfu117@hotmail.com>",

import { n as colors } from "./diagnostics-reporter-CsIG85Q5.mjs";
import { createBuild } from "./adapters/build.mjs";
import { n as resolveDevServerPort, t as createDevServer } from "./dev-DmdXbLyJ.mjs";
import process from "node:process";
import cac from "cac";
import { safeParse } from "valibot";
//#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 probe of a valibot schema to decide whether the
* corresponding CAC option takes a value. Unwraps `optional` / `nullable`
* / `nullish` / `default` / `pipe` wrappers then matches on the inner
* type's kind.
*/
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;
}
return "unknown";
}
/** Whether the CAC option for this schema should be a boolean flag. */
function isBooleanFlag(schema) {
return getSchemaKind(schema) === "boolean";
}
/** Validate and coerce 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 = safeParse(fieldSchema, raw[key]);
if (result.success) flags[key] = result.output;
else issues.push(`--${toKebab(key)}: ${result.issues.map((i) => i.message).join(", ")}`);
}
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(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
});
});
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 { DEVFRAME_CONNECTION_META_FILENAME } from "./constants.mjs";
import { a as diagnostics } from "./storage-D_Xy9v1l.mjs";
import { n as createHostContext, t as createH3DevframeHost } from "./host-h3-SjwRTwkE.mjs";
import { i as normalizeHttpServerUrl, t as startHttpAndWs } from "./server-BFsuZI9e.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-BaCZYhIR.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 : 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);
}
});
if (mcpDispose) {
const closeServer = started.close;
started.close = async () => {
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 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, createDevServer as t };