Devframe Definition
One defineDevframe call returns a portable DevframeDefinition any adapter consumes.
Minimal definition
import { defineDevframe, defineRpcFunction } from 'devframe'
import * as v from 'valibot' // npm i valibot
export default defineDevframe({
id: 'my-devframe',
name: 'My Devframe',
version: '1.0.0',
packageName: 'my-devframe',
importMetaUrl: import.meta.url,
homepage: 'https://github.com/me/my-devframe',
description: 'A one-line summary of what the tool does.',
icon: 'ph:gauge-duotone',
setup(ctx) {
// A scoped context auto-namespaces ids with your devframe `id`.
const my = ctx.scope('my-devframe')
// Register your RPC functions, shared state, etc. here.
my.rpc.register(defineRpcFunction({
name: 'hello', // stored as `my-devframe:hello`
type: 'static',
jsonSerializable: true,
handler: () => ({ message: 'hello' }),
}))
},
})Definition fields
| Field | Type | Description |
|---|---|---|
id | string | Required. Unique namespaced id (kebab-case); prefixes RPC/dock/MCP-tool names. |
name | string | Required. Display name (dock, agent manifests). |
version | string | Required. Semver; shown in hub UIs, diagnostics. |
packageName | string | Required. npm package (@scope/my-tool). |
importMetaUrl | string | Recommended. Pass import.meta.url — the deps resolution base: default resolveFrom for remote assets and declared services. |
homepage | string | Required. Homepage/docs URL. |
description | string | Required. One-line summary. |
icon | string | { light, dark } | Optional Iconify name or URL; light/dark pairs. |
basePath | string | Optional mount-path override. Default / standalone (cli/build), /__<id>/ hosted (vite/embedded). |
duplicationStrategy | 'warn' | 'silent' | 'throw' | 'duplicate' | Hub reaction when another devframe shares this id. Default 'warn'. See Hub; standalone adapters ignore it. |
capabilities | { dev?, build? } | Per-runtime feature flags. boolean = whole runtime; object = individual features. |
services | DevframeServiceInput[] | Wire services consumed — descriptors ({ package, version?, required?, options? }) imported against the plugin's own deps, or ready definitions. See Cross-Plugin Services. |
clientAssets | string | RemoteAssets | Built SPA served as the UI — local dist dir or remote assets. Read by every UI-serving adapter (dev, build, vite, next, hub). |
rpc | { snapshot?: (string | { method, inputs })[] } | RPC config. rpc.snapshot opts an RPC this devframe doesn't own into the static dump. Bare method id bakes the no-arg call; { method, inputs } bakes one record per argument-tuple (inputs = tuples or async (ctx) => tuples). First tuple = fallback. |
setup | (ctx, info?) => void | Promise<void> | Required. Server-side entry point, run in every runtime. Optional 2nd arg carries runtime metadata — notably parsed CLI flags under createCac. |
cli | DevframeCliOptions | CLI adapter defaults. See CLI options. |
Sourcing metadata from package.json
Import metadata from package.json (name → packageName; devframe name is a separate display label):
import pkg from '../package.json' with { type: 'json' }
export default defineDevframe({
id: 'my-devframe',
name: 'My Devframe', // display label
version: pkg.version,
packageName: pkg.name,
importMetaUrl: import.meta.url,
homepage: pkg.homepage,
description: pkg.description,
setup(ctx) { /* … */ },
})Resolving against the plugin's own dependencies
importMetaUrl resolves companion packages against the plugin's own dependencies:
export default defineDevframe({
// …metadata as above
importMetaUrl: import.meta.url,
// Served from the locally installed `my-devframe--assets`, resolved via
// `importMetaUrl` — works under pnpm's strict layout with zero network.
clientAssets: { package: `${pkg.name}--assets`, version: pkg.version },
// Imported from `my-devframe`'s own dependency graph.
services: [{ package: '@scope/my-service', version: pkg.version }],
setup(ctx) { /* … */ },
})For remote assets, importMetaUrl is the default resolveFrom; a per-source value wins; resolveFrom: null opts out.
Serving the UI with clientAssets
import { fileURLToPath } from 'node:url'
export default defineDevframe({
// …metadata as above
importMetaUrl: import.meta.url,
// A local build resolved from the module — works from source and the
// published package.
clientAssets: fileURLToPath(new URL('../dist/spa', import.meta.url)),
setup(ctx) { /* … */ },
})For assets you host yourself, call ctx.views.hostStatic in setup — Client Assets.
Runtime flags
ctx.mode ('dev'/'build') gates runtime-specific work:
defineDevframe({
id: 'my-devframe',
name: 'My Devframe',
setup(ctx) {
if (ctx.mode === 'build') {
// Static-only work — baked into the RPC dump.
}
else {
// Dev-mode wiring, file watchers, etc.
}
},
})The CLI dev server sets mode: 'dev'; createBuild, 'build'.
The setup context
setup(ctx) receives a DevframeNodeContext:
interface DevframeNodeContext {
readonly cwd: string
readonly workspaceRoot: string
readonly mode: 'dev' | 'build'
host: DevframeHost // runtime abstraction (mountStatic / resolveOrigin / getStorageDir)
rpc: RpcFunctionsHost // register + broadcast + sharedState
views: DevframeViewHost // static file hosting (`hostStatic`)
diagnostics: DevframeDiagnosticsHost
agent: DevframeAgentHost // expose tools + resources to coding agents
services: DevframeServicesHost // typed cross-plugin service registry
staticConfig: Partial<DevframeConnectionConfigsRegistry> // this context's own ConnectionMeta.configs
scope: (id) => DevframeScopedNodeContext // namespaced view (preferred)
}Cross-plugin services
ctx.services is a typed, namespaced registry — one integration exposes a capability, others consume it (Cross-Plugin Services).
ctx.services.provide('my-plugin:sources', sources)
ctx.services.whenAvailable('my-plugin:sources', (sources) => {
sources.register(/* ... */)
})Static connection configs
ctx.staticConfig is this context's own ConnectionMeta.configs — read-only boot-time data from the connection handshake; write it during setup(ctx). Contrast ctx.scope(id).settings (mutable, synced).
declare module 'devframe/types' {
interface DevframeConnectionConfigsRegistry {
'my-plugin': { featureFlag: boolean }
}
}
ctx.staticConfig['my-plugin'] = { featureFlag: true }Storage scopes
ctx.host.getStorageDir(scope) places persisted state in three classes:
| Scope | Placement | For |
|---|---|---|
workspace | committable, <workspaceRoot>/.devframe/ | team-shared: saved presets, config |
project | per-checkout, <cwd>/node_modules/.<app>/devframe/ | caches, personal settings |
global | per-user, ~/.<app>/devframe/ | auth tokens, machine-wide prefs |
ctx.scope(id) returns a namespace-scoped view (Scoped Context) auto-prefixing every RPC id, shared-state key, and streaming channel, plus a persisted settings store (project/global scopes use the matching storage classes).
Host adapters can augment ctx — e.g. the vite adapter's dock, command, message, and terminal hosts.
CLI options
cli sets CLI-adapter defaults and plugs flags/commands into CAC:
defineDevframe({
id: 'my-devframe',
name: 'My Devframe',
clientAssets: './client/dist', // built SPA served as the UI
cli: {
command: 'my-devframe', // binary name; default: the `id`
port: 9876, // preferred port; default: 9999
portRange: [9876, 10000], // forwarded to get-port-please
random: false, // forwarded to get-port-please
host: 'localhost', // default host; --host overrides
open: true, // auto-open the browser on dev start; embeds the current OTP so the tab lands authenticated
configure(cli) { // contribute capability flags/commands
cli
.option('--my-flag <value>', 'Tool-specific flag')
},
},
setup(ctx, { flags }) {
// `flags` carries the parsed cac bag — contains built-in flags
// (`--port`, `--host`, `--open`, `--no-open`) and anything you added
// in `configure`.
},
})| Field | Type | Description |
|---|---|---|
command | string | Binary name in --help. Default: the id. |
port | number | Preferred dev-server port. |
portRange | [number, number] | Port scan range (get-port-please). |
random | boolean | Prefer a random open port. |
host | string | Default bind host. |
open | boolean | string | true = origin, string = a path, false = off (--open/--no-open). With auth, embeds the OTP. |
auth | boolean | Disable WS trust flow when localhost-only, single-user. Default true. |
configure | (cli: CAC) => void | Contribute flags/commands before createCac's configureCli. |
Multiple runtimes, one definition
Wire the definition into multiple adapters from one file:
import { createPluginFromDevframe } from '@vitejs/devtools-kit/node'
import { createBuild } from 'devframe/adapters/build'
import { createCac } from 'devframe/adapters/cac'
const devframe = defineDevframe({ id: 'my-devframe', name: 'My Devframe', setup() {} })
// 1. Standalone CLI:
await createCac(devframe).parse()
// 2. Offline snapshot:
await createBuild(devframe, { outDir: 'dist-static' })
// 3. Mount into a host (Vite DevTools shown — other hosts can implement equivalents):
export const myPlugin = () => createPluginFromDevframe(devframe)What's next
- Adapters — deployment targets
- RPC — register server functions
viteadapter — mount into a host