diff --git a/README.md b/README.md index 80d596d..1c4cbb9 100644 --- a/README.md +++ b/README.md @@ -6,63 +6,21 @@

Devframe

-[![npm version][npm-version-src]][npm-version-href] -[![npm downloads][npm-downloads-src]][npm-downloads-href] -[![bundle][bundle-src]][bundle-href] -[![JSDocs][jsdocs-src]][jsdocs-href] -[![License][license-src]][license-href] - -Framework-neutral foundation for building generic DevTools. Describe one devframe — its RPC, its data, its SPA, its CLI shape — and deploy the same definition through any of seven adapters. - -Documentation: [https://devfra.me/](https://devfra.me/). - -## Install - -```sh -pnpm add devframe -``` - -## Hello, Devframe - -```ts -import { defineDevframe, defineRpcFunction } from 'devframe' -import { createCli } from 'devframe/adapters/cli' - -const devframe = defineDevframe({ - id: 'my-devframe', - name: 'My Devframe', - setup(ctx) { - ctx.rpc.register(defineRpcFunction({ - name: 'my-devframe:hello', - type: 'static', - jsonSerializable: true, - handler: () => ({ message: 'hello' }), - })) - }, -}) - -await createCli(devframe).parse() -``` - -## Adapters - -| Adapter | Use case | -|---------|----------| -| `cli` | Standalone CLI tool with `dev` / `build` / `mcp` subcommands. | -| `build` | Generates a static, self-contained SPA snapshot. | -| `vite` | Mounts the devframe into Vite DevTools (or any compatible host) via `@vitejs/devtools-kit`. | -| `embedded` | Overlays inside another devframe's UI. | -| `mcp` | Surfaces the devframe's RPC to coding agents over MCP. | +

+npm version +npm downloads +bundle +JSDocs +License +

-## Repo layout +

+Framework-neutral foundation for building generic DevTools. +

-| Path | Description | -|------|-------------| -| [`packages/devframe`](./packages/devframe) | The published [`devframe`](https://www.npmjs.com/package/devframe) npm package. | -| [`packages/nuxt`](./packages/nuxt) | The [`@devframes/nuxt`](https://www.npmjs.com/package/@devframes/nuxt) Nuxt module adapter. | -| [`docs`](./docs) | VitePress documentation site, deployed at https://devfra.me/. | -| [`examples`](./examples) | End-to-end demos: [`devframe-counter`](./examples/devframe-counter), [`devframe-files-inspector`](./examples/devframe-files-inspector), and [`devframe-streaming-chat`](./examples/devframe-streaming-chat). | -| [`tests`](./tests) | Public-API snapshot tests via [`tsnapi`](https://github.com/posva/tsnapi). | +

+Documentation: https://devfra.me/ +

## Sponsors @@ -75,16 +33,3 @@ await createCli(devframe).parse() ## License [MIT](./LICENSE.md) License © [Anthony Fu](https://github.com/antfu) - - - -[npm-version-src]: https://img.shields.io/npm/v/devframe?style=flat&colorA=080f12&colorB=517158 -[npm-version-href]: https://npmx.dev/package/devframe -[npm-downloads-src]: https://img.shields.io/npm/dm/devframe?style=flat&colorA=080f12&colorB=517158 -[npm-downloads-href]: https://npmx.dev/package/devframe -[bundle-src]: https://img.shields.io/bundlephobia/minzip/devframe?style=flat&colorA=080f12&colorB=517158&label=minzip -[bundle-href]: https://bundlephobia.com/result?p=devframe -[license-src]: https://img.shields.io/github/license/devframes/devframe.svg?style=flat&colorA=080f12&colorB=517158 -[license-href]: https://github.com/devframes/devframe/blob/main/LICENSE.md -[jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=517158 -[jsdocs-href]: https://www.jsdocs.io/package/devframe diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 0cf177b..23a0f11 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -3,9 +3,13 @@ import { fileURLToPath } from 'node:url' import { globSync } from 'tinyglobby' import { defineConfig } from 'vitepress' import { withMermaid } from 'vitepress-plugin-mermaid' +import pkg from '../../packages/devframe/package.json' with { type: 'json' } const errorsDir = fileURLToPath(new URL('../errors/', import.meta.url)) +const repo = 'https://github.com/devframes/devframe' +const brandColor = '#517158' + function listErrorCodes(prefix: string): string[] { return globSync(`${prefix}*.md`, { cwd: errorsDir }) .map(f => f.replace(/\.md$/, '')) @@ -16,26 +20,53 @@ function guideItems(prefix: string): DefaultTheme.NavItemWithLink[] { return [ { text: 'Introduction', link: `${prefix}/guide/` }, { text: 'Devframe Definition', link: `${prefix}/guide/devframe-definition` }, - { text: 'Adapters', link: `${prefix}/guide/adapters` }, { text: 'RPC', link: `${prefix}/guide/rpc` }, { text: 'Shared State', link: `${prefix}/guide/shared-state` }, { text: 'Streaming', link: `${prefix}/guide/streaming` }, { text: 'When Clauses', link: `${prefix}/guide/when-clauses` }, { text: 'Structured Diagnostics', link: `${prefix}/guide/diagnostics` }, - { text: 'Utilities', link: `${prefix}/guide/utilities` }, { text: 'Client', link: `${prefix}/guide/client` }, { text: 'Standalone CLI', link: `${prefix}/guide/standalone-cli` }, - { text: 'Nuxt Helper', link: `${prefix}/guide/nuxt` }, { text: 'Agent-Native (experimental)', link: `${prefix}/guide/agent-native` }, ] } +function adaptersItems(prefix: string): DefaultTheme.NavItemWithLink[] { + return [ + { text: 'Overview', link: `${prefix}/adapters/` }, + { text: 'CLI', link: `${prefix}/adapters/cli` }, + { text: 'Dev', link: `${prefix}/adapters/dev` }, + { text: 'Build', link: `${prefix}/adapters/build` }, + { text: 'Vite', link: `${prefix}/adapters/vite` }, + { text: 'Embedded', link: `${prefix}/adapters/embedded` }, + { text: 'MCP', link: `${prefix}/adapters/mcp` }, + ] +} + +function helpersItems(prefix: string): DefaultTheme.NavItemWithLink[] { + return [ + { text: 'Overview', link: `${prefix}/helpers/` }, + { text: 'Utilities', link: `${prefix}/helpers/utilities` }, + { text: 'Vite Bridge', link: `${prefix}/helpers/vite-bridge` }, + { text: 'Nuxt Module', link: `${prefix}/helpers/nuxt` }, + { text: 'Open Helpers', link: `${prefix}/helpers/open-helpers` }, + ] +} + export function devframeSidebar(prefix = ''): DefaultTheme.SidebarItem[] { return [ { text: 'Guide', items: guideItems(prefix), }, + { + text: 'Adapters', + items: adaptersItems(prefix), + }, + { + text: 'Helpers', + items: helpersItems(prefix), + }, { text: 'Error Reference', link: `${prefix}/errors/`, @@ -48,10 +79,19 @@ export function devframeSidebar(prefix = ''): DefaultTheme.SidebarItem[] { ] } -export function devframeNav(prefix = ''): DefaultTheme.NavItemWithLink[] { +export function devframeNav(prefix = ''): DefaultTheme.NavItem[] { return [ - ...guideItems(prefix), - { text: 'Error Reference', link: `${prefix}/errors/` }, + { text: 'Guide', items: guideItems(prefix) }, + { text: 'Adapters', items: adaptersItems(prefix) }, + { text: 'Helpers', items: helpersItems(prefix) }, + { text: 'Errors', link: `${prefix}/errors/` }, + { + text: `v${pkg.version}`, + items: [ + { text: 'Release Notes', link: `${repo}/releases` }, + { text: 'Contributing', link: `${repo}/blob/main/CONTRIBUTING.md` }, + ], + }, ] } @@ -60,22 +100,22 @@ export default withMermaid(defineConfig({ description: 'Framework-neutral foundation for building generic DevTools — RPC layer, hosts, and adapters.', head: [ ['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }], + ['link', { rel: 'apple-touch-icon', href: '/logo.svg' }], + ['link', { rel: 'mask-icon', href: '/logo.svg', color: brandColor }], + ['meta', { name: 'theme-color', content: brandColor }], ], themeConfig: { logo: { light: '/logo.svg', dark: '/logo.svg' }, - nav: [ - { text: 'Guide', items: guideItems('') }, - { text: 'Error Reference', link: '/errors/' }, - ], + nav: devframeNav(), sidebar: devframeSidebar(), search: { provider: 'local', }, socialLinks: [ - { icon: 'github', link: 'https://github.com/devframes/devframe' }, + { icon: 'github', link: repo }, ], editLink: { - pattern: 'https://github.com/devframes/devframe/edit/main/docs/:path', + pattern: `${repo}/edit/main/docs/:path`, text: 'Suggest changes to this page', }, footer: { diff --git a/docs/adapters/build.md b/docs/adapters/build.md new file mode 100644 index 0000000..0e63cca --- /dev/null +++ b/docs/adapters/build.md @@ -0,0 +1,41 @@ +--- +outline: deep +--- + +# Build + +Produces a self-contained static deploy of a devframe: + +1. Copies the author's SPA dist (`cli.distDir` or `options.distDir`) into ``. +2. Runs `setup(ctx)` with `mode: 'build'`. +3. Collects RPC dumps for every `'static'` function and any `'query'` function with `dump.inputs` / `snapshot: true`. +4. Writes `/__connection.json` (`{ backend: 'static' }`) and sharded dump files under `/__rpc-dump/` — both at the SPA root so the deployed client discovers them via relative paths from `document.baseURI`. +5. When `def.spa` is set, also writes `/spa-loader.json` describing how the SPA hydrates its data. + +```ts +import { createBuild } from 'devframe/adapters/build' +import devframe from './devframe' + +await createBuild(devframe, { + outDir: 'dist-static', + base: '/', +}) +``` + +| Option | Default | Description | +|--------|---------|-------------| +| `outDir` | `dist-static` | Output directory. Cleared on each build. | +| `base` | `/` | Absolute URL base the output is served from. | +| `distDir` | `def.cli?.distDir` | Override the SPA dist directory. | + +The resulting directory hosts on any static web server (`serve`, nginx, GitHub Pages, …). The client auto-detects `static` mode by resolving `./__connection.json` against `document.baseURI` and runs in read-only form. + +`createBuild` copies the SPA verbatim, so deploying under a custom URL base just means building the SPA with relative asset paths (`vite.base: './'`) — the client discovers the effective base at runtime. + +When `def.spa` is set on the definition, `createBuild` also writes `spa-loader.json` next to `index.html` describing how the deployed SPA sources its data: + +- `'none'` — use the baked RPC dump only (read-only static view). +- `'query'` — hydrate from URL search params. +- `'upload'` — accept a drag-and-drop file. + +Deployed SPAs that use `setupBrowser` ship their own client entry that registers the handlers. diff --git a/docs/adapters/cli.md b/docs/adapters/cli.md new file mode 100644 index 0000000..e320d90 --- /dev/null +++ b/docs/adapters/cli.md @@ -0,0 +1,110 @@ +--- +outline: deep +--- + +# CLI + +The CLI adapter wraps a `DevframeDefinition` in a `cac`-powered command-line interface. From one entry it spins up an `h3` dev server with WebSocket RPC, builds static snapshots, builds SPA bundles, or starts an MCP server. + +```ts +import { defineDevframe } from 'devframe' +import { createCli } from 'devframe/adapters/cli' + +const devframe = defineDevframe({ + id: 'my-devframe', + name: 'My Devframe', + cli: { distDir: './client/dist' }, + setup(ctx) { /* register docks, RPC, etc. */ }, +}) + +await createCli(devframe).parse() +``` + +Running the resulting binary: + +```sh +my-devframe # dev server at http://localhost:9999/ +my-devframe --port 8080 +my-devframe build --out-dir dist-static +my-devframe build --out-dir dist-static --base /devtools/ +my-devframe mcp # stdio MCP server (experimental) +``` + +Standalone CLI serves the SPA at `/` by default. The `/__devtools/` prefix is for *hosted* adapters where devframe mounts alongside an existing app — see [Mount paths](./#mount-paths). + +## Options + +`createCli(def, options?)` accepts: + +| Option | Default | Description | +|--------|---------|-------------| +| `defaultPort` | `9999` (or `def.cli?.port`) | Port used by the dev command when `--port` isn't provided. | +| `configureCli` | — | `(cli: CAC) => void` — final hook to add commands/flags at the assembly stage, after the definition's `cli.configure` runs. | +| `onReady` | — | `(info: { origin, port, app }) => void \| Promise` — called once the dev server is listening. Use this to print your own startup banner. | + +`createCli` returns a `CliHandle`: + +```ts +interface CliHandle { + cli: CAC // raw cac instance — mutate before calling parse() + parse: (argv?: string[]) => Promise +} +``` + +The `cli` property lets the caller add ad-hoc commands and flags right before `parse()` when a `configureCli` callback is inconvenient. + +## Definition-level `cli` fields + +```ts +defineDevframe({ + id: 'my-devframe', + cli: { + command: 'my-devframe', // binary name; default: the id + distDir: './client/dist', // required for dev/build/spa + port: 7777, // preferred port + portRange: [7777, 9000], // passed through to get-port-please + random: false, // passed through to get-port-please + host: '127.0.0.1', // default host; --host overrides + open: true, // auto-open the browser on dev start + auth: false, // skip the trust handshake (single-user localhost) + configure(cli) { // contribute capability flags/commands + cli.option('--config ', 'Custom config file') + .option('--no-files', 'Skip file matching') + }, + }, + setup(ctx, { flags }) { + // `flags` is the parsed cac flag bag — includes both devframe's + // built-ins (`--port`, `--host`, `--open`) and anything declared in + // `cli.configure` or `configureCli`. + }, +}) +``` + +`distDir` is the only required field; everything else has sensible defaults. The `configure` hook runs *before* the `configureCli` option passed to `createCli`, so the final tool author always has the last word on flags. + +## Headless logging + +Devframe leaves startup output to the application. Wire `onReady` to print your own banner: + +```ts +await createCli(devframe, { + onReady({ origin }) { + console.log(`ESLint Config Inspector ready at ${origin}`) + }, +}).parse() +``` + +Structured diagnostics (via `logs-sdk`) continue to surface through their normal reporters. + +## Use your own CLI framework + +To integrate devframe into an existing commander / yargs program — or to expose a different command structure than `createCli`'s `dev` / `build` / `mcp` triplet — drop down to the peer factories. Same `DevframeDefinition`, different shell: + +| Building block | Entry | Purpose | +|----------------|-------|---------| +| [`createDevServer(def, opts?)`](./dev) | `devframe/adapters/dev` | h3 + WebSocket RPC + SPA mount | +| [`createBuild(def, opts?)`](./build) | `devframe/adapters/build` | Static deploy | +| [`createMcpServer(def, opts?)`](./mcp) | `devframe/adapters/mcp` | stdio MCP server | +| `parseCliFlags(schema, raw)` | `devframe/adapters/cli` | Validate a flag bag against a `CliFlagsSchema` | + +See the [Standalone CLI guide](/guide/standalone-cli#use-your-own-cli-framework) for a worked commander example. diff --git a/docs/adapters/dev.md b/docs/adapters/dev.md new file mode 100644 index 0000000..12124dc --- /dev/null +++ b/docs/adapters/dev.md @@ -0,0 +1,49 @@ +--- +outline: deep +--- + +# Dev + +The `dev` adapter is the building block `createCli` uses internally — h3 + WebSocket RPC + the author's SPA mounted at the resolved base path. Reach for it directly to mount the dev server inside an existing CLI program (commander, yargs, hand-rolled CAC) or to attach custom middleware to the underlying h3 app. + +```ts +import { createDevServer } from 'devframe/adapters/dev' +import devframe from './devframe' + +const handle = await createDevServer(devframe, { + port: 7777, + onReady: ({ origin }) => console.log(`Ready at ${origin}`), +}) + +// graceful shutdown — SIGINT, hot reload, test teardown +process.on('SIGINT', () => handle.close().then(() => process.exit(0))) +``` + +`createDevServer` returns the underlying `StartedServer` (origin, port, h3 app, WS server, RPC group, `close()`) so callers can integrate it into their own process lifecycle. + +| Option | Default | Description | +|--------|---------|-------------| +| `host` | `def.cli?.host ?? 'localhost'` | Bind host. | +| `port` | resolved via `resolveDevServerPort` | Port to listen on. | +| `flags` | `{}` | Parsed flag bag forwarded to `setup(ctx, { flags })`. | +| `distDir` | `def.cli?.distDir` | Required — throws when neither is set. | +| `basePath` | `resolveBasePath(def, 'standalone')` | Mount path override. | +| `app` | fresh h3 app | Pre-configured h3 app to mount onto (custom middleware, auth, extra static assets). | +| `openBrowser` | resolves from `flags.open` / `def.cli?.open` | Explicit on/off override. `false` disables; a string opens that relative path. | +| `onReady` | — | Callback when the WS server is bound. | + +## Port resolution + +`resolveDevServerPort(def, opts?)` resolves a port up-front (to print or log it) before the server starts: + +```ts +import { resolveDevServerPort } from 'devframe/adapters/dev' + +const port = await resolveDevServerPort(devframe, { host: '127.0.0.1' }) +// honors def.cli?.port / portRange / random +``` + +| Option | Default | Description | +|--------|---------|-------------| +| `host` | `def.cli?.host ?? 'localhost'` | Bind host (passed to `get-port-please` for in-use detection). | +| `defaultPort` | `def.cli?.port ?? 9999` | Override the preferred port. | diff --git a/docs/adapters/embedded.md b/docs/adapters/embedded.md new file mode 100644 index 0000000..5d2ae0b --- /dev/null +++ b/docs/adapters/embedded.md @@ -0,0 +1,20 @@ +--- +outline: deep +--- + +# Embedded + +Register a devframe into an already-running context at runtime. Mirrors the [`vite`](./vite) adapter's plugin-scan, but for callers that need dynamic, post-startup registration. The host decides the mount path; `embedded` is a hosted adapter and inherits the `/__/` default when one is needed. + +```ts +import { createEmbedded } from 'devframe/adapters/embedded' +import devframe from './devframe' + +await createEmbedded(devframe, { ctx: existingCtx }) +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `ctx` | ✓ | Target `DevToolsNodeContext` the devframe is registered into. | + +Useful when a host loads devframes based on runtime conditions (feature flags, user opt-in, dynamic discovery) rather than static config. diff --git a/docs/adapters/index.md b/docs/adapters/index.md new file mode 100644 index 0000000..958c4a5 --- /dev/null +++ b/docs/adapters/index.md @@ -0,0 +1,41 @@ +--- +outline: deep +--- + +# Adapters + +An adapter takes a `DevframeDefinition` and deploys it into a specific runtime — a standalone CLI, a Vite plugin, a static snapshot, an embedded host, or an MCP server. Each adapter ships at its own entry point (`devframe/adapters/`); the bundler pulls in only the ones you use. + +Every adapter factory has the shape `createXxx(devframeDef, options?)`. + +## Comparison + +| Adapter | Entry | Factory | Best for | +|---------|-------|---------|----------| +| [`cli`](./cli) | `devframe/adapters/cli` | `createCli(def, options?)` | Standalone tools run via `node ./my-tool.js` | +| [`dev`](./dev) | `devframe/adapters/dev` | `createDevServer(def, options?)` | Run the dev server programmatically — drive it from any CLI framework | +| [`build`](./build) | `devframe/adapters/build` | `createBuild(def, options?)` | Offline reports, CI artifacts, deployable SPA snapshots | +| [`vite`](./vite) | `@vitejs/devtools-kit/node` | `createPluginFromDevframe(def, options?)` | Mount the definition into Vite DevTools (or any compatible host) | +| [`embedded`](./embedded) | `devframe/adapters/embedded` | `createEmbedded(def, { ctx })` | Runtime registration into an already-running host | +| [`mcp`](./mcp) | `devframe/adapters/mcp` | `createMcpServer(def, options?)` | Exposing a devframe to coding agents | + +## Mount paths + +A devframe's SPA basePath depends on which adapter is running it: + +| Adapter kind | Default basePath | Reason | +|--------------|------------------|--------| +| `cli`, `spa`, `build` (standalone) | `/` | The devframe owns the origin. | +| `vite`, `embedded` (hosted) | `/__/` | The devframe shares the origin with a host app and namespaces itself. | + +Override either side explicitly with `DevframeDefinition.basePath`: + +```ts +defineDevframe({ + id: 'my-devframe', + basePath: '/devframes/', // force this base regardless of adapter + setup(ctx) { /* … */ }, +}) +``` + +SPA authors should build with relative asset paths (`vite.base: './'`); the client resolves its connection descriptor relative to the page at runtime. See [Client](/guide/client#runtime-basepath-discovery) for the discovery rules. diff --git a/docs/adapters/mcp.md b/docs/adapters/mcp.md new file mode 100644 index 0000000..212112c --- /dev/null +++ b/docs/adapters/mcp.md @@ -0,0 +1,21 @@ +--- +outline: deep +--- + +# MCP + +> [!WARNING] Experimental +> The agent-native surface is experimental and may change without a major version bump. + +Translates a devframe's agent host into a [Model Context Protocol](https://modelcontextprotocol.io) server so coding agents (Claude Desktop, Cursor, Zed, Claude Code) can call flagged RPCs and read exposed resources. + +```ts +import { createMcpServer } from 'devframe/adapters/mcp' +import devframe from './devframe' + +await createMcpServer(devframe, { transport: 'stdio' }) +``` + +`@modelcontextprotocol/sdk` is a peer dependency — install it when shipping MCP support. The current transport is `stdio`. + +See the [Agent-Native](/guide/agent-native) page for the full API, safety model, and Claude Desktop integration example. diff --git a/docs/adapters/vite.md b/docs/adapters/vite.md new file mode 100644 index 0000000..504ec04 --- /dev/null +++ b/docs/adapters/vite.md @@ -0,0 +1,25 @@ +--- +outline: deep +--- + +# Vite + +The Vite-DevTools adapter — wraps a `DevframeDefinition` so Vite DevTools' kit plugin-scan picks it up. The factory lives in `@vitejs/devtools-kit/node` so devframe itself stays free of any Vite or `@vitejs/*` dependency. The pattern (`definition → host plugin → mount`) is general; other hosts can implement equivalent bridges. + +```ts +import { createPluginFromDevframe } from '@vitejs/devtools-kit/node' +import devframe from './devframe' + +export default function myVitePlugin() { + return createPluginFromDevframe(devframe) +} +``` + +The returned object has the shape `{ name, devtools: { setup, capabilities } }`. Use this adapter when your devframe should live inside the Vite DevTools dock alongside other integrations. The kit synthesises an iframe dock entry from the definition's `id` / `name` / `icon` / `basePath`; for richer host-side behaviour (extra terminals, commands, dock overrides) pass `options.setup`. See the [DevTools Kit → DevTools Plugin](https://devtools.vite.dev/kit/devtools-plugin) page for the Vite-specific guide. + +| Option | Default | Description | +|--------|---------|-------------| +| `name` | `devframe:` | Override the Vite plugin name. | +| `base` | `def.basePath ?? /.${id}/` | Mount path override. | +| `dock` | `{}` | Overrides for the synthesized iframe dock entry (category, icon, when). | +| `setup` | — | Additional host-only setup hook; receives the kit-augmented context (Vite DevTools' `docks`, `terminals`, `messages`, `commands`). | diff --git a/docs/guide/adapters.md b/docs/guide/adapters.md deleted file mode 100644 index 6d637a5..0000000 --- a/docs/guide/adapters.md +++ /dev/null @@ -1,315 +0,0 @@ ---- -outline: deep ---- - -# Adapters - -An adapter takes a `DevframeDefinition` and deploys it into a specific runtime — a standalone CLI, a Vite plugin, a static snapshot, an SPA, a Kit plugin, an embedded host, or an MCP server. Each adapter ships at its own entry point (`devframe/adapters/`); the bundler pulls in only the ones you use. - -Every adapter factory has the shape `createXxx(devframeDef, options?)`. - -## Comparison - -| Adapter | Entry | Factory | Best for | -|---------|-------|---------|----------| -| [`cli`](#cli) | `devframe/adapters/cli` | `createCli(def, options?)` | Standalone tools run via `node ./my-tool.js` | -| [`dev`](#dev) | `devframe/adapters/dev` | `createDevServer(def, options?)` | Run the dev server programmatically — drive it from any CLI framework | -| [`build`](#build) | `devframe/adapters/build` | `createBuild(def, options?)` | Offline reports, CI artifacts, deployable SPA snapshots | -| [`vite`](#vite) | `@vitejs/devtools-kit/node` | `createPluginFromDevframe(def, options?)` | Mount the definition into Vite DevTools (or any compatible host) | -| [`embedded`](#embedded) | `devframe/adapters/embedded` | `createEmbedded(def, { ctx })` | Runtime registration into an already-running host | -| [`mcp`](#mcp) | `devframe/adapters/mcp` | `createMcpServer(def, options?)` | Exposing a devframe to coding agents | - -## CLI - -The CLI adapter wraps a `DevframeDefinition` in a `cac`-powered command-line interface. From one entry it spins up an `h3` dev server with WebSocket RPC, builds static snapshots, builds SPA bundles, or starts an MCP server. - -```ts -import { defineDevframe } from 'devframe' -import { createCli } from 'devframe/adapters/cli' - -const devframe = defineDevframe({ - id: 'my-devframe', - name: 'My Devframe', - cli: { distDir: './client/dist' }, - setup(ctx) { /* register docks, RPC, etc. */ }, -}) - -await createCli(devframe).parse() -``` - -Running the resulting binary: - -```sh -my-devframe # dev server at http://localhost:9999/ -my-devframe --port 8080 -my-devframe build --out-dir dist-static -my-devframe build --out-dir dist-static --base /devtools/ -my-devframe mcp # stdio MCP server (experimental) -``` - -Standalone CLI serves the SPA at `/` by default. The `/__devtools/` prefix is for *hosted* adapters where devframe mounts alongside an existing app — see [Mount paths](#mount-paths). - -### Options - -`createCli(def, options?)` accepts: - -| Option | Default | Description | -|--------|---------|-------------| -| `defaultPort` | `9999` (or `def.cli?.port`) | Port used by the dev command when `--port` isn't provided. | -| `configureCli` | — | `(cli: CAC) => void` — final hook to add commands/flags at the assembly stage, after the definition's `cli.configure` runs. | -| `onReady` | — | `(info: { origin, port, app }) => void \| Promise` — called once the dev server is listening. Use this to print your own startup banner. | - -`createCli` returns a `CliHandle`: - -```ts -interface CliHandle { - cli: CAC // raw cac instance — mutate before calling parse() - parse: (argv?: string[]) => Promise -} -``` - -The `cli` property lets the caller add ad-hoc commands and flags right before `parse()` when a `configureCli` callback is inconvenient. - -### Definition-level `cli` fields - -```ts -defineDevframe({ - id: 'my-devframe', - cli: { - command: 'my-devframe', // binary name; default: the id - distDir: './client/dist', // required for dev/build/spa - port: 7777, // preferred port - portRange: [7777, 9000], // passed through to get-port-please - random: false, // passed through to get-port-please - host: '127.0.0.1', // default host; --host overrides - open: true, // auto-open the browser on dev start - auth: false, // skip the trust handshake (single-user localhost) - configure(cli) { // contribute capability flags/commands - cli.option('--config ', 'Custom config file') - .option('--no-files', 'Skip file matching') - }, - }, - setup(ctx, { flags }) { - // `flags` is the parsed cac flag bag — includes both devframe's - // built-ins (`--port`, `--host`, `--open`) and anything declared in - // `cli.configure` or `configureCli`. - }, -}) -``` - -`distDir` is the only required field; everything else has sensible defaults. The `configure` hook runs *before* the `configureCli` option passed to `createCli`, so the final tool author always has the last word on flags. - -### Headless logging - -Devframe leaves startup output to the application. Wire `onReady` to print your own banner: - -```ts -await createCli(devframe, { - onReady({ origin }) { - console.log(`ESLint Config Inspector ready at ${origin}`) - }, -}).parse() -``` - -Structured diagnostics (via `logs-sdk`) continue to surface through their normal reporters. - -### Use your own CLI framework - -To integrate devframe into an existing commander / yargs program — or to expose a different command structure than `createCli`'s `dev` / `build` / `mcp` triplet — drop down to the peer factories. Same `DevframeDefinition`, different shell: - -| Building block | Entry | Purpose | -|----------------|-------|---------| -| [`createDevServer(def, opts?)`](#dev) | `devframe/adapters/dev` | h3 + WebSocket RPC + SPA mount | -| [`createBuild(def, opts?)`](#build) | `devframe/adapters/build` | Static deploy | -| [`createMcpServer(def, opts?)`](#mcp) | `devframe/adapters/mcp` | stdio MCP server | -| `parseCliFlags(schema, raw)` | `devframe/adapters/cli` | Validate a flag bag against a `CliFlagsSchema` | - -See the [Standalone CLI guide](./standalone-cli#use-your-own-cli-framework) for a worked commander example. - -## Dev - -The `dev` adapter is the building block `createCli` uses internally — h3 + WebSocket RPC + the author's SPA mounted at the resolved base path. Reach for it directly to mount the dev server inside an existing CLI program (commander, yargs, hand-rolled CAC) or to attach custom middleware to the underlying h3 app. - -```ts -import { createDevServer } from 'devframe/adapters/dev' -import devframe from './devframe' - -const handle = await createDevServer(devframe, { - port: 7777, - onReady: ({ origin }) => console.log(`Ready at ${origin}`), -}) - -// graceful shutdown — SIGINT, hot reload, test teardown -process.on('SIGINT', () => handle.close().then(() => process.exit(0))) -``` - -`createDevServer` returns the underlying `StartedServer` (origin, port, h3 app, WS server, RPC group, `close()`) so callers can integrate it into their own process lifecycle. - -| Option | Default | Description | -|--------|---------|-------------| -| `host` | `def.cli?.host ?? 'localhost'` | Bind host. | -| `port` | resolved via `resolveDevServerPort` | Port to listen on. | -| `flags` | `{}` | Parsed flag bag forwarded to `setup(ctx, { flags })`. | -| `distDir` | `def.cli?.distDir` | Required — throws when neither is set. | -| `basePath` | `resolveBasePath(def, 'standalone')` | Mount path override. | -| `app` | fresh h3 app | Pre-configured h3 app to mount onto (custom middleware, auth, extra static assets). | -| `openBrowser` | resolves from `flags.open` / `def.cli?.open` | Explicit on/off override. `false` disables; a string opens that relative path. | -| `onReady` | — | Callback when the WS server is bound. | - -### Port resolution - -`resolveDevServerPort(def, opts?)` resolves a port up-front (to print or log it) before the server starts: - -```ts -import { resolveDevServerPort } from 'devframe/adapters/dev' - -const port = await resolveDevServerPort(devframe, { host: '127.0.0.1' }) -// honors def.cli?.port / portRange / random -``` - -| Option | Default | Description | -|--------|---------|-------------| -| `host` | `def.cli?.host ?? 'localhost'` | Bind host (passed to `get-port-please` for in-use detection). | -| `defaultPort` | `def.cli?.port ?? 9999` | Override the preferred port. | - -## Mount paths - -A devframe's SPA basePath depends on which adapter is running it: - -| Adapter kind | Default basePath | Reason | -|--------------|------------------|--------| -| `cli`, `spa`, `build` (standalone) | `/` | The devframe owns the origin. | -| `vite`, `embedded` (hosted) | `/__/` | The devframe shares the origin with a host app and namespaces itself. | - -Override either side explicitly with `DevframeDefinition.basePath`: - -```ts -defineDevframe({ - id: 'my-devframe', - basePath: '/devframes/', // force this base regardless of adapter - setup(ctx) { /* … */ }, -}) -``` - -SPA authors should build with relative asset paths (`vite.base: './'`); the client resolves its connection descriptor relative to the page at runtime. See [Client](./client#runtime-basepath-discovery) for the discovery rules. - -## Build - -Produces a self-contained static deploy of a devframe: - -1. Copies the author's SPA dist (`cli.distDir` or `options.distDir`) into ``. -2. Runs `setup(ctx)` with `mode: 'build'`. -3. Collects RPC dumps for every `'static'` function and any `'query'` function with `dump.inputs` / `snapshot: true`. -4. Writes `/__connection.json` (`{ backend: 'static' }`) and sharded dump files under `/__rpc-dump/` — both at the SPA root so the deployed client discovers them via relative paths from `document.baseURI`. -5. When `def.spa` is set, also writes `/spa-loader.json` describing how the SPA hydrates its data. - -```ts -import { createBuild } from 'devframe/adapters/build' -import devframe from './devframe' - -await createBuild(devframe, { - outDir: 'dist-static', - base: '/', -}) -``` - -| Option | Default | Description | -|--------|---------|-------------| -| `outDir` | `dist-static` | Output directory. Cleared on each build. | -| `base` | `/` | Absolute URL base the output is served from. | -| `distDir` | `def.cli?.distDir` | Override the SPA dist directory. | - -The resulting directory hosts on any static web server (`serve`, nginx, GitHub Pages, …). The client auto-detects `static` mode by resolving `./__connection.json` against `document.baseURI` and runs in read-only form. - -`createBuild` copies the SPA verbatim, so deploying under a custom URL base just means building the SPA with relative asset paths (`vite.base: './'`) — the client discovers the effective base at runtime. - -When `def.spa` is set on the definition, `createBuild` also writes `spa-loader.json` next to `index.html` describing how the deployed SPA sources its data: - -- `'none'` — use the baked RPC dump only (read-only static view). -- `'query'` — hydrate from URL search params. -- `'upload'` — accept a drag-and-drop file. - -Deployed SPAs that use `setupBrowser` ship their own client entry that registers the handlers. - -## Vite - -The Vite-DevTools adapter — wraps a `DevframeDefinition` so Vite DevTools' kit plugin-scan picks it up. The factory lives in `@vitejs/devtools-kit/node` so devframe itself stays free of any Vite or `@vitejs/*` dependency. The pattern (`definition → host plugin → mount`) is general; other hosts can implement equivalent bridges. - -```ts -import { createPluginFromDevframe } from '@vitejs/devtools-kit/node' -import devframe from './devframe' - -export default function myVitePlugin() { - return createPluginFromDevframe(devframe) -} -``` - -The returned object has the shape `{ name, devtools: { setup, capabilities } }`. Use this adapter when your devframe should live inside the Vite DevTools dock alongside other integrations. The kit synthesises an iframe dock entry from the definition's `id` / `name` / `icon` / `basePath`; for richer host-side behaviour (extra terminals, commands, dock overrides) pass `options.setup`. See the [DevTools Kit → DevTools Plugin](https://devtools.vite.dev/kit/devtools-plugin) page for the Vite-specific guide. - -| Option | Default | Description | -|--------|---------|-------------| -| `name` | `devframe:` | Override the Vite plugin name. | -| `base` | `def.basePath ?? /.${id}/` | Mount path override. | -| `dock` | `{}` | Overrides for the synthesized iframe dock entry (category, icon, when). | -| `setup` | — | Additional host-only setup hook; receives the kit-augmented context (Vite DevTools' `docks`, `terminals`, `messages`, `commands`). | - -## Embedded - -Register a devframe into an already-running context at runtime. Mirrors the `vite` adapter's plugin-scan, but for callers that need dynamic, post-startup registration. The host decides the mount path; `embedded` is a hosted adapter and inherits the `/__/` default when one is needed. - -```ts -import { createEmbedded } from 'devframe/adapters/embedded' -import devframe from './devframe' - -await createEmbedded(devframe, { ctx: existingCtx }) -``` - -| Option | Required | Description | -|--------|----------|-------------| -| `ctx` | ✓ | Target `DevToolsNodeContext` the devframe is registered into. | - -Useful when a host loads devframes based on runtime conditions (feature flags, user opt-in, dynamic discovery) rather than static config. - -## MCP - -> [!WARNING] Experimental -> The agent-native surface is experimental and may change without a major version bump. - -Translates a devframe's agent host into a [Model Context Protocol](https://modelcontextprotocol.io) server so coding agents (Claude Desktop, Cursor, Zed, Claude Code) can call flagged RPCs and read exposed resources. - -```ts -import { createMcpServer } from 'devframe/adapters/mcp' -import devframe from './devframe' - -await createMcpServer(devframe, { transport: 'stdio' }) -``` - -`@modelcontextprotocol/sdk` is a peer dependency — install it when shipping MCP support. The current transport is `stdio`. - -See the [Agent-Native](./agent-native) page for the full API, safety model, and Claude Desktop integration example. - -## Helpers - -These are not adapters — they're small utilities for integrating devframe into specific runtimes outside the adapter list. - -### `devframe/helpers/vite` - -A thin Vite plugin for mounting a devframe inside an existing Vite dev server, used by `@devframes/nuxt` and available for any Vite-based host (Astro, SolidStart, plain Vite apps). Two modes: - -- **Static mount** (default) — mounts `def.cli.distDir` at `options.base` (`/__/` by default). No RPC server. -- **Bridge mode** (`devMiddleware: true | {…}`) — skips the static mount; the host app owns the SPA. Devframe spawns a separate RPC + WS server and registers Vite middleware at `__connection.json` so the host-served SPA can discover the WS endpoint. - -```ts -import { viteDevBridge } from 'devframe/helpers/vite' -import { defineConfig } from 'vite' -import devframe from './devframe' - -export default defineConfig({ - plugins: [viteDevBridge(devframe)], -}) -``` - -| Option | Default | Description | -|--------|---------|-------------| -| `base` | `def.basePath ?? '/__/'` | Mount path inside the Vite dev server. | -| `devMiddleware` | `false` | `true` or `{ port?, host?, flags? }` to enable bridge mode. | diff --git a/docs/guide/devframe-definition.md b/docs/guide/devframe-definition.md index 5459504..27cddc8 100644 --- a/docs/guide/devframe-definition.md +++ b/docs/guide/devframe-definition.md @@ -4,7 +4,7 @@ outline: deep # Devframe Definition -Every Devframe tool starts with a single `defineDevframe` call. The returned `DevframeDefinition` is a portable value that any of the [adapters](./adapters) can consume — the same definition runs under `createCli`, `createBuild`, `createMcpServer`, the `vite` adapter's `createPluginFromDevframe`, and so on. +Every Devframe tool starts with a single `defineDevframe` call. The returned `DevframeDefinition` is a portable value that any of the [adapters](/adapters/) can consume — the same definition runs under `createCli`, `createBuild`, `createMcpServer`, the `vite` adapter's `createPluginFromDevframe`, and so on. ## Minimal definition @@ -28,7 +28,7 @@ export default defineDevframe({ }) ``` -Host adapters (such as the [`vite` adapter](./adapters#vite) for Vite DevTools) derive their mount entry from `id`, `name`, `icon`, and `basePath` automatically. +Host adapters (such as the [`vite` adapter](/adapters/vite) for Vite DevTools) derive their mount entry from `id`, `name`, `icon`, and `basePath` automatically. ## Definition fields @@ -84,7 +84,7 @@ interface DevToolsNodeContext { } ``` -Host adapters can augment `ctx` with additional surfaces. For example, the [`vite` adapter](./adapters#vite) exposes Vite DevTools' dock, command, message, and terminal hosts via an optional `setup` hook on `createPluginFromDevframe` — consult the host's docs for those extras. +Host adapters can augment `ctx` with additional surfaces. For example, the [`vite` adapter](/adapters/vite) exposes Vite DevTools' dock, command, message, and terminal hosts via an optional `setup` hook on `createPluginFromDevframe` — consult the host's docs for those extras. Each devframe-level host has a dedicated page: - [RPC](./rpc) — `ctx.rpc` @@ -165,7 +165,7 @@ defineDevframe({ }) ``` -See [Adapters](./adapters) for how each adapter consumes these. +See [Adapters](/adapters/) for how each adapter consumes these. ## Multiple runtimes, one definition @@ -190,6 +190,6 @@ export const myPlugin = () => createPluginFromDevframe(devframe) ## What's next -- [Adapters](./adapters) — pick a deployment target +- [Adapters](/adapters/) — pick a deployment target - [RPC](./rpc) — register server functions -- [`vite` adapter](./adapters#vite) — mount your devframe into Vite DevTools or another compatible host +- [`vite` adapter](/adapters/vite) — mount your devframe into Vite DevTools or another compatible host diff --git a/docs/guide/index.md b/docs/guide/index.md index d7464d9..791522e 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -6,7 +6,7 @@ outline: deep **Devframe is an asset: define your devtool once, serve it anywhere.** You describe a single tool — its RPC surface, its data model, its SPA, its CLI shape — and the same definition deploys through any of the runtime adapters: a standalone CLI, a self-contained static report, an embedded SPA, an MCP server, and more. Devframe is framework- and build-tool-agnostic — it has no Vite dependency and no opinion on what UI framework your SPA uses. -[Vite DevTools](https://devtools.vite.dev/) is built on top of devframe. If you need an integrated multi-tool host (docks, command palette, terminals, cross-tool toasts), mount your devframe into Vite DevTools via the [`vite` adapter](./adapters#vite) — or build your own host adapter targeting any environment you like. +[Vite DevTools](https://devtools.vite.dev/) is built on top of devframe. If you need an integrated multi-tool host (docks, command palette, terminals, cross-tool toasts), mount your devframe into Vite DevTools via the [`vite` adapter](/adapters/vite) — or build your own host adapter targeting any environment you like. > [!WARNING] Experimental > The Devframe API is still in development and may change between versions. The agent-native surface (`agent` on `defineRpcFunction`, `ctx.agent`, and the MCP adapter) is additionally flagged as experimental. @@ -32,40 +32,10 @@ Devframe keeps its surface focused on one tool, so the same definition stays por | **[Diagnostics](./diagnostics)** | Coded warnings/errors via `logs-sdk` — registered into the host logger so adapters and consumers share the same surface. | | **[Streaming](./streaming)** | One-way (RPC streaming) and two-way (uploads) channel primitives for long-running data. | | **[When Clauses](./when-clauses)** | VS Code-style conditional expressions for docks, commands, and custom UI. | -| **[Utilities](./utilities)** | Bundled helpers under `devframe/utils/*` — terminal colors, hashing, editor launch, structured-clone serialization, and more. | +| **[Utilities](/helpers/utilities)** | Bundled helpers under `devframe/utils/*` — terminal colors, hashing, editor launch, structured-clone serialization, and more. | | **[Client](./client)** | Browser-side RPC client (`connectDevframe`) with auto-auth and WebSocket / static modes. | | **[Agent-Native](./agent-native)** | Opt-in exposure of your tool's surface to coding agents over MCP. | -## Architecture - -```mermaid -flowchart TB - Definition["DevframeDefinition
(defineDevframe)"] - Definition --> Adapters - - subgraph Adapters["Adapters (choose one per deployment)"] - CLI["cli"] - Vite["vite"] - Build["build"] - Embedded["embedded"] - MCP["mcp"] - end - - Adapters --> Ctx["DevToolsNodeContext"] - - subgraph Ctx["DevToolsNodeContext"] - direction TB - RPC["rpc"] - Views["views (hostStatic)"] - Diagnostics["diagnostics"] - Agent["agent"] - end - - Ctx <-->|WebSocket or static| Client["DevToolsRpcClient
(browser)"] -``` - -Hosts (Vite DevTools is one) can wrap the same definition with their own adapter to augment `ctx` with extras like docks, terminals, and a command palette. - ## Install ```sh @@ -102,7 +72,7 @@ const devframe = defineDevframe({ await createCli(devframe).parse() ``` -The same definition can also be deployed through any of the other adapters — for example, mounted into Vite DevTools via the [`vite` adapter](./adapters#vite). +The same definition can also be deployed through any of the other adapters — for example, mounted into Vite DevTools via the [`vite` adapter](/adapters/vite). Run it: @@ -126,15 +96,29 @@ Devframe deploys the same `DevframeDefinition` through one of these adapters: | `embedded` | `createEmbedded(d, { ctx })` | Runtime registration into an existing host | | `mcp` | `createMcpServer(d, opts)` | Model Context Protocol server | -See [Adapters](./adapters) for the full reference. +See [Adapters](/adapters/) for the full reference. ## Framework- and build-tool-agnostic -Devframe has zero dependencies on Vite or any `@vitejs/*` package — the same definition runs in any Node environment, with any UI framework, against any build tool. Vite DevTools is one host built on top of devframe; mount your definition there with the [`vite` adapter](./adapters#vite), or write adapters for any other host. +Devframe has zero dependencies on Vite or any `@vitejs/*` package — the same definition runs in any Node environment, with any UI framework, against any build tool. Vite DevTools is one host built on top of devframe; mount your definition there with the [`vite` adapter](/adapters/vite), or write adapters for any other host. ## What's next - [Devframe Definition](./devframe-definition) — understand `defineDevframe` and the `DevToolsNodeContext` -- [Adapters](./adapters) — pick the right deployment target for your tool +- [Adapters](/adapters/) — pick the right deployment target for your tool - [RPC](./rpc) — define type-safe server functions your client can call - [Agent-Native](./agent-native) — expose your devframe to Claude Desktop, Cursor, or any MCP client + +## Built with Devframe + +Real-world devtools shipping on Devframe: + +- [**Vite DevTools**](https://devtools.vite.dev/) — the host that bundles multiple devframes into one UI (docks, command palette, terminals). Mount your own definition into it via the [`vite` adapter](/adapters/vite). +- [**ESLint Config Inspector**](https://github.com/eslint/config-inspector) — official ESLint tool for inspecting flat configs. +- [**node-modules-inspector**](https://github.com/antfu/node-modules-inspector) — interactive visualizer for your `node_modules` dependency graph. + +End-to-end examples in this repo, exercising the full adapter surface: + +- [**devframe-counter**](https://github.com/devframes/devframe/tree/main/examples/devframe-counter) — smallest possible demo, exercises all adapters. +- [**devframe-files-inspector**](https://github.com/devframes/devframe/tree/main/examples/devframe-files-inspector) — lists files in cwd via RPC; exercises CLI dev/build/spa surfaces. +- [**devframe-streaming-chat**](https://github.com/devframes/devframe/tree/main/examples/devframe-streaming-chat) — streams synthetic chat tokens from server to client via `ctx.rpc.streaming`. diff --git a/docs/guide/standalone-cli.md b/docs/guide/standalone-cli.md index e808efd..08db130 100644 --- a/docs/guide/standalone-cli.md +++ b/docs/guide/standalone-cli.md @@ -98,7 +98,7 @@ export default defineNuxtConfig({ }) ``` -Build with `nuxt build` and point `cli.distDir` at `./dist/public`. The SPA discovers its effective base at runtime — no `--base` rewrite needed. See the [Nuxt helper docs](./nuxt) for the full reference. +Build with `nuxt build` and point `cli.distDir` at `./dist/public`. The SPA discovers its effective base at runtime — no `--base` rewrite needed. See the [Nuxt helper docs](/helpers/nuxt) for the full reference. ## Connecting from the client @@ -157,21 +157,7 @@ The adapter derives each flag's CAC option from its schema — booleans become ` ## Open helpers -For the two actions every CLI devtool needs — open a file in the editor, reveal a path in the OS file explorer — use the prebuilt recipes instead of re-implementing them: - -```ts -import { openHelpers } from 'devframe/recipes/open-helpers' - -defineDevframe({ - id: 'my-tool', - name: 'My Tool', - setup(ctx) { - openHelpers.forEach(fn => ctx.rpc.register(fn)) - }, -}) -``` - -This registers `devframe:open-in-editor` and `devframe:open-in-finder`. Both helpers reuse the bundled [`launchEditor`](./utilities#devframe-utils-launch-editor) and [`open`](./utilities#devframe-utils-open) utilities, so there's nothing extra to install. +For the two actions every CLI devtool needs — open a file in the editor, reveal a path in the OS file explorer — use the prebuilt recipes from `devframe/recipes/open-helpers` instead of re-implementing them. See [Helpers → Open Helpers](/helpers/open-helpers) for the full reference. ## Snapshot queries for static builds @@ -320,7 +306,7 @@ For typed flag schemas, `parseCliFlags(schema, rawBag)` (from `devframe/adapters ## See also - [Devframe Definition](./devframe-definition) — field reference -- [Adapters → CLI](./adapters#cli) — full CLI adapter reference including `configureCli` and mount-path rules -- [Adapters → Dev](./adapters#dev) — `createDevServer` reference for bring-your-own-CLI integration +- [Adapters → CLI](/adapters/cli) — full CLI adapter reference including `configureCli` and mount-path rules +- [Adapters → Dev](/adapters/dev) — `createDevServer` reference for bring-your-own-CLI integration - [Client](./client) — `connectDevframe`, shared state, caching - [Agent-Native](./agent-native) — exposing your tool to Claude Desktop, Cursor, etc. diff --git a/docs/helpers/index.md b/docs/helpers/index.md new file mode 100644 index 0000000..819ad21 --- /dev/null +++ b/docs/helpers/index.md @@ -0,0 +1,16 @@ +--- +outline: deep +--- + +# Helpers + +Helpers are the optional, opt-in surface around the core `defineDevframe` API: small wrappers for runtime integration, prebuilt RPC recipes, and a curated set of low-level utilities. None of them are required to ship a devframe — reach for them when they match the shape of what you're building. + +| Helper | Entry | What it does | +|--------|-------|--------------| +| [Utilities](./utilities) | `devframe/utils/*` | Bundled small utilities — terminal colors, hashing, editor launch, structured-clone serialization, and more. | +| [Vite Bridge](./vite-bridge) | `devframe/helpers/vite` | Vite plugin for mounting a devframe inside any Vite-based host (Astro, SolidStart, plain Vite). | +| [Nuxt Module](./nuxt) | `@devframes/nuxt` | Nuxt module that wires a Nuxt SPA as a devframe client and serves the dev-time RPC bridge. | +| [Open Helpers](./open-helpers) | `devframe/recipes/open-helpers` | Prebuilt RPC actions for "open in editor" and "reveal in Finder". | + +Helpers vs. [adapters](/adapters/): an adapter takes a `DevframeDefinition` and deploys it as a runnable surface (CLI, dev server, static build, MCP server). A helper is a smaller piece — a Vite plugin, a Nuxt module, a recipe, a utility function — that you compose alongside an adapter. diff --git a/docs/guide/nuxt.md b/docs/helpers/nuxt.md similarity index 94% rename from docs/guide/nuxt.md rename to docs/helpers/nuxt.md index 7c2cd12..fb484d8 100644 --- a/docs/guide/nuxt.md +++ b/docs/helpers/nuxt.md @@ -9,7 +9,7 @@ The `@devframes/nuxt` module wires a Nuxt-built SPA as a devframe client, and op It handles the four things every Nuxt-powered standalone devtool needs: 1. **Base-agnostic assets.** Sets `app.baseURL: './'` and `vite.base: './'` so the same production build works at `/`, `/tool/`, and any other deployment path without build-time URL rewriting. -2. **Runtime RPC connection.** Adds a client plugin that calls [`connectDevframe()`](./client) once on page load and provides the result as `$rpc` on the Nuxt app. +2. **Runtime RPC connection.** Adds a client plugin that calls [`connectDevframe()`](/guide/client) once on page load and provides the result as `$rpc` on the Nuxt app. 3. **Dev-time RPC bridge.** When you pass `devframe`, `nuxt dev` spins up a separate WebSocket RPC server and serves `__connection.json` so the SPA can reach it — no hand-rolled Vite plugin required. 4. **TypeScript augmentation.** `useNuxtApp().$rpc` is typed as `DevToolsRpcClient` out of the box. @@ -130,6 +130,6 @@ At runtime the built SPA fetches `./__connection.json` (resolved against `docume ## See also -- [Standalone CLI recipe](./standalone-cli) — end-to-end walk-through -- [Client](./client) — `connectDevframe` reference -- [Adapters](./adapters) — CLI / Vite / Build / SPA / Kit / Embedded / MCP +- [Standalone CLI recipe](/guide/standalone-cli) — end-to-end walk-through +- [Client](/guide/client) — `connectDevframe` reference +- [Adapters](/adapters/) — CLI / Vite / Build / Embedded / MCP diff --git a/docs/helpers/open-helpers.md b/docs/helpers/open-helpers.md new file mode 100644 index 0000000..9e7b60b --- /dev/null +++ b/docs/helpers/open-helpers.md @@ -0,0 +1,56 @@ +--- +outline: deep +--- + +# Open Helpers + +Prebuilt RPC actions for the two file-system actions every CLI devtool needs — opening a file in the editor, revealing a path in the OS file explorer. Use the recipe instead of re-implementing them so every devframe converges on the same registered names and payload shape. + +```ts +import { openHelpers } from 'devframe/recipes/open-helpers' + +defineDevframe({ + id: 'my-tool', + name: 'My Tool', + setup(ctx) { + openHelpers.forEach(fn => ctx.rpc.register(fn)) + }, +}) +``` + +## Exports + +| Export | Registered name | Type | Args | Purpose | +|--------|------------------|------|------|---------| +| `openInEditor` | `devframe:open-in-editor` | `action` | `[filename: string]` | Open the file in the user's editor via [`launchEditor`](./utilities#devframe-utils-launch-editor). Accepts `file`, `file:line`, or `file:line:column`. | +| `openInFinder` | `devframe:open-in-finder` | `action` | `[path: string]` | Reveal the path in the OS file explorer via [`open`](./utilities#devframe-utils-open). | +| `openHelpers` | — | `readonly [openInEditor, openInFinder]` | — | Convenience array for batch registration. | + +Both functions are `action`-type RPCs returning `void` and use `valibot` schemas (`v.string()`) for their single argument. + +## Pick and choose + +Register only the helper you need rather than the whole array: + +```ts +import { openInEditor } from 'devframe/recipes/open-helpers' + +defineDevframe({ + id: 'my-tool', + setup(ctx) { + ctx.rpc.register(openInEditor) + }, +}) +``` + +## On the client + +The SPA calls these like any other RPC: + +```ts +const rpc = await connectDevframe() +await rpc.call('devframe:open-in-editor', 'src/main.ts:42:7') +await rpc.call('devframe:open-in-finder', '/abs/path/to/dir') +``` + +`launchEditor`'s editor auto-detection reads the `LAUNCH_EDITOR` environment variable on the server side — there is no client-side configuration. diff --git a/docs/guide/utilities.md b/docs/helpers/utilities.md similarity index 96% rename from docs/guide/utilities.md rename to docs/helpers/utilities.md index 69d177d..0d27ab9 100644 --- a/docs/guide/utilities.md +++ b/docs/helpers/utilities.md @@ -46,7 +46,7 @@ launchEditor('src/main.ts:42:7') launchEditor('src/main.ts:42:7', 'code') ``` -The auto-detection reads the `LAUNCH_EDITOR` environment variable and falls back to common defaults. Most devframes consume this through the prebuilt `openInEditor` recipe — see [Open helpers](./standalone-cli#open-helpers). +The auto-detection reads the `LAUNCH_EDITOR` environment variable and falls back to common defaults. Most devframes consume this through the prebuilt `openInEditor` recipe — see [Open helpers](./open-helpers). ### `devframe/utils/hash` @@ -120,7 +120,7 @@ off() ### `devframe/utils/shared-state` -Underlying immutable state container used by `ctx.rpc.sharedState`. Most devframes interact with it indirectly — see [Shared State](./shared-state). Available directly when you need a state hub outside the RPC host. +Underlying immutable state container used by `ctx.rpc.sharedState`. Most devframes interact with it indirectly — see [Shared State](/guide/shared-state). Available directly when you need a state hub outside the RPC host. ```ts import { createSharedState } from 'devframe/utils/shared-state' @@ -134,11 +134,11 @@ state.value() // { count: 1 } ### `devframe/utils/streaming-channel` -Low-level sink/reader primitives for streamed RPC payloads. Most devframes consume these through `ctx.rpc.streaming` — see [Streaming](./streaming). +Low-level sink/reader primitives for streamed RPC payloads. Most devframes consume these through `ctx.rpc.streaming` — see [Streaming](/guide/streaming). ### `devframe/utils/when` -Statically-validated when-clause expressions for conditional UI visibility. The runtime + types ship from here; the consumer fields (`when` on docks and commands) are kit-side. See [When Clauses](./when-clauses). +Statically-validated when-clause expressions for conditional UI visibility. The runtime + types ship from here; the consumer fields (`when` on docks and commands) are kit-side. See [When Clauses](/guide/when-clauses). ## Why a `utils/*` subpath diff --git a/docs/helpers/vite-bridge.md b/docs/helpers/vite-bridge.md new file mode 100644 index 0000000..3a8bfb2 --- /dev/null +++ b/docs/helpers/vite-bridge.md @@ -0,0 +1,33 @@ +--- +outline: deep +--- + +# Vite Bridge + +A thin Vite plugin for mounting a devframe inside an existing Vite dev server. Used by [`@devframes/nuxt`](./nuxt) and available for any Vite-based host (Astro, SolidStart, plain Vite apps). + +This sits below the [`vite` adapter](/adapters/vite) on the abstraction ladder: the adapter targets the full Vite DevTools dock; the bridge is the lower-level Vite plugin you reach for when you want a devframe to ride along with an existing app's dev server without the DevTools dock. + +```ts +import { viteDevBridge } from 'devframe/helpers/vite' +import { defineConfig } from 'vite' +import devframe from './devframe' + +export default defineConfig({ + plugins: [viteDevBridge(devframe)], +}) +``` + +## Modes + +- **Static mount** (default) — mounts `def.cli.distDir` at `options.base` (`/__/` by default). No RPC server. Useful when you only need the SPA bundle served from a known path. +- **Bridge mode** (`devMiddleware: true | {…}`) — skips the static mount; the host app owns the SPA. Devframe spawns a separate RPC + WS server and registers Vite middleware at `__connection.json` so the host-served SPA can discover the WS endpoint. + +## Options + +| Option | Default | Description | +|--------|---------|-------------| +| `base` | `def.basePath ?? '/__/'` | Mount path inside the Vite dev server. | +| `devMiddleware` | `false` | `true` or `{ port?, host?, flags? }` to enable bridge mode. | + +When `devMiddleware` is an object, the inner fields mirror [`createDevServer`](/adapters/dev) — `port` pins the WS server port, `host` sets the bind host, and `flags` is forwarded to `def.setup(ctx, { flags })`. diff --git a/packages/devframe/README.md b/packages/devframe/README.md index 067293b..6665550 100644 --- a/packages/devframe/README.md +++ b/packages/devframe/README.md @@ -1,84 +1,28 @@ # devframe -Framework- and build-tool-agnostic foundation for building generic DevTools. Define your devtool once — its RPC, its data, its SPA, its CLI shape — and deploy the same definition anywhere through a set of pluggable adapters. +[![npm version][npm-version-src]][npm-version-href] +[![npm downloads][npm-downloads-src]][npm-downloads-href] +[![bundle][bundle-src]][bundle-href] +[![JSDocs][jsdocs-src]][jsdocs-href] +[![License][license-src]][license-href] -Full documentation: [https://devfra.me/](https://devfra.me/). +Framework-neutral foundation for building generic DevTools. -## Install +Documentation: [https://devfra.me/](https://devfra.me/). -```sh -pnpm add devframe -``` - -## Hello, Devframe - -```ts -import { defineDevframe, defineRpcFunction } from 'devframe' -import { createCli } from 'devframe/adapters/cli' - -const devframe = defineDevframe({ - id: 'my-devframe', - name: 'My Devframe', - setup(ctx) { - ctx.rpc.register(defineRpcFunction({ - name: 'my-devframe:hello', - type: 'static', - jsonSerializable: true, - handler: () => ({ message: 'hello' }), - })) - }, -}) - -await createCli(devframe).parse() -``` - -## Adapters - -| Adapter | Use case | -|---------|----------| -| `cli` | Standalone CLI tool with `dev` / `build` / `mcp` subcommands. | -| `build` | Generates a static, self-contained SPA snapshot. | -| `vite` | Mounts the devframe into Vite DevTools (or any compatible host) via `@vitejs/devtools-kit`. | -| `embedded` | Overlays inside another devtool's UI. | -| `mcp` | Surfaces the devframe's RPC to coding agents over MCP. | - -## Agent-Native (experimental) - -> [!WARNING] -> The agent-native surface — the `agent` field on `defineRpcFunction`, `DevToolsAgentHost`, and the `devframe/adapters/mcp` adapter — may change without a major version bump until it stabilizes. - -Devframe surfaces a devframe's RPC functions, tools, and resources to coding agents over [MCP](https://modelcontextprotocol.io). Flag an RPC function with `agent: { description }` to expose it, then spin up an MCP server: - -```ts -import { defineDevframe, defineRpcFunction } from 'devframe' -import { createMcpServer } from 'devframe/adapters/mcp' - -const getSummary = defineRpcFunction({ - name: 'my-plugin:get-summary', - type: 'query', - agent: { - description: 'Return a short summary of the current build state.', - }, - setup: ctx => ({ handler: async () => buildSummary() }), -}) - -const devframe = defineDevframe({ - id: 'my-plugin', - setup(ctx) { - ctx.rpc.register(getSummary) - ctx.agent.registerResource({ - id: 'latest-build', - name: 'Latest build', - read: () => ({ text: renderMarkdown(latestBuild) }), - }) - }, -}) - -await createMcpServer(devframe, { transport: 'stdio' }) -``` +## License -Or via the CLI: `devframe mcp`. `@modelcontextprotocol/sdk` is a peer dependency — add it when you want MCP support. See the [Agent-Native guide](https://devfra.me/guide/agent-native) for the full API and Claude Desktop integration example. +[MIT](../../LICENSE.md) License © [Anthony Fu](https://github.com/antfu) -## License + -[MIT](./LICENSE.md) +[npm-version-src]: https://img.shields.io/npm/v/devframe?style=flat&colorA=080f12&colorB=517158 +[npm-version-href]: https://npmx.dev/package/devframe +[npm-downloads-src]: https://img.shields.io/npm/dm/devframe?style=flat&colorA=080f12&colorB=517158 +[npm-downloads-href]: https://npmx.dev/package/devframe +[bundle-src]: https://img.shields.io/bundlephobia/minzip/devframe?style=flat&colorA=080f12&colorB=517158&label=minzip +[bundle-href]: https://bundlephobia.com/result?p=devframe +[license-src]: https://img.shields.io/github/license/devframes/devframe.svg?style=flat&colorA=080f12&colorB=517158 +[license-href]: https://github.com/devframes/devframe/blob/main/LICENSE.md +[jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=517158 +[jsdocs-href]: https://www.jsdocs.io/package/devframe