Skip to content

Devframe Definition ​

One defineDevframe call returns a portable DevframeDefinition any adapter consumes.

Minimal definition ​

ts
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 ​

FieldTypeDescription
idstringRequired. Unique namespaced id (kebab-case); prefixes RPC/dock/MCP-tool names.
namestringRequired. Display name (dock, agent manifests).
versionstringRequired. Semver; shown in hub UIs, diagnostics.
packageNamestringRequired. npm package (@scope/my-tool).
importMetaUrlstringRecommended. Pass import.meta.url — the deps resolution base: default resolveFrom for remote assets and declared services.
homepagestringRequired. Homepage/docs URL.
descriptionstringRequired. One-line summary.
iconstring | { light, dark }Optional Iconify name or URL; light/dark pairs.
basePathstringOptional 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.
servicesDevframeServiceInput[]Wire services consumed — descriptors ({ package, version?, required?, options? }) imported against the plugin's own deps, or ready definitions. See Cross-Plugin Services.
clientAssetsstring | RemoteAssetsBuilt 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.
cliDevframeCliOptionsCLI adapter defaults. See CLI options.

Sourcing metadata from package.json ​

Import metadata from package.json (name → packageName; devframe name is a separate display label):

ts
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:

ts
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 ​

ts
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:

ts
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:

ts
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).

ts
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).

ts
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:

ScopePlacementFor
workspacecommittable, <workspaceRoot>/.devframe/team-shared: saved presets, config
projectper-checkout, <cwd>/node_modules/.<app>/devframe/caches, personal settings
globalper-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:

ts
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`.
  },
})
FieldTypeDescription
commandstringBinary name in --help. Default: the id.
portnumberPreferred dev-server port.
portRange[number, number]Port scan range (get-port-please).
randombooleanPrefer a random open port.
hoststringDefault bind host.
openboolean | stringtrue = origin, string = a path, false = off (--open/--no-open). With auth, embeds the OTP.
authbooleanDisable WS trust flow when localhost-only, single-user. Default true.
configure(cli: CAC) => voidContribute flags/commands before createCac's configureCli.

Multiple runtimes, one definition ​

Wire the definition into multiple adapters from one file:

ts
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 ​

Released under the MIT License.