Skip to content

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.

Options

createCli(def, options?) accepts:

OptionDefaultDescription
defaultPort9999 (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<void> — 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<void>
}

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 <file>', '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 blockEntryPurpose
createDevServer(def, opts?)devframe/adapters/devh3 + WebSocket RPC + SPA mount
createBuild(def, opts?)devframe/adapters/buildStatic deploy
createMcpServer(def, opts?)devframe/adapters/mcpstdio MCP server
parseCliFlags(schema, raw)devframe/adapters/cliValidate a flag bag against a CliFlagsSchema

See the Standalone CLI guide for a worked commander example.

Released under the MIT License.