# Dowel documentation > Dowel is one binary for scaffolding, code generation, plugin installs, hot reload and a stdio MCP server, with command verbs you know from php artisan. # Getting Started Everything you need to add `dowel` to your project, run your first command, and wire the MCP server into your AI client. - [Pick your path](#pick-your-path) - [What is dowel?](#what-is-dowel) - [Related Documentation](#related-documentation) ## Pick your path - [**Installation**](https://anilcancakir.com/dowel/docs/getting-started/installation.md): Add dowel to a Dart or Flutter project. - [**Quickstart**](https://anilcancakir.com/dowel/docs/getting-started/quickstart.md): 5-step end-to-end walkthrough from install to first running command. - [**MCP setup**](https://anilcancakir.com/dowel/docs/mcp/setup.md): Wire the dowel MCP server into Claude Code, Cursor, Windsurf, or another AI client. ## What is dowel? "Composable CLI framework and stdio MCP server for Flutter and Dart. Scaffolding, code generation, transactional plugin installs, hot reload orchestration, REPL, and AI agent tool surfaces in one binary." That description captures what it ships today: a single binary that covers the full lifecycle of a Flutter project from initial scaffold through live inspection by an AI agent, without stitching together a collection of separate tools. Built on pure Dart 3.4+ with no Flutter runtime dependency, `dowel` runs as a plain VM process and ships 22 built-in commands across six groups: consumer setup, plugin lifecycle, code generators, dev-loop orchestration, inspection, and MCP server management. The plugin system extends this catalogue via `DowelServiceProvider` subclasses, so teams can bundle project-specific commands alongside the built-ins and install them into any consumer project with a single `plugin:install` call. The framework targets both developers (who use the CLI) and AI agents (which use the MCP server) with first-class support for both surfaces. What sets it apart from standalone CLI tools is the tight integration of four capabilities in one binary: a Signature-DSL command framework for fast command authoring, a transactional plugin installer with idempotency and rollback, a stdio Model Context Protocol (MCP) server that exposes both substrate commands and plugin-contributed tools to any compliant AI client, and direct VM Service hooks for hot reload, hot restart, and connected REPL evaluation against a running Flutter app. No orchestration layer is needed between them; they share the same registry, the same provider contract, and the same binary entry point. ## Related Documentation - New here? Start with [Installation](https://anilcancakir.com/dowel/docs/getting-started/installation.md). - Already installed? Run the [Quickstart](https://anilcancakir.com/dowel/docs/getting-started/quickstart.md). --- # Installation Getting started with dowel requires a few steps to wire the CLI binary and codegen scaffolding into your project. - [Requirements](#requirements) - [Installation](#installation-step) - [Basic Setup](#basic-setup) - [Verify Installation](#verify-installation) - [Related Documentation](#related-documentation) ## Requirements Before adding dowel to your project, ensure your environment meets these minimum version requirements. We recommend staying on the latest stable Dart SDK for the best experience. dowel is a pure Dart package. Flutter is optional: it is only required when you intend to drive a running Flutter app (hot reload, Tapir gestures, Crumbs inspection). Projects that only need scaffolding, code generation, or the MCP server can run on any Dart 3.4+ environment. | Dependency | Minimum Version | Recommended | |:---|:---|:---| | Dart | `>= 3.4.0` | `3.6.0+` | | Flutter | optional | `3.27.0+` | ## Installation Add `dowel` to your project using the Dart CLI: ```bash dart pub add dowel ``` Alternatively, add it manually to your `pubspec.yaml`: ```yaml dependencies: dowel: ^0.0.21 ``` Then fetch dependencies: ```bash dart pub get ``` ## Basic Setup After installing the package, run the scaffold command to create the canonical consumer wrapper in your project: ```bash dart run dowel install ``` This command writes three files into your project: | File | Purpose | |:---|:---| | `bin/dispatcher.dart` | The runnable entry point. `dart run :dispatcher ` (or `./bin/dowel ` once the AOT bundle is built) dispatches here. | | `lib/app/_plugins.g.dart` | Generated plugin provider list. Updated by `plugin:install` and `plugins:refresh`. | | `lib/app/commands/_index.g.dart` | Generated command index. Updated by `make:command` and `commands:refresh`. | The command is idempotent: re-running it skips files that already exist. Pass `--force` to overwrite: ```bash dart run dowel install --force ``` ### Dependency injection into pubspec.yaml `install` also ensures `dowel` is listed as a direct dependency in your `pubspec.yaml`. The generated barrel files at `lib/app/_plugins.g.dart` and `lib/app/commands/_index.g.dart` import from `package:dowel/dowel.dart`, so the analyzer requires a direct dep to pass `depend_on_referenced_packages`. The injection is automatic and follows two modes: - **Monorepo / path-dep workflow**: when your project already resolves `dowel` via a local path (detected from `.dart_tool/package_config.json`), the scaffold injects a `path:` reference pointing to the same checkout. This avoids a pub.dev fetch against a version that may not yet be published. - **pub.dev workflow**: when no local resolution is found, the scaffold injects the `any` constraint and lets pub resolve the version transitively. Replace `any` with a pinned range (e.g. `^0.0.21`) before committing. ## Verify Installation Run the built-in `list` command to confirm the scaffold is wired correctly: ```bash dart run dowel list ``` The output lists all registered commands grouped by the part of the name before the `:`. Commands without a `:` (`start`, `install`, `tinker` and the rest) print first, with no heading; the namespaced commands follow under one heading per prefix, in alphabetical order. A fresh scaffold with no additional plugins shows the 22 built-in commands: ``` ℹ Available commands (22): doctor Run environment preflight checks (flutter, dart, port availability). help Show detailed help for a single command. hot-restart Hot restart the running app (sends `R` to flutter run's stdin). Drops Dart state, keeps process. install Scaffold the canonical Flutter consumer (bin/dispatcher.dart + barrels) and install bin/dowel fast-CLI. list List every registered command grouped by namespace. logs Print or --follow the captured flutter run log. reload Hot reload the running app (sends `r` to flutter run's stdin). State preserved. restart Stop + start the running flutter app. start Boot `flutter run -d ` detached and record the VM Service URI to this project's session. status Print JSON status of the recorded flutter app. stop Stop the running flutter app and delete this project's session. tinker Connected REPL into the running Flutter app (Dart expression evaluation via VM Service). commands commands:refresh Rescan lib/app/commands/ and rewrite the auto-discovery index. make make:command Scaffold a new DowelCommand subclass under lib/app/commands/ (or lib/src/commands/ when run inside a plugin). make:fast-cli Scaffold a fast-CLI wrapper (bin/dowel) plus AOT-compiled dowel binary, giving local invocations a ~50ms startup vs ~3s with dart run. make:plugin Scaffold a new dowel plugin skeleton. mcp mcp:install Add the dowel MCP server entry to .mcp.json (idempotent; preserves other server entries). mcp:serve Run the dowel MCP server (stdio JSON-RPC) for Claude Code / Cursor / Windsurf. After editing .dowel/mcp.json or env vars, run /mcp reconnect dowel in Claude Code to apply the changes. mcp:uninstall Remove the dowel MCP server entry from .mcp.json (preserves other entries). plugin plugin:install Register a third-party dowel plugin (install.yaml manifest preferred; direct registration into the dowel scaffold otherwise). plugin:uninstall Uninstall a third-party dowel plugin (reverses the install manifest). plugins plugins:refresh Regenerate lib/app/_plugins.g.dart from .dowel/plugins.json registry. ``` The stray paragraph under `mcp:serve` is that command's two-paragraph description printed as written. If the list prints without errors, the installation is complete. ## Related Documentation - [Quickstart](https://anilcancakir.com/dowel/docs/getting-started/quickstart.md): a first command, a plugin and the MCP server. - [Commands](https://anilcancakir.com/dowel/docs/commands/index.md): what each built-in command does. - [MCP Setup](https://anilcancakir.com/dowel/docs/mcp/setup.md): wire the server into an AI client. --- # Quickstart A 5-step walkthrough taking a fresh Dart or Flutter project from zero to a working dowel setup with a plugin installed and a custom command registered. - [1. Install and scaffold](#1-install-and-scaffold) - [2. Add a custom command](#2-add-a-custom-command) - [3. Install a plugin](#3-install-a-plugin) - [4. Start a Flutter app (optional)](#4-start-a-flutter-app-optional) - [5. List everything wired](#5-list-everything-wired) - [What's next?](#whats-next) - [Related Documentation](#related-documentation) Prerequisites: Dart 3.4+ SDK, a Flutter project with a valid `pubspec.yaml`, and `flutter pub get` already run so `.dart_tool/package_config.json` is present. --- ## 1. Install and scaffold Add `dowel` to your project dependencies, then run `install` to write the canonical wrapper files. The scaffold is idempotent: re-running it when the files already exist is a safe no-op (pass `--force` to overwrite). ```bash dart pub add dowel dart run dowel install ``` The scaffold writes three files: - `bin/dispatcher.dart`: the CLI entry point you will invoke for every subsequent `dart run :dispatcher` call (or `./bin/dowel ` once the AOT bundle is built). - `lib/app/_plugins.g.dart`: codegen barrel that wires registered plugin providers into the consumer wrapper automatically. - `lib/app/commands/_index.g.dart`: codegen barrel that auto-discovers commands under `lib/app/commands/` so `dart run dowel list` picks them up without manual registration. After scaffolding, run commands with `./bin/dowel` or `dart run dowel`; both delegate to the consumer wrapper `bin/dispatcher.dart`, which loads your commands and plugins. --- ## 2. Add a custom command Generate a command scaffold under `lib/app/commands/`. The name argument is normalised: `HelloWorld` becomes class `HelloWorldCommand` with signature `hello-world`. The codegen barrel is refreshed automatically so the command appears on the next `dowel list` without an extra step. ```bash dart run dowel make:command HelloWorld ``` The generator creates `lib/app/commands/hello_world_command.dart` with a skeleton `handle` method and a pre-filled `signature` string. Edit the file to implement your command logic, then run `dart run dowel hello-world` to invoke it. If you are authoring a plugin (a `lib/src/_dowel_provider.dart` exists at the project root) the generator writes to `lib/src/commands/` instead and injects the import and registration line into the provider file automatically. --- ## 3. Install a plugin Add the plugin package to your `pubspec.yaml` and resolve it, then register it with the consumer wrapper via `plugin:install`. The command runs a two-stage resolution: 1. When the plugin ships an `install.yaml` manifest, the manifest flow runs: ops are parsed, conflict-checked, and committed atomically, then the plugin's `DowelServiceProvider` is registered in `.dowel/plugins.json` and `lib/app/_plugins.g.dart` is refreshed. 2. When no manifest is found but the canonical scaffold (`lib/app/_plugins.g.dart`) is present, registration writes directly to `.dowel/plugins.json` and refreshes the codegen barrel (no `bin/dispatcher.dart` edit needed). Without the scaffold the command stops and asks you to run `dart run dowel install`. ```bash dart pub add awesome_plugin dart run dowel plugin:install awesome_plugin ``` After installation the plugin's commands appear in `dart run dowel list`, under the heading of the prefix before the `:` in their names. Pass `--dry-run` to preview every write operation before it commits. Pass `--use-yaml-only` to error out instead of falling back when no `install.yaml` is found. --- ## 4. Start a Flutter app (optional) If your project ships a Flutter app, `start` boots `flutter run` in a detached process and writes a per-project session with the VM Service URI. The session file is what lets MCP clients and hot-reload/hot-restart commands locate the running instance without you providing the URI each time. ```bash dart run dowel start --device=chrome ``` The default target is `chrome`. Pass `--device=macos` (or any Flutter device identifier) for desktop or mobile targets. The command scrapes the VM Service URI from the flutter run output, normalises it to a `ws://` WebSocket address, and writes the full state (PID, VM URI, web port, device, project root) to this project's session at `~/.dowel/sessions//state.json`. Sessions are per project, so a second app can be driven at the same time without either overwriting the other. Once the state file is present, `reload`, `hot-restart`, `logs`, and `stop` all resolve the running process automatically from the file without extra flags. --- ## 5. List everything wired Verify the full command registry by running `list`. The output is grouped by `:` namespace: commands without a `:` come first with no heading, and each `:` prefix (built-in or plugin-contributed) gets its own section. ```bash dart run dowel list ``` A clean setup shows the 22 built-in commands: 12 without a namespace (`start`, `stop`, `status`, `logs`, `restart`, `reload`, `hot-restart`, `doctor`, `list`, `help`, `install`, `tinker`) listed first, then the namespaced ones under the headings `commands`, `make`, `mcp`, `plugin` and `plugins`. Your `hello-world` command appears among the commands without a namespace, and a plugin's commands appear under the heading of the prefix before the `:` in their names. --- ## What's next? - Browse the full [command catalogue](https://anilcancakir.com/dowel/docs/commands/index.md) to see what each builtin does. - Wire the MCP server into your AI client via [MCP setup](https://anilcancakir.com/dowel/docs/mcp/setup.md). - Write your own plugin via the [plugin authoring guide](https://anilcancakir.com/dowel/docs/plugins/authoring.md). --- All five commands shown above are part of the `dowel` built-in command set and require no additional packages beyond the initial `dart pub add` in step 1. Plugin commands contributed by third-party packages appear in `dowel list` under their own namespace after `plugin:install` completes. ## Related Documentation - [Installation](https://anilcancakir.com/dowel/docs/getting-started/installation.md): requirements and the project scaffold. - [Plugin Authoring](https://anilcancakir.com/dowel/docs/plugins/authoring.md): write a plugin of your own. --- # Commands Catalogue of every user-facing command shipped by `dowel`. Twenty-two commands, grouped by intent. Every command is invoked as `dart run dowel `, or as `./bin/dowel ` once the project has run `install` (the fast wrapper). Commands are auto-discovered through the registered providers list; nothing wires by hand. Need a quick reminder of what a command does without leaving the terminal? Run `dart run dowel list` for the full registry grouped by namespace, or `dart run dowel help ` for the per-command flag surface. This page exists for the deeper view: the boot mode, the MCP exposure, and the grouping rationale. - [How to read this page](#how-to-read-this-page) - [Naming conventions](#naming-conventions) - [Lifecycle](#lifecycle) - [Scaffolding](#scaffolding) - [Plugin management](#plugin-management) - [MCP](#mcp) - [Introspection](#introspection) - [Code generation](#code-generation) - [Featured deep-dives](#featured-deep-dives) - [Boot mode at a glance](#boot-mode-at-a-glance) - [Related Documentation](#related-documentation) ## How to read this page Each group section ships a single table with four columns: - **Command** is the canonical name you type after `dart run dowel` (or `./bin/dowel`). - **Description** is a one-line summary of the command, drawn from the `description` getter that `dart run dowel list` prints (reworded here where the getter's wording has fallen behind the behaviour, for example the `~/.dowel/state.json` path it still names for `start` and `stop`). - **Boot Mode** is the `CommandBoot` value the dispatcher reads before invoking `handle()` (see `lib/src/console/command_boot.dart`). `none` means pure CLI: no Flutter binding, no VM Service connection. `connected` means the command dials this project's session and fails fast if no app is running. - **MCP Tool** is the canonical `dowel_*` tool name surfaced over stdio JSON-RPC by `mcp:serve`, or the literal `no` when the command is not in the substrate allowlist. The allowlist lives in `lib/src/mcp/mcp_server.dart` (`_safeDowelCommandNames`, ten commands) and intentionally omits `help`, codegen, installer, and MCP-meta commands. ## Naming conventions Two shapes appear in the catalogue: - Single word (`start`, `stop`, `status`, `logs`, `restart`, `reload`, `hot-restart`, `doctor`, `tinker`, `help`, `list`, `install`) for top-level actions on the substrate. - Namespaced `:` (`make:plugin`, `make:command`, `make:fast-cli`, `plugin:install`, `plugin:uninstall`, `plugins:refresh`, `mcp:serve`, `mcp:install`, `mcp:uninstall`, `commands:refresh`) for actions scoped to a subsystem. `hot-restart` is a single word that happens to contain a hyphen: it lives in the lifecycle group and is not namespaced, so `list` prints it among the commands without a heading. It sends the `R` keypress to `flutter run`. ## Lifecycle Seven commands that own the running Flutter process: spawn, stop, query, tail, restart, reload, hot-restart. All run pure CLI; none of them boot Flutter or Magic in the dowel process. State lives in a per-project session directory under `~/.dowel/sessions/`, written by `start` and consumed by everything that needs the VM Service URI. `~/.dowel/state.json` remains a pointer to whichever session started last. Any command that acts on the recorded app refuses when the session belongs to another project, so a `stop` from the wrong checkout cannot kill a sibling's run. All seven are also surfaced as MCP tools. Together with `list`, `doctor` and `tinker` from the [Introspection](#introspection) group they make up the ten MCP tools, the agent-facing surface area of the substrate. | Command | Description | Boot Mode | MCP Tool | |:---|:---|:---|:---| | `start` | Boot `flutter run -d ` detached and record the VM Service URI to this project's session. | none | dowel_start | | `stop` | Stop the running flutter app's whole process group, wait until it and its web port are gone, and delete this project's session. | none | dowel_stop | | `status` | Print JSON status of the recorded flutter app. | none | dowel_status | | `logs` | Print or `--follow` the captured flutter run log. | none | dowel_logs | | `restart` | Stop and start the running flutter app; carries the prior session's device, web port, VM Service port, CDP port, `--flutter-arg` values and profile build mode across. | none | dowel_restart | | `reload` | Hot reload the running app (sends `r` to flutter run's stdin). State preserved. | none | dowel_reload | | `hot-restart` | Hot restart the running app (sends `R` to flutter run's stdin). Drops Dart state, keeps process. | none | dowel_hot_restart | ## Scaffolding Four commands that write source files into the consumer project. They are deliberately excluded from the MCP allowlist: source mutation is better routed through the client's own file tools, and the stub system uses placeholder substitution (`{{ name }}`, `{{ pascalName }}`, `{{ commandPrefix }}`) that benefits from interactive prompts. | Command | Description | Boot Mode | MCP Tool | |:---|:---|:---|:---| | `make:plugin` | Scaffold a new dowel plugin skeleton. | none | no | | `make:command` | Scaffold a new DowelCommand subclass under `lib/app/commands/` (or `lib/src/commands/` when run inside a plugin). | none | no | | `make:fast-cli` | Scaffold the POSIX `bin/dowel` wrapper and pre-compile the AOT cache under `.dowel/cli-bundle/` for about 100 ms startup. | none | no | | `install` | Scaffold the canonical native Flutter consumer wrapper (`bin/dispatcher.dart` plus `lib/app/_plugins.g.dart` plus `lib/app/commands/_index.g.dart`) and chain `make:fast-cli` in-process. | none | no | ## Plugin management Three commands that maintain the `.dowel/plugins.json` registry and its generated barrel `lib/app/_plugins.g.dart`. Atomic writes (`.tmp` plus rename) across every mutation, so concurrent readers (editors, lint daemons) never observe partial state. `plugin:install` routes through two modes (manifest, direct registration) depending on whether the plugin ships an `install.yaml`. | Command | Description | Boot Mode | MCP Tool | |:---|:---|:---|:---| | `plugin:install` | Register a third-party dowel plugin (`install.yaml` manifest preferred; direct registration into the dowel scaffold otherwise). | none | no | | `plugin:uninstall` | Uninstall a third-party dowel plugin (reverses the install manifest). | none | no | | `plugins:refresh` | Regenerate `lib/app/_plugins.g.dart` from `.dowel/plugins.json` registry. | none | no | ## MCP Three commands: `mcp:serve` runs the dowel MCP server, and `mcp:install` and `mcp:uninstall` wire it into a host like Claude Code, Cursor, or Windsurf. The `mcp:*` meta commands recurse into the server and are intentionally excluded from the allowlist. Per-tool visibility is configured via `.dowel/mcp.json`, environment variables, and CLI flags (the three layers compose Cargo-style: file plus env plus flags, with deny rules always winning). | Command | Description | Boot Mode | MCP Tool | |:---|:---|:---|:---| | `mcp:serve` | Run the dowel MCP server (stdio JSON-RPC) for Claude Code, Cursor, Windsurf. | none | no | | `mcp:install` | Add the dowel MCP server entry to `.mcp.json` (idempotent; preserves other server entries). | none | no | | `mcp:uninstall` | Remove the dowel MCP server entry from `.mcp.json` (preserves other entries). | none | no | ## Introspection Four commands that observe the substrate or its host. `tinker` is the only command in the catalogue with `CommandBoot.connected`: it dials the running app's VM Service. It is still exposed as `dowel_tinker` (its `--eval` one-shot path drives the MCP tool); `help` is the one command here outside the MCP allowlist. | Command | Description | Boot Mode | MCP Tool | |:---|:---|:---|:---| | `help` | Show detailed help for a single command. | none | no | | `list` | List every registered command grouped by namespace. | none | dowel_list | | `doctor` | Run environment preflight checks (flutter, dart, port availability). | none | dowel_doctor | | `tinker` | Connected REPL into the running Flutter app (Dart expression evaluation via VM Service). | connected | dowel_tinker | ## Code generation One command that rewrites the `lib/app/commands/_index.g.dart` barrel. Codegen sits in its own bucket because the consumer rarely runs it by hand: `make:command` and `plugin:install` invoke it as a follow-up step. The generated barrel is never edited manually; the source of truth is the set of `*_command.dart` files under `lib/app/commands/`. | Command | Description | Boot Mode | MCP Tool | |:---|:---|:---|:---| | `commands:refresh` | Rescan `lib/app/commands/` and rewrite the auto-discovery index. | none | no | ## Boot mode at a glance Twenty-one of the twenty-two commands run with `CommandBoot.none`. Only `tinker` is `CommandBoot.connected`, because its job is to evaluate Dart expressions against a live VM Service isolate. Everything else (lifecycle commands included) shells out to `flutter run` or operates on disk; the dowel process itself never depends on a Flutter binding. This separation is what lets the substrate ship as a pure-Dart package: the consumer wrapper supplies the optional Magic / Flutter context, while the framework itself stays runtime-free. A third boot mode, `headless`, is reserved for a V1.x release that will let Magic-bound commands (think `migrate`, `db:seed`) boot just enough of the host to read `Magic.Config` without spinning up a UI. ## Featured deep-dives Seven commands earn their own pages because their flags, modes, or composition rules outgrow a single table row: - [start](https://anilcancakir.com/dowel/docs/commands/start.md) - [plugin:install](https://anilcancakir.com/dowel/docs/commands/plugin-install.md) - [mcp:serve](https://anilcancakir.com/dowel/docs/commands/mcp-serve.md) - [make:plugin](https://anilcancakir.com/dowel/docs/commands/make-plugin.md) - [make:fast-cli](https://anilcancakir.com/dowel/docs/commands/make-fast-cli.md) - [install](https://anilcancakir.com/dowel/docs/commands/install.md) - [tinker](https://anilcancakir.com/dowel/docs/commands/tinker.md) Slug rule: the deep-dive URL drops the `:` separator in favour of `-`, so `plugin:install` lives at `commands/plugin-install/`, `mcp:serve` at `commands/mcp-serve/` and `make:fast-cli` at `commands/make-fast-cli/`. The remaining fifteen commands share this index page; reach for `dart run dowel help ` for their full flag surface. ## Related Documentation - [Getting Started](https://anilcancakir.com/dowel/docs/getting-started/index.md): what dowel is and how the pieces fit. - [MCP Tool Reference](https://anilcancakir.com/dowel/docs/mcp/tool-reference.md): the ten substrate tools these commands map to. - [Signature DSL](https://anilcancakir.com/dowel/docs/reference/signature-dsl.md): how a command declares its arguments and flags. --- # install Scaffold the canonical Flutter consumer project structure and install the fast-CLI wrapper in a single step. Running `install` produces `bin/dispatcher.dart`, the two generated barrels, and `bin/dowel`, then injects `dowel` into `pubspec.yaml` so every future `plugin:install` and `make:command` invocation resolves cleanly. - [Prerequisites](#prerequisites) - [Basic Usage](#basic-usage) - [Synopsis](#synopsis) - [What It Writes](#what-it-writes) - [Pubspec Injection](#pubspec-injection) - [Idempotency](#idempotency) - [Fast-CLI Auto-Chain](#fast-cli-auto-chain) - [Examples](#examples) - [Related Documentation](#related-documentation) --- ## Prerequisites Add `dowel` to the project before invoking `install`. The package needs to resolve from `pubspec.yaml` so the substrate CLI is discoverable via `dart run`: ```bash dart pub add dowel ``` Equivalent manual edit, then `dart pub get`: ```yaml dependencies: dowel: ^0.0.21 ``` `install` itself re-injects the dep at scaffold time (path-dep when `.dart_tool/package_config.json` resolves `dowel` to a relative `rootUri`, otherwise `dowel: any`), so the pub-add step is idempotent with the scaffold's own pubspec writes. --- ## Basic Usage ```bash dart run dowel install ``` Run once in a new Flutter project root. The command reads the project name from `pubspec.yaml`, writes the four artefacts listed below, and prints a summary. After this completes, use the consumer wrapper alias: ```bash dart run dowel ``` or, after the fast-CLI build finishes: ```bash ./bin/dowel ``` --- ## Synopsis Full signature as declared in `lib/src/commands/install_command.dart`: ``` install {--force : Overwrite files even when they already exist} ``` | Flag | Effect | |:---|:---| | `--force` | Overwrite `bin/dispatcher.dart`, the two generated barrels, and `bin/dowel` even when they are already present. Does not re-inject already-present pubspec entries. | --- ## What It Writes `install` produces four artefacts in this order: ### 1. `bin/dispatcher.dart` A generated CLI entry point that wires the consumer's own commands and plugin providers into the dowel runtime. The file is written from the `dispatcher.dart.stub` template with `{{ name }}` substituted to the `pubspec.yaml` project name. Header comment: `GENERATED by 'install' command; DO NOT EDIT. Regenerate via 'make:command' or 'plugins:refresh'.` Structure: - imports `package:/app/commands/_index.g.dart as consumer_commands` - imports `package:/app/_plugins.g.dart as plugins` - defines `_ConsumerCommandsProvider extends DowelServiceProvider` registering the consumer barrel - `main(List args)` calls `runDowel(args, baseProviders: [_ConsumerCommandsProvider(), ...plugins.autoDiscoveredProviders()], delegateToConsumer: false, collectMcpTools: args.isNotEmpty && args.first == 'mcp:serve')` The `collectMcpTools` parameter is conditionally enabled only when the command is `mcp:serve`, so plugin providers' MCP tools register for the server without overhead on non-MCP CLI invocations. ### 2. `lib/app/_plugins.g.dart` The plugins codegen barrel. Written as an empty-but-valid barrel on first install. `plugin:install` and `plugins:refresh` regenerate this file when plugins are added or removed. Never edit by hand: the source of truth is `.dowel/plugins.json`. ### 3. `lib/app/commands/_index.g.dart` The commands codegen barrel. Written as an empty-but-valid barrel on first install. `make:command` and `commands:refresh` regenerate this file as new command files appear under `lib/app/commands/`. Never edit by hand. ### 4. `bin/dowel` (via Fast-CLI Auto-Chain) A POSIX shell wrapper written by the `make:fast-cli` sub-command, which `install` calls in-process at the end of the scaffold phase. See [Fast-CLI Auto-Chain](#fast-cli-auto-chain) for details. --- ## Pubspec Injection `install` injects a `dowel` dependency into `pubspec.yaml` after writing the four artefacts. Two injection modes: **Path-dep mode (monorepo)** When `.dart_tool/package_config.json` lists `dowel` with a *relative* `rootUri` (sibling-package monorepo workflow), `install` resolves the path against the consumer root and writes a `path:` dependency: ```yaml dependencies: dowel: path: ../dowel ``` A `file://` absolute URI or any other absolute `rootUri` (the shape pub-cache produces) skips this branch and falls through to pub.dev mode. **Pub.dev mode** When no local relative-rootUri entry resolves, `install` injects the unconstrained version form so the next `dart pub get` (or `flutter pub get`) pulls the published package: ```yaml dependencies: dowel: any ``` Run `dart pub get` (or `flutter pub get`) after `install` completes to resolve to a concrete version. Tighten the constraint by hand (e.g. `^0.0.21`) once the consumer has chosen a baseline. The injection is idempotent: if `pubspec.yaml` already contains a `dowel:` key under `dependencies:`, neither form is written again (regardless of `--force`). --- ## Idempotency Re-running `install` without `--force` is a no-op when the artefacts already exist. Each file goes through a `_shouldWrite(path, force)` guard: if the file exists and `force` is false, the write is skipped and a `[skip]` line is printed for that artefact. The pubspec injection is always idempotent (key presence check, independent of `--force`). Pass `--force` to overwrite all four artefacts unconditionally. Useful after renaming the project or updating the stub template to a newer version: ```bash dart run dowel install --force ``` `--force` is forwarded to the `make:fast-cli` auto-chain as well, so `bin/dowel` is rewritten and the AOT binary is recompiled from scratch. --- ## Fast-CLI Auto-Chain At the end of `scaffoldInto`, `install` calls `MakeFastCliCommand.scaffoldInto(root, force, ctx)` in-process. This produces `bin/dowel`, compiles the AOT binary under `.dowel/cli-bundle/`, and patches `.gitignore`. The call happens only after all scaffold writes and pubspec injection succeed, so a partial failure in the scaffold phase does not leave a compiled binary that references a missing dispatcher. The fast-CLI step is not separately gated: if `dart build cli` fails (e.g. the Dart SDK is too old to support it, the toolchain is missing, or the build itself errors), `make:fast-cli` returns a non-zero exit code and `install` propagates that failure. The scaffold artefacts written before the failure remain on disk so the project is still usable via `dart run dowel ` (the slower fallback). Re-running `install --force` after fixing the underlying issue recompiles `bin/dowel` and the AOT cache. See [make:fast-cli](https://anilcancakir.com/dowel/docs/commands/make-fast-cli.md) for the full performance profile, cache-hit behaviour, and POSIX-only caveat. --- ## Examples ### 1. Fresh install in a new Flutter project ```bash flutter create my_app cd my_app dart pub add dowel dart run dowel install ``` Output: ``` write bin/dispatcher.dart write lib/app/_plugins.g.dart write lib/app/commands/_index.g.dart inject pubspec.yaml (dowel: any) write bin/dowel (Building dowel CLI, one-time ~8s...) Done. Run './bin/dowel status' to verify. ``` ### 2. Force-overwrite after project rename ```bash dart run dowel install --force ``` Regenerates `bin/dispatcher.dart` with the updated project name and recompiles `bin/dowel`. ### 3. Native-asset hooks note `install` uses `dart build cli` (via the `make:fast-cli` auto-chain) to compile `bin/dispatcher.dart` into an AOT binary. This requires Dart 3.5 or later because transitive dependencies may declare `hook/build.dart` build hooks. On earlier SDKs, `bin/dowel` is not written but the rest of the scaffold completes normally. Use `dart run dowel ` as the fallback invocation form on those environments. ### 4. Monorepo path-dep install In a monorepo where `dowel` lives at `../dowel`, run `flutter pub get` once to populate `.dart_tool/package_config.json`, then: ```bash dart run dowel install ``` The path-dep form is detected automatically and written to `pubspec.yaml`. No flag needed. --- ## Related Documentation - [make:command](https://anilcancakir.com/dowel/docs/commands/index.md): scaffold a single `DowelCommand` subclass under `lib/app/commands/` and regenerate `_index.g.dart`. - [plugin:install](https://anilcancakir.com/dowel/docs/commands/plugin-install.md): register a third-party plugin after `flutter pub add`; requires the `lib/app/_plugins.g.dart` barrel that `install` writes. - [make:fast-cli](https://anilcancakir.com/dowel/docs/commands/make-fast-cli.md): the sub-command auto-chained by `install`; run it standalone to rebuild `bin/dowel` without touching the scaffold artefacts. - [Getting Started: Installation](https://anilcancakir.com/dowel/docs/getting-started/installation.md): step-by-step walkthrough from `dart pub add` to first `./bin/dowel status` for both vanilla Flutter and magic-stack projects. --- # start The `start` command spawns `flutter run` as a detached background process, scrapes the VM Service URI from its output, and records the session so that every downstream tool (MCP server, `stop`, `restart`, `tapir:snap`, `tinker`, etc.) knows where the running app lives. - [Basic Usage](#basic-usage) - [Synopsis](#synopsis) - [Options](#options) - [Behaviour](#behavior) - [State File](#state-file) - [Examples](#examples) - [Troubleshooting](#troubleshooting) - [Related Documentation](#related-documentation) The session lives in a directory of its own, **per project**: `~/.dowel/sessions//state.json`, with the log and the FIFO beside it. It used to be one global `~/.dowel/state.json`, so a second project's `start` silently took the slot and every command from the first project then drove the second app, succeeding each time. That file is still written, as a pointer to whichever session started last, because hand-writing it is the documented recovery recipe when `start` cannot boot an app. Two projects at once still need distinct `--port`, `--vm-service-port` and `--cdp-port` values; `start` fails fast when one is taken. Override the session path with `--state=` or `DOWEL_STATE_FILE` when you need to name it explicitly. ## Basic Usage ```bash dart run dowel start ``` Launches a Chrome web dev session on port `3100` with the VM Service listener on port `8181`. The command exits as soon as the URI is confirmed; the Flutter process continues running in the background. ## Synopsis ``` dart run dowel start [--device=] [--port=] [--vm-service-port=] [--[no-]dds] [--[no-]profile-static] [--cdp-port=] [--timeout=] [--flutter-arg=]... ``` `start` accepts no positional arguments. All configuration is done via named options and flags declared in `configure(ArgParser)` in `lib/src/commands/start_command.dart`. ## Options | Option | Type | Default | Description | |:---|:---|:---|:---| | `--device` | string | `chrome` | Flutter device target. Accepts `chrome`, `macos`, `linux`, `windows`, an iOS UDID, or an Android serial number. | | `--port` | int | `3100` | Web server port passed as `--web-port` to `flutter run`. Ignored for non-Chrome targets. | | `--vm-service-port` | int | `8181` | VM Service listener port. Recorded in `state.json`; used by `mcp:serve` and connected-mode tools to open the WebSocket. | | `--dds` | flag | `false` | Enable Dart Development Service. When absent (default), `--no-dds` is forwarded to `flutter run` so tapir and tinker connect directly to the VM Service. | | `--profile-static` | flag | `false` | Start a static-profile run. Sets `profile: "static"` in `state.json`; otherwise `"debug"`. On a device (Android, iOS, desktop) it also forwards `--profile` to `flutter run`, once even when `--flutter-arg=--profile` is passed too; there is no hot reload in that mode. On Chrome it stays a label, because a web profile build has no VM Service. `restart` replays it from `state.json`; `restart --no-profile-static` drops it. | | `--cdp-port` | int | (none) | Chrome DevTools Protocol port. When set, `start` pre-launches Chrome with `--remote-debugging-port=` and runs Flutter on `-d web-server`, recording `chromePid` / `tmpProfileDir` / `cdpPort` in `state.json`. Required for `tapir:resize` / `tapir:device`. Only valid with `--device=chrome` or `--device=web-server`, and requires Flutter SDK 3.30.0 or newer. A subsequent `restart` preserves this port. The port is probed before Chrome launches; if it is already in use you receive a clear error with the `--cdp-port` hint instead of a misleading "Is Chrome installed?" message. | | `--timeout` | int | `90` | Seconds to wait for the VM Service URI to appear in the `flutter run` log. Increase on cold starts where build and DartDev initialisation takes longer than the default (common on first run after a clean Flutter SDK install or on low-powered CI). On the plain branch a timeout while `flutter run` is still alive exits `75` and leaves the session recording (see [Troubleshooting](#troubleshooting)). Applies to the `--cdp-port` branch and to the plain one alike; on the `--cdp-port` branch it also bounds the wait for the web server's `is being served at` line, which used to be a fixed 60 seconds and failed a web compile that ran long under load. | | `--flutter-arg` | string | (none) | Extra argument forwarded verbatim to `flutter run`, repeatable. Use it for anything `start` has no flag of its own for: `--flutter-arg=--dart-define=KEY=VALUE`, `--flutter-arg=--flavor=dev`, `--flutter-arg=--web-renderer=html`. Forwarded AFTER the arguments `start` builds, so a repeated flag overrides the default it chose. A value containing commas is kept whole (`splitCommas` is off), so `--flutter-arg=--dart-define=TAGS=a,b` reaches Flutter as one argument. Recorded in `state.json` as `flutterArgs` and replayed by `restart`; `restart --flutter-arg=` replaces the whole carried set, which is also how to clear one. | ## Behaviour **FIFO stdin channel.** Before launching Flutter, `start` calls `mkfifo` to create a named pipe at `~/.dowel/sessions//flutter-dev.fifo`. A background `tail -f /dev/null` process holds the write end of the FIFO open permanently, preventing `flutter run`'s stdin from receiving EOF when no keystroke sender is active. `flutter run` is then launched with the FIFO as its stdin, so the `reload` and `hot-restart` commands can send `r` or `R` into the pipe and trigger flutter_tools' own handler. This approach works on every device target (web, desktop, mobile) because it routes through `flutter_tools` rather than raw VM Service RPCs, which the Chrome `dwds` bridge rejects. **Detached spawn and URI scrape.** The shell one-liner run by `start` launches both background processes (`tail` holder and `flutter run`) and echoes their PIDs in `HOLDER=` / `FLUTTER=` format. `start` captures those PIDs from the wrapper's stdout, then polls the log file at `~/.dowel/sessions//flutter-dev.log` every 250 ms until it finds a line matching either the web format (`Debug service listening on ws://...`) or the desktop/mobile format (`Dart VM Service on is available at: http://...`). `http://` and `https://` URIs are normalised to their `ws://` and `wss://` equivalents and a `/ws` suffix is appended when missing. **State file write.** After the URI is confirmed, `start` writes this project's session state atomically (`.tmp` + rename) with the full process inventory: PIDs, FIFO path, VM Service URI, web port, device target, profile mode, project root, and a UTC `startedAt` timestamp. The MCP server (`mcp:serve`) reads this file at startup to discover the running app; `stop` reads it to find the app to stop; `status` reads it to report the live process. **One process group per app.** The wrapper shell is spawned in Dart's detached mode, which calls `setsid()`, so the `tail` holder, the flutter tool and every child the tool starts (`frontend_server`, the web server, `adb` or `iproxy` helpers) share a process group of their own, separate from the caller's. Chrome, launched detached by `--cdp-port`, gets its own group the same way. `stop`, `restart` and a failed `--cdp-port` start stop an app through that group: 1. Check that the recorded pid is still the process `start` spawned: `stop` compares its age (`ps -o etime=`) against the session's `startedAt`, and a pid whose process started later is a reused number, left alone. 2. Read the group id from the live pid (`ps -o pgid= -p `), or from the FIFO holder when the tool is gone and a child of it may not be. It is not stored, because once every member exits the number is free for reuse. A pid that shares the caller's own group is signalled alone, never its group. 3. Send `SIGTERM` to the group and wait up to 5 seconds for every member to exit. A zombie does not count as a member; a process listing that fails counts as a live one. 4. Send `SIGKILL` to the group if any member is left, and wait up to 5 seconds again. 5. Wait up to 5 seconds more for the ports the app held to come free: the web port of a browser session, the CDP port for Chrome. A port that stays bound is reported, never answered with `SIGKILL`: once the group is gone, whatever holds it is not the app. The command returns as soon as all of it is gone (about 15 seconds at worst per group, 30 with Chrome), or reports what is left when the budget runs out. On a host where `ps` cannot run (a slim container image) or prints an elapsed time in an unknown shape, the group cannot be found: the pid alone gets `SIGTERM`, as before process groups, nothing is waited on, and a warning says its children may outlive it. A `SIGTERM` to the tool pid alone, which is what these paths used to send, ended the tool and left `frontend_server` running under pid 1, still compiling; returning straight after the signal let the next `start` race a port the old app still held. ## State File `start` writes the session atomically, once as soon as the child PIDs are known and again when the VM Service URI lands. The schema (from `lib/src/state/state_file.dart:13-24`): ```json { "pid": 12345, "stdinPipe": "/Users/you/.dowel/sessions/3f9a1c5e7b20/flutter-dev.fifo", "stdinHolderPid": 12344, "vmServiceUri": "ws://127.0.0.1:8181/AbCdEfGhIjK=/ws", "webPort": 3100, "vmServicePort": 8181, "startedAt": "2026-05-19T10:00:00.000Z", "profile": "debug", "projectRoot": "/Users/you/Code/my-app", "device": "chrome", "chromePid": null, "tmpProfileDir": null, "cdpPort": null } ``` Field reference: | Field | Type | Notes | |:---|:---|:---| | `pid` | int | PID of the `flutter run` process. `stop` reads its process group from it and stops the whole group. | | `stdinPipe` | string | Absolute path to the FIFO. `reload` and `hot-restart` write `r\n` / `R\n` here. | | `stdinHolderPid` | int | PID of the `tail -f /dev/null` holder that keeps the FIFO write-end open. It shares the tool's process group, so it stops with it; `stop` also signals it directly. | | `vmServiceUri` | string | Canonical `ws://host:port//ws` URI. All connected-mode tools open this WebSocket. | | `webPort` | int | `--web-port` value forwarded to Flutter. Chrome only; ignored for other targets. | | `flutterArgs` | list | The `--flutter-arg` values this session was started with. Absent when there were none. `restart` reads it so the relaunched app keeps the build configuration the running one had; a dropped define compiles clean and behaves differently, where a dropped port at least refuses to bind. | | `vmServicePort` | int | Informational; the port embedded in `vmServiceUri`. | | `startedAt` | string | ISO 8601 UTC timestamp of the `start` invocation. | | `profile` | string | `"debug"` or `"static"` (set by `--profile-static`). `restart` reads it so a profile session comes back as a profile build. | | `projectRoot` | string | Absolute path to the working directory at invocation time. | | `device` | string | The `--device` value passed to this run. | | `chromePid` | int or null | PID of the Chrome process pre-launched by `--cdp-port`; `null` on non-CDP runs. `stop` reaps it. | | `tmpProfileDir` | string or null | Chrome temporary profile directory created by `--cdp-port`; `null` otherwise. `stop` deletes it. | | `cdpPort` | int or null | The `--cdp-port` value when CDP is enabled; `null` otherwise. Preserved across `restart` (forwarded into the next `start` before `stop` deletes this file). | ## Examples **Chrome web session (default):** ```bash dart run dowel start ``` Equivalent to `dart run dowel start --device=chrome --port=3100 --vm-service-port=8181`. **macOS desktop session:** ```bash dart run dowel start --device=macos ``` `--port` is ignored for non-Chrome targets. Flutter receives `--no-dds` (the default) so the Tapir and Tinker tools connect directly to the VM Service socket. **Custom web port with DDS enabled:** ```bash dart run dowel start --device=chrome --port=4000 --dds ``` Starts the Chrome session on port `4000` and omits `--no-dds`, letting the Dart Development Service run. Use this when your tooling requires DDS (for example, a secondary IDE debugger). **CDP session for `tapir:resize` / `tapir:device`:** ```bash dart run dowel start --device=chrome --cdp-port=9222 ``` Pre-launches Chrome with `--remote-debugging-port=9222` and runs Flutter on `-d web-server`, recording `chromePid` / `tmpProfileDir` / `cdpPort` in `state.json` so `tapir:resize` and `tapir:device` can drive the Chrome DevTools Protocol. Requires Flutter SDK 3.30.0 or newer. A subsequent `restart` preserves the CDP port. **CDP session with extended VM Service timeout (cold CI or slow machine):** ```bash dart run dowel start --device=chrome --cdp-port=9222 --timeout=180 ``` Allows up to 180 seconds for the VM Service URI to appear in the log. Use this when the default 90 seconds is not enough for a cold Flutter SDK install or a slow CI host. ## Troubleshooting **Port already in use (`--port` collision).** If another process holds the web port, `flutter run` exits immediately and the log file shows `Error: Address already in use`. Pick a free port with `--port=` or stop the occupying process with `lsof -ti: | xargs kill`. **`mkfifo` permission denied.** `start` creates the FIFO at `~/.dowel/sessions//flutter-dev.fifo`. If `~/.dowel/` was created by a previous run with different ownership or mode bits, `mkfifo` fails. Fix the owner or mode of that session directory (`chmod -R u+rwx ~/.dowel/sessions/`), or remove only that project's directory (`rm -rf ~/.dowel/sessions/`) and run `dart run dowel start` again; the sessions of your other projects stay untouched. Note: `start` is POSIX-only (macOS/Linux). It will not work on Windows. **VM Service URI never appears (timeout).** When `--timeout` runs out before the URI shows up in the log, `start` checks whether `flutter run` is still alive. - **Still running (exit 75).** A first native build (CocoaPods, SwiftPM, Gradle) routinely takes longer than 90 seconds, and the app keeps building after `start` returns. `start` prints `flutter run (pid ) is still building after s ...` and exits `75` (`EX_TEMPFAIL`, `DowelCommand.exitTryAgain`), so a script can tell "not ready yet" from a failure. The session stays recorded with `booting: true`; `status` shows it, and the next connected command reads the URI from the log once the build finishes. A connected command run before that point also exits `75` while the recorded `flutter run` is alive, so a script can retry on the one code until it succeeds. Pass `--timeout=600` to make `start` itself wait for a cold build. - **Exited (exit 1).** `flutter run` died before announcing a URI, almost always a build or launch error. `start` prints `flutter run (pid ) exited before announcing a VM Service URI` with the log path, stops the FIFO holder and removes the session (the log stays), so `status` does not report a dead app as booting. Common causes in `~/.dowel/sessions//flutter-dev.log`: a wrong `--device` value, a missing `CHROME_EXECUTABLE` env var in headless environments, or a compile error in the app's entry point. On the `--cdp-port` branch a timeout still fails the start with exit 1 and reaps Chrome and the flutter tool, since a web build has no long native phase to wait out. **Connected command reports an unregistered extension (exit 75).** A connected command that calls `ctx.callExtension('ext.')` before the app has registered that extension (the app is still booting, it is a release build, or the plugin is not installed in it) prints one line, `ext. is not registered in the running app yet (...). Retry in a few seconds.`, and exits `75`. The JSON-RPC `-32601` error reaches the command as `ExtensionNotRegisteredException`, so a command that wants to wait for the extension can catch it without importing `package:vm_service`. **Web server not ready in time (`--cdp-port`).** On the `--cdp-port` branch `start` waits for the `is being served at` line before it navigates Chrome, and fails with `Timed out after s waiting for "is being served at"`. The deadline is `--timeout`; raise it when the web compile is slow. The failed start stops the flutter tool's and Chrome's process groups and waits for their ports, so a retry straight after it does not collide with the leftovers. **CDP port already in use (`--cdp-port` collision).** When the configured CDP port is held by another process, `start` exits immediately before launching Chrome and emits: `CDP port is already in use; pass --cdp-port or free it before running start.` Choose a free port (for example `--cdp-port=9224`) or kill the occupying process with `lsof -ti: | xargs kill`. ## Related Documentation - [stop](https://anilcancakir.com/dowel/docs/commands/index.md): stop the running Flutter process group and wait until it and its ports are gone (see [One process group per app](#behavior)), force-stop the app on an Android device (`adb -s shell am force-stop `, the id read from `android/app/build.gradle*`), and delete `state.json`. An app that outlives `SIGKILL` keeps its session and `stop` exits 1. - [restart](https://anilcancakir.com/dowel/docs/commands/index.md): full stop + start cycle; preserves the prior session's `--cdp-port` (read from `state.json` before `stop` deletes it, then forwarded into `start`). An explicit `--cdp-port` on the `restart` invocation wins. The start begins only after `stop` has seen the old app and its ports go, and a `stop` that fails aborts the restart. - [reload](https://anilcancakir.com/dowel/docs/commands/index.md): send `r\n` to the FIFO for a hot reload without a full restart. - [hot-restart](https://anilcancakir.com/dowel/docs/commands/index.md): send `R\n` to the FIFO for a hot restart that resets app state. - [mcp:serve](https://anilcancakir.com/dowel/docs/commands/mcp-serve.md): start the stdio JSON-RPC MCP server; reads this project's session to discover the running app. --- # tinker Connected REPL into the running Flutter app. Evaluates Dart expressions via the VM Service protocol and pretty-prints the result. Requires the app to be running before invoking (see [start](https://anilcancakir.com/dowel/docs/commands/start.md)). - [Basic Usage](#basic-usage) - [Synopsis](#synopsis) - [Connected Mode](#connected-mode) - [Two Modes](#two-modes) - [Examples](#examples) - [VM Service Evaluate](#vm-service-evaluate) - [MCP Surface](#mcp-surface) - [Related Documentation](#related-documentation) --- ## Basic Usage ```bash dart run dowel tinker ``` Drops you into an interactive read-eval-print loop connected to the running Flutter app. Type any Dart expression at the `>>>` prompt and press Enter. Press Ctrl+D or type `exit` / `quit` to end the session. ```bash dart run dowel tinker # via consumer wrapper (same effect) ``` --- ## Synopsis ``` tinker {--eval=expr} ``` | Option | Description | |:---|:---| | `--eval=` | Evaluate a single Dart expression and exit. REPL is skipped. | --- ## Connected Mode `tinker` sets `CommandBoot.connected` as its boot mode. Before `handle` runs, the dispatcher reads this project's session (written by `start`) and dials the VM Service WebSocket URI recorded there. If no state file is present, or the app is not running, the command exits with an error. Prerequisite: the Flutter app must be running via `dart run dowel start` (or the consumer wrapper equivalent) before any `tinker` invocation. The `status` command reports whether a state file is present and what VM Service URI it holds. --- ## Two Modes ### REPL (default) When `--eval` is not provided, `tinker` enters an interactive read-eval-print loop: 1. Prints `Tinker connected. Type expressions; Ctrl+D to exit.` 2. Displays a `>>>` prompt and reads one line at a time from stdin. 3. Evaluates the line in the running app's root library via VM Service. 4. Formats the result through the `Tinker.casters` chain and prints it. 5. Repeats until stdin closes (Ctrl+D) or the user types `exit` or `quit`. The session prints `Tinker session ended.` on exit. ### `--eval` (one-shot) When `--eval=` is supplied, `tinker` evaluates the expression once, writes the formatted result to stdout, and exits immediately. No prompt or session messages are printed. Exit code is `0` on success, `1` on evaluation error. This mode is pipe-friendly: scripts and pipelines can capture the output without filtering out interactive chatter. ```bash dart run dowel tinker --eval "1+1" # prints: 2 echo $? # 0 ``` --- ## Examples ```bash # Interactive REPL session dart run dowel tinker # >>> Magic.find().rxState.value # MonitorState{...} # >>> Ctrl+D # Tinker session ended. # One-shot eval (pipe-friendly) dart run dowel tinker --eval "DateTime.now().toIso8601String()" # 2026-05-19T10:30:00.000Z ``` --- ## VM Service Evaluate Expression evaluation routes through `VmServiceClient.evaluate`, which opens a WebSocket to the DDS endpoint recorded in the session. On each evaluation, the client calls `getVM()` for a fresh isolate ID (no cache, so device-target switches and app restarts do not produce stale isolate references), then resolves the isolate's root library ID and delegates to the VM Service `evaluate` RPC. Expressions containing `await` are automatically wrapped in an async IIFE (`(() async => )()`) so callers do not need to reason about await-awareness. The `InstanceRef` response is unwrapped and passed through the `Tinker.casters` chain for pretty-printing before it reaches stdout. --- ## MCP Surface dowel ships `dowel_tinker` as a substrate MCP tool. When `mcp:serve` runs, any MCP client (Claude Code, Cursor, Windsurf, etc.) can call `dowel_tinker` with an `eval` argument to evaluate Dart in the running app. This is the same evaluation path as `dart run dowel tinker --eval "..."` but accessible to AI agents over stdio JSON-RPC; the dispatcher uses the same lazy-reconnect to the VM Service that the CLI form uses, and the result returns as MCP text content. See [mcp:serve](https://anilcancakir.com/dowel/docs/commands/mcp-serve.md) and the [MCP tool reference](https://anilcancakir.com/dowel/docs/mcp/tool-reference.md) for the full input schema, filter precedence, and the substrate vs plugin separation. --- ## Related Documentation - [start](https://anilcancakir.com/dowel/docs/commands/start.md): boot the Flutter app and write the VM Service URI to the state file. - [status](https://anilcancakir.com/dowel/docs/commands/index.md): confirm the app is running and inspect the recorded state. --- # plugin:install Register a third-party dowel plugin into the consumer project. After adding a package to `pubspec.yaml` and running `flutter pub get`, `plugin:install` wires the plugin's `DowelServiceProvider` without requiring manual edits to `bin/dispatcher.dart` or `lib/app/_plugins.g.dart`. - [Basic Usage](#basic-usage) - [Synopsis](#synopsis) - [Two Routing Modes](#two-routing-modes) - [Examples](#examples) - [Idempotency](#idempotency) - [Reversibility](#reversibility) - [Related Documentation](#related-documentation) --- ## Basic Usage ```bash dart run dowel plugin:install ``` `` is the plugin's pubspec package name (e.g. `magic_logger`). The package must be listed in `pubspec.yaml` and resolved via `flutter pub get` before this command runs. Pre-flight checks: the package must appear in `pubspec.yaml`, and `.dart_tool/package_config.json` must contain the package name. Any failure exits with code `1` before any write is attempted. A plugin without an `install.yaml` also needs the scaffold from `dart run dowel install` (`lib/app/_plugins.g.dart`); without it the command exits with code `1` and tells you to run `install` first. --- ## Synopsis Full signature as declared in `lib/src/commands/plugin_install_command.dart`: ``` plugin:install {name : Plugin pubspec package name (e.g. magic_logger)} {--bootstrap-command= : Plugin install sub-command to chain after registration} {--use-yaml-only : Fail if install.yaml not found instead of registering the plugin directly} {--force : Bypass conflict detection (inherited from DowelInstallCommand)} {--dry-run : Print staged ops without writing (inherited)} {--non-interactive : Skip prompts, use defaults (inherited)} {--no-bootstrap : Skip post-install bootstrap hint message (inherited)} ``` The four inherited flags come from `DowelInstallCommand.baseFlags` and are available on every install command, not only `plugin:install`. | Flag | Effect | |:---|:---| | `--bootstrap-command=cmd` | Overrides the post-registration bootstrap hint (e.g. `logger:install`). | | `--use-yaml-only` | Error out when no `install.yaml` found; no direct registration (Mode 2). | | `--force` | Bypass conflict detection; overwrite user-modified files. | | `--dry-run` | Print every staged op without touching disk. | | `--non-interactive` | Skip interactive prompts; use defaults. Required in non-TTY environments. | | `--no-bootstrap` | Suppress the post-install bootstrap hint message. | --- ## Two Routing Modes The core of `handle` dispatches through two code paths. Mode 1 wins when an `install.yaml` manifest is present; Mode 2 runs otherwise, and needs the scaffold barrel from `dart run dowel install`. ### Mode 1: install.yaml Manifest Flow **Condition**: `resolveInstallYaml(name)` returns a non-null path. The resolver checks `/install.yaml` first, then `/assets/install.yaml` (`resolveInstallYaml` in `lib/src/commands/plugin_install_command.dart`). `ManifestParser.parseFile` parses the YAML into an `InstallManifest`. `ManifestInstaller` stages `InstallOperation` objects on an `InstallTransaction`. `InstallTransaction.commit` writes every file to a `.tmp` path first; on full success, all `.tmp` files are renamed atomically. Any `.tmp` write failure deletes all staged temps and returns `Error(rolledBack: true)`. On `Success`, the plugin entry is written to `.dowel/plugins.json` and `PluginsRefreshCommand` regenerates `lib/app/_plugins.g.dart` in-process. Preferred authoring path: declarative, conflict-checked, dry-run-capable, and partially reversible via `plugin:uninstall` (V1 limitations; see Reversibility). ### Mode 2: Direct Registration **Condition**: No `install.yaml` found AND `lib/app/_plugins.g.dart` exists at the project root. Without that barrel the command exits with code `1` and asks you to run `dart run dowel install` first. `_registerDowelProvider` runs directly: writes a `PluginEntry` to `.dowel/plugins.json` and regenerates `_plugins.g.dart`. No wrapper edit occurs. Covers plugins without a manifest. Naming convention: `my_plugin` maps to `package:my_plugin/cli.dart` and class `MyPluginDowelProvider`. Pass `--use-yaml-only` to error out instead of reaching this path. --- ## Examples ### 1. Manifest-based plugin install (Mode 1) ```bash flutter pub add magic_logger dart run dowel plugin:install magic_logger ``` Output: ``` Success: applied 4 ops; record at .dowel/installed/magic_logger.json Bootstrap with: dowel logger:install ``` ### 2. Canonical scaffold, no install.yaml (Mode 2) ```bash flutter pub add awesome_plugin dart run dowel plugin:install awesome_plugin ``` Output: ``` Registered "awesome_plugin" via the dowel scaffold (no install.yaml needed). ``` ### 3. Dry-run preview ```bash dart run dowel plugin:install magic_logger --dry-run ``` Output: ``` DryRun: previewed 4 ops; no changes written WriteFile lib/config/logger.dart InjectImport lib/main.dart InjectAfterPattern lib/app/providers/app_service_provider.dart AddPubspecAsset assets/logging/ ``` --- ## Idempotency Re-running `plugin:install` for the same plugin is safe in both modes. `PluginsRegistryFile.addPlugin` replaces by name so `.dowel/plugins.json` always contains exactly one entry per plugin. `ConfigEditor.addImportToFile` and `insertCodeAfterPattern` skip injection when the target content is already present. All `InstallTransaction` file writes use `.tmp` + atomic rename: concurrent readers never observe partial state. A mid-commit `.tmp` write failure deletes all staged temps and returns `Error(rolledBack: true)` before any rename occurs. --- ## Reversibility `plugin:uninstall ` is the complement to `plugin:install`. A Mode 1 manifest commit writes `.dowel/installed/.json` listing every op and content hash. V1 reversibility is partial: `WriteFile` ops are fully reversed (tamper-check then delete); `InjectImport`, `InjectBeforePattern`, and `InjectAfterPattern` ops are logged as `[skipped]` (no anchor-bracketed markers in V1; operator must reverse by hand). Mode 2 installs write no record, so `plugin:uninstall` cannot auto-reverse them. Full bidirectional reversibility is planned for V1.1. --- ## Related Documentation - [plugin:uninstall](https://anilcancakir.com/dowel/docs/commands/index.md): removes a plugin registered via `plugin:install`, using the `.dowel/installed/.json` record. - [plugins:refresh](https://anilcancakir.com/dowel/docs/commands/index.md): regenerates `lib/app/_plugins.g.dart` from `.dowel/plugins.json` when the codegen barrel drifts out of sync. - [install.yaml schema](https://anilcancakir.com/dowel/docs/plugins/install-yaml.md): full reference for the declarative manifest format consumed by Mode 1. --- # make:plugin Scaffold a new `dowel` plugin skeleton under `packages//` (or a custom target directory). Produces a fully wired, immediately testable plugin package in a single command. - [Basic Usage](#basic-usage) - [Synopsis](#synopsis) - [Two Modes](#two-modes) - [8-Phase Pipeline](#8-phase-pipeline) - [Output Layout](#output-layout) - [Examples](#examples) - [Workspace Enrollment](#workspace-enrollment) - [Related Documentation](#related-documentation) --- ## Basic Usage ```bash dart run dowel make:plugin ``` `` must be a valid Dart package name: snake_case, starts with a lowercase letter, contains only `[a-z0-9_]`. Writes to `/packages//` by default and enrolls the new plugin into the parent Flutter app's pub workspace when one is detected. --- ## Synopsis ``` make:plugin {name} {--path=} {--target=} {--magic} {--bootstrap-command=} ``` | Argument / Option | Description | |:---|:---| | `name` | Plugin package name in snake_case (required). | | `--path=` | Target directory. Takes precedence over `--target` when both are set. | | `--target=` | Target directory (alias for `--path`). | | `--magic` | Scaffold a magic-aware plugin: adds `install.yaml`, Magic ServiceProvider, install/uninstall commands, and the `magic` dep to `pubspec.yaml`. Entirely optional. | | `--bootstrap-command=` | Override the default bootstrap command name (default: `:install`). | --- ## Two Modes ### Generic mode (default) Running without `--magic` produces a Magic-free plugin. Stubs sourced from `assets/stubs/make_plugin/generic/`: `pubspec.yaml`, `bin/.dart`, `lib/cli.dart`, `lib/.dart`, `lib/src/_dowel_provider.dart`, `test/_dowel_provider_test.dart`, `README.md`. The generic plugin exposes a single `DowelServiceProvider` subclass with an empty `mcpTools()` override. No dependency on `magic` or any other framework is added. ### Magic mode (`--magic`) When `--magic` is passed (or auto-detected because `magic:install` is in the running registry), four generic stubs are replaced by variants from `assets/stubs/make_plugin/magic/` (`pubspec.yaml`, `cli.dart`, `runtime.dart`, `provider.dart`), and six additional files are written from the same directory: | Stub | Written to | |:---|:---| | `install_command.dart` | `lib/src/commands/install_command.dart` | | `uninstall_command.dart` | `lib/src/commands/uninstall_command.dart` | | `install.yaml` | `install.yaml` | | `config_stub.dart` | `assets/stubs/install/_config.dart.stub` | | `install_command_test.dart` | `test/cli/install_command_test.dart` | | `service_provider.dart` | `lib/src/_service_provider.dart` | Magic mode wires a `ManifestInstaller`-compatible `install.yaml` manifest, a Magic-aware `ServiceProvider`, and install/uninstall commands with test scaffolds. The parent app's `plugin:install` can then resolve the manifest automatically. --- ## 8-Phase Pipeline 1. **Validate name.** Reject empty or non-snake_case names with an actionable error. Mirrors Dart's pubspec `name:` rule. 2. **Resolve target directory.** Flag precedence: `--path` wins over `--target`; both override the default `/packages//`. 3. **Spawn `flutter create --template=package`.** Lays down the baseline package: `pubspec.yaml`, `LICENSE`, `CHANGELOG.md`, `.gitignore`, `.metadata`, `analysis_options.yaml`, `lib/.dart`, `test/_test.dart`, `README.md`. 4. **Render generic stubs.** Seven files from `assets/stubs/make_plugin/generic/` overwrite `flutter create`'s defaults. Placeholders (`{{ name }}`, `{{ pascalName }}`, `{{ dowelPath }}`, `{{ commandPrefix }}`) are substituted at render time. In magic mode, four stubs are sourced from `assets/stubs/make_plugin/magic/` instead. 5. **Render magic add-ons (magic mode only).** Six additional files from `assets/stubs/make_plugin/magic/` are written. The `{{ magicPath }}` placeholder (relative path to the `magic` package root) is only available in this phase. 6. **Detect parent Flutter app.** `WorkspaceEnroller.detectParentFlutterApp` walks up from the target directory looking for a parent Flutter app `pubspec.yaml`. 7. **Enroll in workspace (when parent detected).** Adds `resolution: workspace` to the plugin's `pubspec.yaml` and appends the plugin's relative path to the parent app's `workspace:` list. 8. **Print success banner.** Reports the scaffolded path, `cd && dart pub get && dart test`, and whether the parent app's `pubspec.yaml` was modified. --- ## Output Layout ``` packages// ├── bin/ │ └── .dart # CLI entry point ├── lib/ │ ├── .dart # Runtime barrel │ ├── cli.dart # CLI barrel │ └── src/ │ ├── _dowel_provider.dart # DowelServiceProvider subclass │ ├── _service_provider.dart # [magic] Magic ServiceProvider │ └── commands/ │ ├── install_command.dart # [magic] │ └── uninstall_command.dart # [magic] ├── assets/stubs/install/ │ └── _config.dart.stub # [magic] config template ├── test/ │ ├── _dowel_provider_test.dart │ └── cli/ │ └── install_command_test.dart # [magic] ├── install.yaml # [magic] ManifestInstaller manifest ├── pubspec.yaml └── README.md ``` Items without `[magic]` are present in both modes. --- ## Examples **Generic plugin at the default location:** ```bash dart run dowel make:plugin magic_logger ``` Writes to `packages/magic_logger/`. Strips the `magic_` prefix: `commandPrefix=logger`, bootstrap command defaults to `logger:install`. **Magic-aware plugin:** ```bash dart run dowel make:plugin magic_logger --magic ``` Same layout plus `install.yaml`, `service_provider.dart`, install/uninstall commands, a config stub, and the `magic` path dependency in `pubspec.yaml`. **Plugin at a custom path:** ```bash dart run dowel make:plugin my_analytics --path=/workspace/plugins/my_analytics ``` Writes to the absolute path. Workspace enrollment still runs: if a parent Flutter app is found above that path, the plugin is enrolled. --- ## Workspace Enrollment When the target directory lives inside or adjacent to a Flutter application, `WorkspaceEnroller` walks the directory tree upward. On finding a parent `pubspec.yaml` that declares a Flutter dependency: 1. The plugin's `pubspec.yaml` gains `resolution: workspace`. 2. The parent app's `pubspec.yaml` gains a `workspace:` entry pointing to the plugin's relative path. After enrollment, a single `flutter pub get` in the parent app resolves all workspace members together. When no parent Flutter app is detected, the plugin is left as a standalone package and the success banner reports the outcome. --- ## Related Documentation - [make:command](https://anilcancakir.com/dowel/docs/commands/index.md): scaffold a single `DowelCommand` subclass inside an existing consumer project or plugin. - [plugins:refresh](https://anilcancakir.com/dowel/docs/commands/index.md): regenerate `lib/app/_plugins.g.dart` from `.dowel/plugins.json` after manually editing the registry. - [plugin:install](https://anilcancakir.com/dowel/docs/commands/plugin-install.md): register a third-party plugin (resolves `install.yaml` manifests produced by `make:plugin --magic`). --- # make:fast-cli Scaffold a POSIX shell wrapper (`bin/dowel`) and AOT-compiled dowel binary for about 100 ms startup, avoiding the 3-second `dart run` overhead with build hooks. - [Basic Usage](#basic-usage) - [Synopsis](#synopsis) - [What It Does](#what-it-does) - [Output](#output) - [Re-running](#re-running) - [Staleness Detection](#staleness-detection) - [Performance](#performance) - [Caveats](#caveats) - [Related Documentation](#related-documentation) --- ## Basic Usage ```bash dart run dowel make:fast-cli ``` Writes four files and directories: `bin/dowel` (POSIX shell wrapper, executable), `.dowel/cli-bundle/` (compiled binary + runtime libraries), `.dowel/build.stamp` (staleness marker), and patches `.gitignore` to ignore the cache. Caches the build result so subsequent invocations are cached (real time under 100ms on hit). --- ## Synopsis ``` make:fast-cli {--force : Overwrite bin/dowel even when it already exists} ``` | Option | Description | |:---|:---| | `--force` | Rewrite `bin/dowel` and recompile the binary, even if a cached version exists. | --- ## What It Does 1. **Validates the project structure.** Checks for `pubspec.yaml` and `bin/dispatcher.dart`; errors with actionable guidance if either is missing. 2. **Scaffolds the wrapper script.** Writes a POSIX `sh` script at `bin/dowel` that resolves symlinks via a portable `follow_links()` subshell and derives the project root at runtime. 3. **Computes a staleness key.** Captures the SHA256 hash of `pubspec.lock` and the Dart SDK version string, storing both in `.dowel/build.stamp` for later comparison. 4. **Compiles the binary.** Runs `dart build cli -t bin/dispatcher.dart -o .dowel/cli-bundle`, producing an AOT-native binary at `.dowel/cli-bundle/bundle/bin/dispatcher`. 5. **Patches `.gitignore`.** Appends `.dowel/` if not already present, ensuring the compiled cache is excluded from version control and never tracked. Idempotent. --- ## Output - **`bin/dowel`** (executable, ~50 LoC): shell wrapper that checks staleness, auto-recompiles if needed, then execs into the binary. - **`.dowel/cli-bundle/bundle/bin/dispatcher`**: AOT-compiled native executable. - **`.dowel/cli-bundle/bundle/lib/*.dylib`** (on macOS; `.so` on Linux): runtime libraries bundled with the binary. - **`.dowel/build.stamp`**: one-line file with `:`. Lets the wrapper detect when to rebuild. --- ## Re-running Run `make:fast-cli` again without `--force`: the wrapper detects `bin/dowel` already exists and skips the override. Run with `--force`: rewrites the wrapper and recompiles. The wrapper script itself is the source of truth for staleness detection. When the user runs `./bin/dowel ` and the Dart SDK updates or `pubspec.lock` changes, the wrapper notices (inside the lock-acquire section) and transparently rebuilds. No manual re-run needed. --- ## Staleness Detection The wrapper's `needs_build()` function triggers a rebuild when any of these conditions hold: 1. **Binary missing.** The compiled executable at `.dowel/cli-bundle/bundle/bin/dispatcher` does not exist. 2. **Stamp file missing or empty.** The `.dowel/build.stamp` file is absent or has zero size. 3. **Stamp file content mismatches.** The stored compile key (format: `:`) differs from the current key, meaning either the lock or the SDK changed. 4. **`pubspec.yaml` modified since last build.** The mtime of `pubspec.yaml` is newer than the stamp file's mtime, detected via the `-nt` (newer than) operator. 5. **`lib/app/_plugins.g.dart` modified since last build.** The mtime of `lib/app/_plugins.g.dart` is newer than the stamp file's mtime. This condition was added in 0.0.5 to invalidate the cache when `plugin:install` or `plugins:refresh` regenerates the file (issue #9 GAP A). Any match triggers a recompile inside the lock-acquire section. The re-check inside the lock prevents redundant builds when multiple invocations race. ### The build lock `.dowel/.dowel.lock/` is a directory created with an atomic `mkdir` and holds the owner's pid. It is held only while building: the wrapper removes it and clears its trap before `exec`ing the binary, because `exec` replaces the shell and a trap would never run, which used to leave a long-lived `mcp:serve` holding the lock for its whole life. A dead owner's lock is reclaimed through a `kill -0` probe; a live owner is waited on for at most `DOWEL_LOCK_TIMEOUT` seconds (default 600), after which the call fails and names the pid. A build compiles into `.dowel/cli-bundle.build.` and is swapped in by rename, so a binary another process is running is never overwritten; leftovers from a killed build are removed by the next build. --- ## Performance ### Cache hit (wrapper runs existing binary) ```bash time ./bin/dowel status # real 0m0.045s # user 0m0.020s # sys 0m0.015s ``` about 100 ms round-trip on a modern machine (no `dart run`, no "Running build hooks..." overhead). ### Cache miss (first run or staleness detected) ```bash time dart run dowel make:fast-cli # Building dowel CLI (one-time, ~5s)... # real 0m8.234s ``` ~8 seconds (depends on machine and dependency size). The wrapper prints "Building dowel CLI..." so users know a compile is in progress and not to interrupt. ### Comparison with `dart run` ```bash time dart run dowel status # real 0m3.100s # (includes "Running build hooks..." unless all hooks are cached) time ./bin/dowel status # real 0m0.045s # (cached; 68x faster) ``` --- ## Caveats **V1 POSIX-only (macOS + Linux).** Windows users continue to use `dart run dowel `. A `.cmd` variant is deferred to V1.x. **Requires Dart 3.5 or later.** The command uses `dart build cli`, which requires Dart 3.5+ because transitive dependencies may declare `hook/build.dart` via pub-provided build hooks. Earlier SDK versions fail with "dart compile does not support build hooks." **No cross-compilation.** The compiled binary targets the local machine architecture and OS. Distributing the binary to a different architecture or OS requires re-running `make:fast-cli` on that target. --- ## Related Documentation - [make:command](https://anilcancakir.com/dowel/docs/commands/index.md): scaffold a single `DowelCommand` subclass. - [make:plugin](https://anilcancakir.com/dowel/docs/commands/make-plugin.md): scaffold a full plugin package. - [install](https://anilcancakir.com/dowel/docs/commands/install.md): initialise a dowel consumer project structure. --- # mcp:serve Start the dowel MCP server over stdio JSON-RPC, exposing substrate and plugin-contributed tools to AI clients such as Claude Code, Cursor, and Windsurf. **Contents** - [Basic Usage](#basic-usage) - [Synopsis](#synopsis) - [Soft-Fail Behaviour](#soft-fail-behavior) - [3-Layer Filter](#3-layer-filter) - [Tools Surfaced](#tools-surfaced) - [Examples](#examples) - [Related Documentation](#related-documentation) --- ## Basic Usage ```bash ./bin/dowel mcp:serve ``` The server opens a stdio JSON-RPC channel, registers every tool that passes the active filter, and blocks until the MCP client disconnects. Exit code is `0` on a clean disconnect, `1` on a startup `StateError`. In normal usage the AI client launches the server automatically from your `.mcp.json` entry. The form above uses the fast-compiled binary; the standalone fallback is useful for debugging: ```bash dart run :dispatcher mcp:serve ``` --- ## Synopsis ``` ./bin/dowel mcp:serve [--include-package ]... [--exclude-package ]... [--include-tool ]... [--exclude-tool ]... ``` or, when `bin/dowel` is absent or on Windows: ``` dart run :dispatcher mcp:serve [--include-package ]... [--exclude-package ]... [--include-tool ]... [--exclude-tool ]... ``` All four flags are **repeatable**. Each can also be set via environment variable or `.dowel/mcp.json` (see [3-Layer Filter](#3-layer-filter)). | Flag | Effect | |:---|:---| | `--include-package ` | Accept only tools from the named package. CLI value replaces env and file allow lists for the package axis. | | `--exclude-package ` | Block all tools from the named package. Deny wins over allow across every layer. | | `--include-tool ` | Accept only the named tool. CLI value replaces env and file allow lists for the tool axis. | | `--exclude-tool ` | Block the named tool. Deny wins over allow across every layer. | --- ## Soft-Fail Behaviour The server does **not** require a running Flutter app at startup. When the session is absent or contains no `vmServiceUri`, initialisation completes normally: all tools register and the server stays online (source: `mcp_server.dart:137-149`). Tool calls that need the VM Service attempt a **lazy reconnect** on first invocation (source: `mcp_server.dart:228-256`): - Dispatch checks whether `_vmClient` is null. - If null, a single shared in-flight future reads the session, connects the WebSocket, and captures the main isolate ID. - Concurrent calls that arrive during the reconnect await the same future, preventing a client-leak race. - On failure, dispatch returns `isError: true` with an actionable message: `Run dowel start to launch the Flutter app, then retry the tool call.` - The guard clears in `finally` so the next call retries cleanly. **Note:** the server does NOT auto-reload filter configuration in V1. After editing `.dowel/mcp.json` or any `DOWEL_MCP_*` env var, run `/mcp reconnect dowel` in Claude Code (or restart the server process in other clients) to apply the change. --- ## 3-Layer Filter Tool visibility is controlled by three layers using **Cargo-style precedence** (source: `mcp_filter_config.dart:97-235`): | Layer | Source | Priority | |:---|:---|:---| | File | `.dowel/mcp.json` in project root | Lowest | | Env | `DOWEL_MCP_*` environment variables | Middle | | CLI | `--include-*` / `--exclude-*` flags | Highest | **Allow lists (replace):** a non-null value at a higher layer replaces the lower layer entirely for that axis. Resolution: `cli.allow ?? env.allow ?? file.allow`. Null at all three layers means "include all". **Deny lists (union):** the union of all three layers. A name denied at any layer is denied in the result; deny always wins over allow. ### File layer ```json { "packages": { "allow": null, "deny": ["crumbs"] }, "tools": { "allow": ["dowel_start", "dowel_tinker"], "deny": [] } } ``` An absent file is treated as "no opinion" (all tools pass). ### Env layer | Variable | Axis | |:---|:---| | `DOWEL_MCP_PACKAGES_ALLOW` | Package allow list (CSV) | | `DOWEL_MCP_PACKAGES_DENY` | Package deny list (CSV) | | `DOWEL_MCP_TOOLS_ALLOW` | Tool allow list (CSV) | | `DOWEL_MCP_TOOLS_DENY` | Tool deny list (CSV) | ### CLI layer Pass each `--include-*` / `--exclude-*` flag multiple times to accumulate entries. A non-empty `--include-*` replaces the env and file allow list for that axis. --- ## Tools Surfaced At initialise time the server merges two tool sets, then applies the active filter. **Substrate tools (10):** run in-process via the dowel command registry. Nine work without a running Flutter app; `dowel_tinker` dispatches over the VM Service and lazy-reconnects on first call. Their filter package name is `dowel`. | MCP Tool Name | Dowel Command | |:---|:---| | `dowel_start` | `start` | | `dowel_stop` | `stop` | | `dowel_status` | `status` | | `dowel_logs` | `logs` | | `dowel_restart` | `restart` | | `dowel_reload` | `reload` | | `dowel_hot_restart` | `hot-restart` | | `dowel_doctor` | `doctor` | | `dowel_list` | `list` | | `dowel_tinker` | `tinker` | Excluded from the substrate allowlist: `help`, `make:*`, `*:refresh`, `mcp:*`, `plugin:*`, `install`. These require a TTY, mutate source on disk, or recurse into the server itself (source: `mcp_server.dart:744-755`). **Plugin tools:** contributed via `DowelServiceProvider.mcpTools()` on each registered provider. The default returns an empty list. Providers such as `TapirDowelProvider` and `CrumbsDowelProvider` override it to expose tools that dispatch over the VM Service extension surface and require a running Flutter app. Both sets flow through the same filter, so a `--exclude-tool` deny against a plugin tool name behaves identically to a deny against a substrate name. The current plugin tool catalogues live on each plugin's own MCP tool reference site ([tapir](https://anilcancakir.com/tapir/docs/mcp/tool-reference), [crumbs](https://anilcancakir.com/crumbs/docs/mcp/tool-reference)). --- ## Examples ### 1. Filter via file: allow only specific packages ```json { "packages": { "allow": ["dowel", "tapir"], "deny": [] }, "tools": { "allow": null, "deny": [] } } ``` ```bash ./bin/dowel mcp:serve ``` Only substrate and Tapir tools register. Crumbs is excluded because its package is not in the allow list. ### 2. Filter via env vars: deny specific tools ```bash export DOWEL_MCP_PACKAGES_DENY="crumbs" ./bin/dowel mcp:serve ``` All Crumbs tools are excluded; all others remain available. The env deny is UNIONed with any deny entries already in `.dowel/mcp.json`. ### 3. Filter via CLI flags: pin to a minimal tool set ```bash ./bin/dowel mcp:serve \ --include-tool dowel_start \ --include-tool dowel_stop \ --include-tool dowel_tinker ``` Only the three named tools register. Any `tools.allow` in `.dowel/mcp.json` or `DOWEL_MCP_TOOLS_ALLOW` is ignored for this invocation (CLI replaces). --- ## Related Documentation - [mcp:install](https://anilcancakir.com/dowel/docs/commands/index.md): write or update the `.mcp.json` client entry. - [mcp:uninstall](https://anilcancakir.com/dowel/docs/commands/index.md): remove the `dowel` entry from `.mcp.json`. - [MCP setup guide](https://anilcancakir.com/dowel/docs/mcp/setup.md): end-to-end wiring for Claude Code, Cursor, Windsurf. - [MCP tool reference](https://anilcancakir.com/dowel/docs/mcp/tool-reference.md): full parameter and return value reference for every substrate and plugin tool. --- # MCP Integration Overview How `dowel` exposes its commands to AI clients as MCP tools: what MCP is, the ten substrate tools, the tools plugins contribute and the state file that ties a tool call to a running app. - [What is MCP](#what-is-mcp) - [Why MCP in dowel](#why-mcp-in-dowel) - [10 Substrate Tools](#substrate-tools) - [Plugin-Contributed Tools](#plugin-contributed-tools) - [State File Contract](#state-file-contract) - [Architecture Diagram](#architecture-diagram) - [Related Documentation](#related-documentation) --- ## What is MCP The [Model Context Protocol](https://modelcontextprotocol.io) (MCP) is an open standard that defines how AI clients (Claude Code, Cursor, Windsurf, and similar) discover and invoke tools exposed by a local or remote server over a JSON-RPC transport. A client connects once, calls `initialize` to receive the full tool catalogue, and then invokes individual tools by name. The server handles routing, argument validation, and error formatting, returning structured content the client model can reason over. MCP decouples the tool surface (what the agent can do) from the tool implementation (how it is done), so the same server can serve multiple AI clients without code duplication. --- ## Why MCP in dowel LLM agents working on Flutter codebases traditionally had one interaction mode: read source files and shell out to CLI commands. That works for static analysis and compilation, but it breaks down the moment the agent needs to observe or control a *running* Flutter application. Capturing a Semantics snapshot, tailing HTTP traffic, evaluating an expression in the live isolate, or hot-reloading after a code change all require a live connection to the Dart VM Service. Passing raw VM Service WebSocket URIs and protocol bytes through shell commands is fragile and opaque. dowel's MCP integration solves this by surfacing the running Flutter app's capabilities as first-class MCP tools. The agent calls `dowel_start` to launch the app, `dowel_reload` to push a hot reload, and plugin-contributed tools to snap the widget tree or tail HTTP requests, all without copy-pasting commands or parsing raw output. The MCP server handles VM Service discovery, lazy reconnect across stop-restart cycles, and structured error formatting so the model can self-correct. --- ## 10 Substrate Tools The MCP server always registers the following ten tools, derived from the dowel CLI's built-in command set. Tool names are normalised from `cmd:name` to `cmd_name` so they satisfy the MCP identifier constraint (`[a-zA-Z][a-zA-Z0-9_]*`). These tools run in-process via the dowel registry; nine of them require no VM Service connection (so `dowel_start` works even before any Flutter app is running), while `dowel_tinker` dispatches over the VM Service and lazy-reconnects on first call. | Tool | Maps to CLI command | Purpose | |:---|:---|:---| | `dowel_start` | `start` | Launch the Flutter app via `flutter run` and write `state.json` | | `dowel_stop` | `stop` | Stop the running Flutter process group, wait until it is gone, and delete `state.json` | | `dowel_status` | `status` | Read `state.json` and return the current process metadata as JSON | | `dowel_logs` | `logs` | Return the log captured from `flutter run` (`follow` tails it) | | `dowel_restart` | `restart` | Stop the running app and start it again, carrying the prior session's device and ports across (not the `R` hot restart, which is `dowel_hot_restart`) | | `dowel_reload` | `reload` | Hot reload (sends `r` to `flutter run`) without losing widget state | | `dowel_hot_restart` | `hot-restart` | Hot restart (sends `R` to `flutter run`), resetting ephemeral state | | `dowel_doctor` | `doctor` | Run preflight checks (`flutter --version`, `dart --version`, port 3100 free, Flutter SDK 3.30.0 or newer for `--cdp-port`) and report each as pass or fail | | `dowel_list` | `list` | List all registered dowel commands with their signatures | | `dowel_tinker` | `tinker` | Evaluate Dart expression in running app via VM Service. Maps to: tinker command. Requires running app (dowel_start first). | Commands intentionally excluded from the MCP allowlist: interactive commands (`help`), codegen commands (`make:*`, `*:refresh`), installer commands (`plugin:*`, `install`), and MCP meta commands (`mcp:*`). They either require a TTY, mutate source files better handled by the client's own file tools, or recurse into the MCP server itself. --- ## Plugin-Contributed Tools Beyond the ten substrate tools, plugins extend the MCP catalogue by overriding `DowelServiceProvider.mcpTools()`. The default implementation returns an empty list; plugins return a list of `McpToolDescriptor` instances that the MCP server collects at initialise time and registers alongside the substrate tools. The same `McpFilterConfig` allow/deny rules apply uniformly, so a deny rule against a plugin tool name works identically to `tools.deny: [dowel_start]`. Plugin tools dispatch through the VM Service (not in-process), so they require a running Flutter app. The MCP server lazy-reconnects on each dispatch call, meaning the agent can call `dowel_start` first and the very next plugin tool call picks up the new app automatically. **Plugin registration is explicit.** The consumer's `bin/dispatcher.dart` wrapper must list each provider in the `baseProviders` list it passes to `runDowel(...)` (auto-discovery via `lib/app/_plugins.g.dart` handles this for `plugin:install`-registered packages). Consumers invoke `./bin/dowel mcp:serve` (post-install via `mcp:install`) or `dart run :dispatcher mcp:serve` (Windows / no `bin/dowel` fallback) to start the MCP server with plugins collected. The `bin/mcp.dart` entry point (`dart run dowel:mcp`) loads only the substrate commands without plugin tools. The two sibling packages that ship production plugin tools are: | Package | MCP tool reference | |:---|:---| | `tapir` | [anilcancakir.com/tapir/docs/mcp/tool-reference](https://anilcancakir.com/tapir/docs/mcp/tool-reference) | | `crumbs` | [anilcancakir.com/crumbs/docs/mcp/tool-reference](https://anilcancakir.com/crumbs/docs/mcp/tool-reference) | Each package's provider is wired by the consumer (`TapirDowelProvider`, `CrumbsDowelProvider`) registered in the wrapper's `baseProviders` list. See the per-package CLAUDE.md for registration details and each plugin's tool reference site for the current tool catalogue. `magic_tinker` remains the CLI REPL host for `dart run dowel tinker`, but its MCP surface is now the substrate's `dowel_tinker`. --- ## State File Contract `dowel start` writes a JSON file per project at `~/.dowel/sessions//state.json` (the path is machine-local and user-scoped). The MCP server reads this file during `initialize` to discover the running Flutter app's VM Service WebSocket URI. If the file is absent or lacks a `vmServiceUri` key, the server stays online and registers all tools, but VM Service dispatch calls return an actionable error until the app is started. The state file holds one record per project: starting a second app in the same project overwrites the previous record, while another project gets a session of its own. It is never committed to version control (`.gitignore` / `.pubignore` exclude `.dowel/`). Fields (from `lib/src/state/state_file.dart`): | Field | Type | Notes | |:---|:---|:---| | `pid` | `int` | PID of the `flutter run` process | | `vmServiceUri` | `string` | Canonical `ws://host:port//ws` used for VM Service connection | | `webPort` | `int` | `--web-port` value passed to `flutter run` | | `vmServicePort` | `int` | Informational; defaults to 8181 | | `startedAt` | `string` | ISO 8601 UTC timestamp | | `profile` | `string` | `debug` or `static` | | `projectRoot` | `string` | Absolute path to the consumer project | | `device` | `string` | `chrome`, `macos`, `linux`, `windows`, or a device UDID | | `chromePid` | `int` or `null` | Chrome process PID when `--device=chrome` (D6 capture) | | `tmpProfileDir` | `string` or `null` | Temp Chrome profile directory path (D6 capture) | --- ## Architecture Diagram ``` Claude Code (MCP client) | | stdio JSON-RPC (initialize / tools/call) | ./bin/dowel mcp:serve (or: dart run :dispatcher mcp:serve) | McpServer (mcp:serve) | +-- substrate tools (dowel_start, dowel_stop, ...) | | | v | DowelRegistry (in-process command dispatch) | +-- plugin tools (tapir_*, crumbs_*) + dowel_tinker | | VM Service WebSocket | ws://localhost:PORT//ws | VmServiceClient | | ext.tapir.* / ext.crumbs.* / VM evaluate RPC | Flutter app isolate (debug mode) | +-------+-------+ | | TapirIntegration CrumbsIntegration (widget tree) (HTTP + logs + exceptions) ``` **Flow summary:** 1. The MCP client (Claude Code) connects to the server over stdio and calls `initialize`. 2. `McpServer` reads this project's session and opens a VM Service WebSocket to the running Flutter app. 3. All registered tools become available to the client model. 4. Substrate tool calls (prefix `dowel:`) route in-process through `DowelRegistry` without VM Service involvement, so `dowel_start` works even when no app is running. 5. Plugin tool calls dispatch through `VmServiceClient.callServiceExtension`, reaching the Flutter app's registered extension handlers in the debug isolate. 6. Results return as `CallToolResult` text content; errors carry `isError: true` with an actionable message so the model can self-correct without a human in the loop. --- ## Related Documentation - [Setup guide](https://anilcancakir.com/dowel/docs/mcp/setup.md): install the MCP server, configure `.dowel/mcp.json`, and wire provider registration in `bin/dispatcher.dart`. - [Tool reference](https://anilcancakir.com/dowel/docs/mcp/tool-reference.md): per-tool input schema, example calls, and error codes for all ten substrate tools, plus links to each plugin's own MCP tool reference site. --- # MCP Server Setup The dowel CLI ships with a built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that exposes your running Flutter app to AI coding assistants. The server speaks stdio JSON-RPC and surfaces tools contributed by every registered `DowelServiceProvider` (substrate commands plus Tapir, Crumbs, and any custom plugin). - [Without dowel MCP](#without-dowel-mcp) - [With dowel MCP](#with-dowel-mcp) - [Installation](#installation) - [Server Identity](#server-identity) - [Add a Rule](#add-a-rule) - [Automated Install](#automated-install) - [Available Tools](#available-tools) - [Related Documentation](#related-documentation) This page documents the per-client install matrix: the exact configuration snippet each MCP-compatible client needs to spawn `./bin/dowel mcp:serve` (or `dart run :dispatcher mcp:serve` as a fallback) against your project. The dowel MCP server is stdio-only in V1 (no remote HTTP endpoint); every client below launches the CLI locally with the project directory as the working directory. ## Without dowel MCP LLM agents working against a running Flutter app are blind without dowel MCP: - No access to the live VM Service: cannot read `Magic.find().rxState` or any controller state. - No way to drive the UI: gestures, snapshots, screenshots, modal waits all need the Tapir extension surface. - No HTTP / log / exception inspection: Crumbs's runtime feed stays invisible. - No connected REPL: every state inspection becomes a `print()` round-trip + hot-reload. ## With dowel MCP The dowel MCP server bridges the agent to the running app over the VM Service: - 10 substrate command tools (`dowel_start`, `dowel_stop`, `dowel_status`, `dowel_logs`, `dowel_restart`, `dowel_reload`, `dowel_hot_restart`, `dowel_doctor`, `dowel_list`, `dowel_tinker`). - Plugin tools surface automatically once the consumer's `bin/dispatcher.dart` wrapper registers the provider; see each plugin's MCP tool reference for the current catalogue ([tapir](https://anilcancakir.com/tapir/docs/mcp/tool-reference), [crumbs](https://anilcancakir.com/crumbs/docs/mcp/tool-reference)). - Tool visibility filters via `.dowel/mcp.json` + env vars + CLI flags (Cargo-style replace for allow, union for deny). - Zero release-build impact: every dowel integration gates on `kDebugMode` at the consumer's `main.dart`. Add `use dowel` to your prompt to nudge the agent toward the MCP tool surface: ``` Show me the current MonitorController state. use dowel ``` ## Installation ### Prerequisites The MCP server ships with `dowel`. Add the package + scaffold the consumer wrapper first so `./bin/dowel` and `bin/dispatcher.dart` exist: ```bash dart pub add dowel dart run dowel install ``` `install` writes `bin/dispatcher.dart` + `bin/dowel` + the codegen barrels (one-time per project; idempotent on re-run). After that, `mcp:install` writes the `.mcp.json` entry the clients below consume. ### Canonical entry shape The canonical `.mcp.json` entry shape (written by `./bin/dowel mcp:install` or `dart run dowel mcp:install`): ```json { "mcpServers": { "dowel": { "command": "./bin/dowel", "args": ["mcp:serve"], "cwd": "." } } } ``` When `bin/dowel` is absent or on Windows, `mcp:install` writes `{"command": "dart", "args": ["run", ":dispatcher", "mcp:serve"]}` instead. Every client below uses the canonical shape above, though the `command` + `args` payload adjusts based on platform and dowel availability. The `cwd` field must point at the project root (the directory that contains `pubspec.yaml`). --- ### Cursor Go to: **Settings, Cursor Settings, MCP, Add new global MCP server**. Paste into `~/.cursor/mcp.json`: ```json { "mcpServers": { "dowel": { "command": "./bin/dowel", "args": ["mcp:serve"], "cwd": "." } } } ``` When `bin/dowel` is absent or on Windows, `mcp:install` writes `{"command": "dart", "args": ["run", ":dispatcher", "mcp:serve"]}` instead. For project-scoped use, write the same snippet to `.cursor/mcp.json` in the project root. The spawned process is equivalent to running `./bin/dowel mcp:serve` from that directory. --- ### Claude Code Run this command from the project root to add the dowel MCP server: ```bash claude mcp add dowel -- ./bin/dowel mcp:serve ``` For project-scoped configuration: ```bash claude mcp add --scope project dowel -- ./bin/dowel mcp:serve ``` (When `bin/dowel` is absent or on Windows, use `dart run :dispatcher mcp:serve` instead.) You can also write `.mcp.json` directly with the canonical entry shape from the [Installation](#installation) section above. > **Note:** Claude Code does NOT auto-reconnect on `.mcp.json` edits. Run `/mcp reconnect dowel` inside Claude Code after every edit to load the new entry. --- ### Claude Desktop On Windows and macOS, install the official [Claude Desktop application](https://claude.ai/download). Open **File, Settings, Developer, MCP Servers, Edit Config** to open `claude_desktop_config.json`: - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` Add the dowel MCP server configuration. Replace the `cwd` value with the absolute path to your Flutter project root, since Claude Desktop has no project context: ```json { "mcpServers": { "dowel": { "command": "./bin/dowel", "args": ["mcp:serve"], "cwd": "/absolute/path/to/your/flutter/project" } } } ``` When `bin/dowel` is absent or on Windows, `mcp:install` writes `{"command": "dart", "args": ["run", ":dispatcher", "mcp:serve"]}` instead. > **Important:** Fully quit Claude Desktop via **File, Exit** (closing the window just minimises it). Restart it for the new entry to take effect. --- ### VS Code (GitHub Copilot) Create or edit `.vscode/mcp.json` in your project: ```json { "servers": { "dowel": { "type": "stdio", "command": "./bin/dowel", "args": ["mcp:serve"], "cwd": "." } } } ``` When `bin/dowel` is absent or on Windows, `mcp:install` writes `{"command": "dart", "args": ["run", ":dispatcher", "mcp:serve"]}` instead. For user-level configuration, add the same entry to your VS Code settings under `chat.mcp.servers`. --- ### Windsurf Add to your Windsurf MCP configuration: ```json { "mcpServers": { "dowel": { "command": "./bin/dowel", "args": ["mcp:serve"], "cwd": "." } } } ``` When `bin/dowel` is absent or on Windows, `mcp:install` writes `{"command": "dart", "args": ["run", ":dispatcher", "mcp:serve"]}` instead. Reload the Cascade panel after editing the config; Windsurf spawns `./bin/dowel mcp:serve` on next request. --- ### JetBrains IDEs (Junie / AI Assistant) **For Junie:** Open Junie, click the three dots in the top right corner, then **Settings, MCP Settings**: ```json { "mcpServers": { "dowel": { "command": "./bin/dowel", "args": ["mcp:serve"], "cwd": "." } } } ``` When `bin/dowel` is absent or on Windows, `mcp:install` writes `{"command": "dart", "args": ["run", ":dispatcher", "mcp:serve"]}` instead. **For AI Assistant:** Go to **Settings, Tools, AI Assistant, MCP** and add the same entry via the "as JSON" option. --- ### Cline / Roo-Code Add to your Cline MCP server configuration (typically `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` on macOS, equivalent paths on other OSes): ```json { "mcpServers": { "dowel": { "command": "./bin/dowel", "args": ["mcp:serve"], "cwd": "." } } } ``` When `bin/dowel` is absent or on Windows, `mcp:install` writes `{"command": "dart", "args": ["run", ":dispatcher", "mcp:serve"]}` instead. Roo-Code reads the same `mcpServers` shape; point its config file at the snippet above. --- ### OpenCode Add to your OpenCode configuration file (`~/.config/opencode/opencode.json` or per-project `opencode.json`): ```json { "mcp": { "servers": { "dowel": { "type": "local", "command": ["./bin/dowel", "mcp:serve"], "cwd": "." } } } } ``` OpenCode takes the executable and its arguments from the one `command` array. When `bin/dowel` is absent or on Windows, use `["dart", "run", ":dispatcher", "mcp:serve"]` instead; that is the entry `mcp:install` writes for the other clients. The `local` transport spawns the process directly; `cwd` pins the project root. --- ### Gemini CLI Add to `~/.gemini/settings.json`: ```json { "mcpServers": { "dowel": { "command": "./bin/dowel", "args": ["mcp:serve"], "cwd": "." } } } ``` When `bin/dowel` is absent or on Windows, `mcp:install` writes `{"command": "dart", "args": ["run", ":dispatcher", "mcp:serve"]}` instead. Restart `gemini` after editing; the CLI re-spawns `./bin/dowel mcp:serve` on the next session. --- ### Antigravity [Antigravity](https://antigravity.google/) is Google's AI-powered IDE built on VS Code. 1. Open the **Agent** side panel (`Cmd/Ctrl + L` or **View, Open View..., Agent**). 2. Click **Additional options (...)** in the upper right of the Agent panel. 3. Select **MCP Servers**. 4. Click **Manage MCP Servers**. 5. Click **View raw config** in the upper right of the Manage MCPs editor view. 6. Add this configuration: ```json { "mcpServers": { "dowel": { "command": "./bin/dowel", "args": ["mcp:serve"], "cwd": "." } } } ``` When `bin/dowel` is absent or on Windows, `mcp:install` writes `{"command": "dart", "args": ["run", ":dispatcher", "mcp:serve"]}` instead. > **Tip:** Install the [Dart and Flutter extensions](https://docs.flutter.dev/tools/vs-code) for the best development experience. --- ### Firebase Studio [Firebase Studio](https://firebase.studio/) is an agentic cloud-based development environment by Google. 1. In your Firebase Studio project, create `.idx/mcp.json` if it does not exist. 2. Add the dowel MCP configuration: ```json { "mcpServers": { "dowel": { "command": "./bin/dowel", "args": ["mcp:serve"], "cwd": "." } } } ``` When `bin/dowel` is absent or on Windows, `mcp:install` writes `{"command": "dart", "args": ["run", ":dispatcher", "mcp:serve"]}` instead. 3. Rebuild your workspace: - Open the Command Palette (`Shift + Ctrl + P`). - Enter **Firebase Studio: Rebuild Environment**. --- ### Other Clients For any other MCP-compatible client that supports stdio transport, spawn the server with: ```bash ./bin/dowel mcp:serve ``` When `bin/dowel` is absent or on Windows, spawn with `dart run :dispatcher mcp:serve` instead. The working directory must contain a `pubspec.yaml` that depends on `dowel` (directly or via a path / git dep). Clients that require a JSON entry shape accept the canonical form: ```json { "mcpServers": { "dowel": { "command": "./bin/dowel", "args": ["mcp:serve"], "cwd": "." } } } ``` When `bin/dowel` is absent or on Windows, `mcp:install` writes `{"command": "dart", "args": ["run", ":dispatcher", "mcp:serve"]}` instead. Remote HTTP transport is NOT supported in V1; the dowel MCP server is stdio-only because it bridges to the local VM Service URI recorded in this project's session by `./bin/dowel start` (or `dart run :dispatcher start` when `bin/dowel` is absent). --- ## Server Identity The dowel MCP server advertises itself as `serverInfo: {"name": "dowel_mcp", "version": ""}` in the `initialize` response. The `name` field is fixed regardless of the installed plugin set. Clients such as Claude Code, Cursor, Windsurf, and others derive tool prefixes (like `mcp__dowel__dowel_start`) from the `.mcp.json` key (typically `dowel`), not from `serverInfo.name`. The MCP specification treats `serverInfo.name` as a display hint, not a stable identifier for tool prefix derivation. The `version` field in `serverInfo` tracks the package version at release time, manually synced as part of the release cut process. See the MCP specification at https://modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle for the full lifecycle semantics. This addresses issue #9 NIT 7, which called out the version drift problem. --- ## Add a Rule To make the agent reach for dowel MCP without an explicit `use dowel` prompt, add a rule to your AI client: **Cursor:** Settings, Rules. **Claude Code:** Add to `CLAUDE.md` in the project root. **Windsurf:** Memories panel, add a workspace rule. **JetBrains AI Assistant:** Settings, Tools, AI Assistant, Custom Instructions. Example rule: ``` Always use dowel MCP when working with this Flutter project. Inspect running app state via dowel_tinker, drive the UI via tapir_* tools, and read HTTP / log / exception feeds via crumbs_*. Prefer dowel_status before assuming the app is running. ``` --- ## Automated Install The dowel CLI ships a built-in command that writes the canonical `.mcp.json` entry for you: ```bash dart run dowel mcp:install ``` The command is idempotent: pre-existing `mcpServers` keys are preserved, and running it twice replaces the `dowel` key in-place without creating duplicates. By default it writes to `.mcp.json` in the current working directory; override the path via `--path`. The command always writes the `mcpServers` shape, which fits clients that read that key (Claude Code, Cursor, Windsurf); VS Code reads `servers`, so write its file by hand as shown above: ```bash dart run dowel mcp:install --path ./config/mcp.json ``` To remove the entry later: ```bash dart run dowel mcp:uninstall ``` Both commands work without a running Flutter app; they only edit the JSON file. --- ## Available Tools The dowel MCP server surfaces 10 substrate tools plus plugin-contributed tools from any registered sibling plugin (`tapir`, `crumbs`, or custom plugins). See [tool-reference.md](https://anilcancakir.com/dowel/docs/mcp/tool-reference.md) for the per-tool input schema, output shape, and example invocations of the substrate tools, plus links to each plugin's MCP tool reference for the current plugin tool catalogue. Tool visibility is filtered in three layers (file `.dowel/mcp.json` lowest priority, env vars middle, CLI flags highest priority; deny always wins over allow). To temporarily hide a tool surface without uninstalling the server, edit `.dowel/mcp.json`: ```json { "packages": { "allow": null, "deny": [] }, "tools": { "allow": null, "deny": [] } } ``` After editing `.dowel/mcp.json` or env vars, run `/mcp reconnect dowel` inside Claude Code (or the equivalent reconnect action in your client); the server does NOT auto-reload in V1. ## Related Documentation - [MCP Overview](https://anilcancakir.com/dowel/docs/mcp/overview.md): the substrate and plugin tool layers. - [MCP Tool Reference](https://anilcancakir.com/dowel/docs/mcp/tool-reference.md): every substrate tool and its input. - [mcp:serve](https://anilcancakir.com/dowel/docs/commands/mcp-serve.md): the server command and its filter flags. --- # MCP Tool Reference Catalogue of every MCP tool surfaced by `dowel` directly. The MCP server exposes **10 substrate tools** (always available) plus plugin-contributed tools when the matching provider is registered in the consumer's `bin/dispatcher.dart`. Plugin tool catalogues live on each plugin's own documentation site (see [Plugin Tools](#plugin-tools) below). - [Tool Naming Convention](#tool-naming-convention) - [Substrate Tools](#substrate-tools) - [dowel_start](#dowel_start) - [dowel_stop](#dowel_stop) - [dowel_status](#dowel_status) - [dowel_logs](#dowel_logs) - [dowel_restart](#dowel_restart) - [dowel_reload](#dowel_reload) - [dowel_hot_restart](#dowel_hot_restart) - [dowel_doctor](#dowel_doctor) - [dowel_list](#dowel_list) - [dowel_tinker](#dowel_tinker) - [Plugin Tools](#plugin-tools) - [Filter Configuration](#filter-configuration) - [Related Documentation](#related-documentation) --- ## Tool Naming Convention ### Substrate tools Substrate tools are built-in dowel commands exposed by `McpServer` directly. Their names follow the pattern `dowel_` where the dowel command name is transformed: - `:` (namespace separator) becomes `_` - `-` (word separator) becomes `_` Examples: `start` maps to `dowel_start`; `hot-restart` maps to `dowel_hot_restart`. ### Plugin tools Plugin tools are contributed via `DowelServiceProvider.mcpTools()`. Each plugin package owns a prefix that matches the package name or domain: | Plugin package | Tool prefix | Reference | |:---|:---|:---| | `tapir` | `tapir_` | [anilcancakir.com/tapir/docs/mcp/tool-reference](https://anilcancakir.com/tapir/docs/mcp/tool-reference) | | `crumbs` | `crumbs_` | [anilcancakir.com/crumbs/docs/mcp/tool-reference](https://anilcancakir.com/crumbs/docs/mcp/tool-reference) | The same `:` and `-` transformation applies inside the suffix part. ### Exclusions from the allowlist Interactive (`help`), codegen (`make:*`, `*:refresh`), installer (`plugin:*`, `install`), and MCP meta (`mcp:*`) commands are intentionally excluded. They either require a TTY, mutate source on disk (better served by the client's own file tools), or recurse into the MCP server itself. `tinker` was previously excluded as interactive but is now absorbed into the substrate as `dowel_tinker` (the `--eval` one-shot path drives the MCP surface). --- ## Substrate Tools The 10 substrate tools are always present when the MCP server starts. They are served in-process: the server builds a `MapInput` from the MCP arguments, runs the dowel command via `BufferedOutput`, and returns the combined stdout + exit code as MCP text content. ### dowel_start Maps to dowel command: `start` Starts the Flutter app in debug mode and records the process PID and VM Service URI in this project's session file. Once it exists the plugin tools (tapir, crumbs, tinker) can connect to the running VM. | Parameter | Type | Default | Description | |:---|:---|:---|:---| | `device` | string | `chrome` | Flutter device id. Common values: `chrome` (web), `macos` (desktop), `` (Android), or any id from `flutter devices`. | | `port` | string | `3100` | Web port for the chrome device (numeric string). Ignored for non-web devices. | | `vm-service-port` | string | `8181` | Port the VM Service binds to on the host. Change when 8181 is already taken. | | `dds` | boolean | `false` | Enable the Dart Development Service (DDS) proxy. Set `true` when a tool requires DDS-only features. | | `profile-static` | boolean | `false` | Run a device (Android, iOS, desktop) with `flutter run --profile`: release-like performance, no hot reload. On `chrome` it only tags the session, because a web profile build has no VM Service. | ### dowel_stop Maps to dowel command: `stop` Sends `SIGTERM` to the process group of the recorded Flutter tool, so `frontend_server` and the other children stop with it, and waits up to 5 seconds for the group. A member still there is sent `SIGKILL` and waited on for up to 5 seconds more; then, on a web session, the web port gets up to 5 seconds to come free. A CDP session's Chrome is stopped the same way, with its CDP port. A pid that now belongs to a process started after the session was recorded is left alone. The call returns once the app is gone (about 15 seconds at worst, 30 with Chrome), so a `dowel_start` straight after it does not race the old app for its port. Then it deletes this project's session; an app that outlives `SIGKILL` keeps its session and fails the call, so it can be retried. On an Android device it also runs `adb -s shell am force-stop ` (the id comes from `android/app/build.gradle*`), because signalling the flutter tool leaves the app running. After this call, plugin tools will fail until `dowel_start` is called again. No parameters. ### dowel_status Maps to dowel command: `status` Reads this project's session and returns the current process state as JSON: PID, device, VM Service URI, start timestamp, and whether the process is still alive. No parameters. ### dowel_logs Maps to dowel command: `logs` Returns the log captured from the Flutter process since this project's session started. Use this to see compilation errors, framework warnings, and `print()` output from the app. | Parameter | Type | Default | Description | |:---|:---|:---|:---| | `follow` | boolean | `false` | When `true`, tail the live log stream until the client disconnects (`-f` on the CLI). When `false`, return the whole captured log and exit immediately. | ### dowel_restart Maps to dowel command: `restart` Stops the running Flutter app and starts it again (stop plus start, not a hot restart). The relaunch carries the prior session's device, web port, VM Service port, CDP port, `--flutter-arg` values and profile build mode across. The Dart process is new, so all in-memory state is lost. For the `R` keypress, use [dowel_hot_restart](#dowel_hot_restart). No parameters. ### dowel_reload Maps to dowel command: `reload` Sends `r` to the stdin of the running `flutter run` process (through the FIFO the session records), so it works on every device target. Equivalent to pressing `r` in the flutter run terminal. Updates code in place; widget state is preserved. Faster than `dowel_restart` but does not pick up changes to `initState` or constructor bodies. No parameters. ### dowel_hot_restart Maps to dowel command: `hot-restart` Sends `R` to the stdin of the running `flutter run` process (the same FIFO as `dowel_reload`): tears down the widget tree and rebuilds from `main()` while keeping the Dart VM alive. Faster than a full process restart; slower than hot reload. Use when hot reload cannot pick up the change (e.g. changes to `StatefulWidget.createState`, `didChangeDependencies`, or provider boot logic). No parameters. ### dowel_doctor Maps to dowel command: `doctor` Runs four preflight checks, each reported as pass or fail: `flutter --version`, `dart --version`, port 3100 free, and a Flutter SDK of 3.30.0 or newer (needed for `--cdp-port`). It also prints advisory warnings for a stale `.mcp.json` entry and an SDK too old for CDP commands; those never change the exit code. Run this first when `dowel_start` fails unexpectedly. No parameters. ### dowel_list Maps to dowel command: `list` Lists every registered dowel command with its signature and description. Output mirrors `dart run dowel list`. Useful for discovering commands contributed by plugin providers registered in the consumer's `bin/dispatcher.dart`. No parameters. ### dowel_tinker Maps to dowel command: `tinker` Evaluate a Dart expression inside the running Flutter app via the VM Service evaluate RPC. Takes required `eval` argument (Dart expression). Returns the formatted evaluation result as text. Dispatch follows the same lazy-reconnect path as plugin VM-extension tools; the expression runs in the foreground isolate's root library scope and is passed through the `Tinker.casters` chain before return. | Parameter | Type | Default | Description | |:---|:---|:---|:---| | `eval` | string | (required) | Dart expression to evaluate in the running app's root library (e.g. `User.current.name`, `MonitorController.instance.refresh()`, `1+1`). Runs synchronously in the foreground isolate; the formatted result returns as text. | --- ## Plugin Tools Plugin tools are contributed by sibling packages via `DowelServiceProvider.mcpTools()`. They surface in the MCP server only when the corresponding provider is registered in the consumer's `bin/dispatcher.dart` and the MCP server is restarted (run `./bin/dowel mcp:serve` or `dart run :dispatcher mcp:serve` on Windows or when `bin/dowel` is absent). Each plugin tool dispatches over a `ext..*` VM Service extension registered by the plugin inside the running Flutter app. --- ### tapir E2E interaction driver. Tools operate over `ext.tapir.*` VM Service extensions installed by `tapir` inside the running app. Tool names use the `tapir_` prefix. The plugin's MCP tool catalogue evolves alongside the package; the canonical reference lives at [anilcancakir.com/tapir/docs/mcp/tool-reference](https://anilcancakir.com/tapir/docs/mcp/tool-reference) (per-tool input schema, required parameters, return shape, example invocations). --- ### crumbs Runtime inspector. Tools read ring buffers populated by `ext.crumbs.*` VM Service extensions installed by `crumbs` inside the running app. Tool names use the `crumbs_` prefix. The plugin's MCP tool catalogue evolves alongside the package; the canonical reference lives at [anilcancakir.com/crumbs/docs/mcp/tool-reference](https://anilcancakir.com/crumbs/docs/mcp/tool-reference) (per-tool input schema, required parameters, return shape, example invocations). --- ### Tinker (absorbed into the substrate) The connected REPL is no longer a plugin tool: it ships as the substrate's `dowel_tinker` (see above). `magic_tinker` is still consumed by the host app for the interactive REPL CLI (`dart run dowel tinker`), but its MCP surface has been removed to avoid duplicating the substrate path. Use `dowel_tinker` from any MCP client for live state inspection, controller calls, or facade events. --- ## Filter Configuration The MCP server applies a three-layer filter (file, then env vars, then CLI flags) to control which plugin tools surface. Each layer uses Cargo-style replacement for allow lists and union for deny lists. ### Precedence rules - **Allow lists**: CLI replaces env, which replaces file. A non-null allow list at a higher layer REPLACES the lower layer entirely. Null means "no opinion at this layer." - **Deny lists**: union of all three layers. A tool denied in any layer stays denied. - **Deny wins over allow**: when a name appears in both the effective allow set and the effective deny set, the deny wins. ### Layer 1: `.dowel/mcp.json` Place this file in the project root (next to `bin/dispatcher.dart`): ```json { "packages": { "allow": null, "deny": [] }, "tools": { "allow": null, "deny": [] } } ``` Field reference: | Key | Type | Meaning | |:---|:---|:---| | `packages.allow` | `null` or `string[]` | `null` = allow all packages; array = allow only named packages | | `packages.deny` | `string[]` | Package names always excluded (wins over allow) | | `tools.allow` | `null` or `string[]` | `null` = allow all tool names; array = allow only named tools | | `tools.deny` | `string[]` | Tool names always excluded (wins over allow) | Package names match `DowelServiceProvider.providerName`: `dowel` (substrate), `tapir`, `crumbs`. Tool names match `McpToolDescriptor.name` exactly (e.g. `dowel_tinker`, or any name listed on each plugin's tool-reference site). ### Layer 2: environment variables | Variable | Effect | |:---|:---| | `DOWEL_MCP_PACKAGES_ALLOW` | CSV of package names to allow | | `DOWEL_MCP_PACKAGES_DENY` | CSV of package names to deny | | `DOWEL_MCP_TOOLS_ALLOW` | CSV of tool names to allow | | `DOWEL_MCP_TOOLS_DENY` | CSV of tool names to deny | ### Layer 3: CLI flags Pass flags directly to the MCP server invocation: ```bash ./bin/dowel mcp:serve \ --include-package=tapir \ --exclude-tool=dowel_stop ``` CLI flags replace the env + file allow lists and are unioned into the deny sets. **Example: expose tapir tools only** `{ "packages": { "allow": ["tapir"], "deny": [] }, "tools": { "allow": null, "deny": [] } }` **Example: hide destructive substrate tools** `{ "packages": { "allow": null, "deny": [] }, "tools": { "allow": null, "deny": ["dowel_stop"] } }` --- ## Related Documentation - [MCP Overview](https://anilcancakir.com/dowel/docs/mcp/overview.md): architecture, transport, and server entry points - [MCP Setup](https://anilcancakir.com/dowel/docs/mcp/setup.md): consumer registration, `.mcp.json`, and first-run checklist --- # Plugin Authoring Guide A plugin is a Dart package that ships a `DowelServiceProvider` subclass. The subclass declares the CLI commands and MCP tool descriptors the plugin contributes. When a consumer calls `plugin:install `, dowel registers the provider in `lib/app/_plugins.g.dart`, which is loaded at startup and merged into the `DowelRegistry`. Commands appear in `dowel list`; MCP tool descriptors surface through the stdio JSON-RPC server so LLM agents can invoke them by name. - [Getting Started](#getting-started) - [DowelServiceProvider Contract](#dowelserviceprovider-contract) - [Writing Commands](#writing-commands) - [Adding MCP Tools](#adding-mcp-tools) - [Publishing](#publishing) - [Linking from a Consumer](#linking-from-consumer) - [Related Documentation](#related-documentation) --- ## Getting Started Scaffold the plugin skeleton with the `make:plugin` command: ```bash dart run dowel make:plugin awesome_plugin ``` The generator creates the following tree inside `awesome_plugin/`: ``` awesome_plugin/ lib/ awesome_plugin.dart # barrel export src/ awesome_plugin_dowel_provider.dart # DowelServiceProvider subclass commands/ # one file per DowelCommand pubspec.yaml # declares dowel dependency CHANGELOG.md README.md ``` The generated provider is immediately wirable: `plugin:install awesome_plugin` injects `AwesomePluginDowelProvider.new` into the consumer's `_plugins.g.dart`. --- ## DowelServiceProvider Contract Every plugin must extend `DowelServiceProvider` (`lib/src/console/dowel_service_provider.dart:11-25`): ```dart abstract class DowelServiceProvider { /// Human-readable provider name used in collision error messages. /// Defaults to the runtime class name. String get providerName => runtimeType.toString(); /// Returns the commands this provider contributes to the application. List commands(); /// Returns the MCP tool descriptors this provider contributes. /// /// Defaults to an empty list so existing providers compile without /// modification. Override to expose VM-Service-backed tools to the MCP /// server. List mcpTools() => const []; } ``` Key points: - `commands()` is **abstract**: return every `DowelCommand` subclass your plugin ships. Duplicate command names throw `DowelCommandCollisionException`. - `mcpTools()` defaults to an empty list. Override it to expose VM Service extension tools to the MCP server. - `providerName` defaults to `runtimeType.toString()`. Override it with a stable identifier (e.g. `'tapir'`) so collision messages survive class renames. - There is no `register()` lifecycle hook on `DowelServiceProvider`. Initialisation belongs in command constructors or the host app's boot phase. Minimal scaffold provider (`assets/stubs/make_plugin/generic/provider.dart.stub`): ```dart import 'package:dowel/dowel.dart'; final class AwesomePluginDowelProvider extends DowelServiceProvider { @override List commands() => [ // Commands added via `make:command` land here automatically. ]; // To expose MCP tools, override mcpTools() returning your descriptor list. } ``` --- ## Writing Commands Each entry in `commands()` is a `DowelCommand` subclass. Declare the command surface via the Signature DSL or `configure(ArgParser)`: - **Signature DSL** (preferred): `String get signature => 'cmd:name {arg} {--flag}'`. See [Signature DSL reference](https://anilcancakir.com/dowel/docs/reference/signature-dsl.md) for the full syntax. - **`configure(ArgParser)`**: explicit fallback for cases the DSL cannot express (subcommands, allowed values with validation, etc.). Scaffold a new command inside the plugin directory: ```bash dart run dowel make:command MyVerb ``` The generator writes `lib/src/commands/my_verb_command.dart` and auto-registers it in the provider's `commands()` list. See [make:command](https://anilcancakir.com/dowel/docs/commands/index.md) for the full scaffold walkthrough. --- ## Adding MCP Tools Override `mcpTools()` and return a `List`. Each descriptor (`lib/src/mcp/mcp_tool_descriptor.dart:24-82`) is a `const`-constructible value with four required fields: | Field | Type | Purpose | |:---|:---|:---| | `name` | `String` | Snake-case, service-prefixed tool name (e.g. `awesome_start`). Must be globally unique across all loaded providers. | | `description` | `String` | Action-oriented, LLM-targeted description. Imperative opening sentence, usage bullets, constraint-forward language. | | `inputSchema` | `Map` | JSON Schema draft-7 object describing accepted input. Pass `{'type': 'object', 'properties': {}}` when the tool takes no arguments. | | `extensionMethod` | `String` | VM Service extension registered in the running Flutter isolate (e.g. `ext.awesome.start`). Internal routing key; not included in the MCP wire shape. | Example provider with commands and MCP tools: ```dart import 'package:dowel/dowel.dart'; final class AwesomeStartCommand extends DowelCommand { @override String get signature => 'awesome:start'; @override String get description => 'Start the awesome service.'; @override CommandBoot get boot => CommandBoot.connected; @override Future handle(DowelContext context) async { context.output.success('Awesome service started.'); return 0; } } final class AwesomeStopCommand extends DowelCommand { @override String get signature => 'awesome:stop'; @override String get description => 'Stop the awesome service.'; @override CommandBoot get boot => CommandBoot.connected; @override Future handle(DowelContext context) async { context.output.success('Awesome service stopped.'); return 0; } } final class AwesomePluginDowelProvider extends DowelServiceProvider { @override List commands() => [ AwesomeStartCommand(), AwesomeStopCommand(), ]; @override List mcpTools() => [ McpToolDescriptor( name: 'awesome_start', description: 'Start the awesome service', inputSchema: {'type': 'object', 'properties': {}}, extensionMethod: 'ext.awesome.start', ), ]; } ``` `extensionMethod` must match the `registerExtension` call in the running Flutter isolate; the descriptor is not callable without it. The MCP server collects descriptors from all loaded providers when `runDowel(collectMcpTools: true)` fires (`bin/mcp.dart` sets this automatically). Tool names must be unique; a collision throws `DowelMcpToolCollisionException` at startup. Naming convention: prefix every tool name with a short service identifier followed by an underscore (for example `my_plugin_greet`). The prefix should match the package's `providerName` (or a recognisable abbreviation thereof). --- ## Publishing 1. Bump the version in `pubspec.yaml` following SemVer (patch for fixes, minor for new commands/tools, major for breaking provider API changes). 2. Declare the dependency on `dowel` with a caret constraint, not `path:`: ```yaml dependencies: dowel: ^0.0.21 ``` 3. Validate the archive before tagging: ```bash dart pub publish --dry-run ``` 4. Add a CHANGELOG entry, then publish: ```bash dart pub publish ``` --- ## Linking from Consumer The consumer installs the plugin with: ```bash dart run dowel plugin:install awesome_plugin ``` This registers `AwesomePluginDowelProvider.new` in `lib/app/_plugins.g.dart` and re-generates the barrel; the provider is discovered at the next `dowel start`. See [plugin:install](https://anilcancakir.com/dowel/docs/commands/plugin-install.md) for the two routing modes (manifest, direct registration). --- ## Related Documentation - [install-yaml.md](https://anilcancakir.com/dowel/docs/plugins/install-yaml.md) - `install.yaml` manifest schema for declaring install steps declaratively. - [installer-dsl.md](https://anilcancakir.com/dowel/docs/plugins/installer-dsl.md) - `PluginInstaller` fluent DSL for authoring programmatic install logic. - [make:plugin](https://anilcancakir.com/dowel/docs/commands/make-plugin.md) - scaffold command reference with all flags and the generated file tree. --- # install.yaml Schema Reference Canonical schema for the declarative `install.yaml` manifest consumed by `plugin:install`'s manifest flow. - [Overview](#overview) - [Top-Level Fields](#top-level-fields) - [plugin_name](#plugin_name) - [pubspec / dev_pubspec / pubspec_assets (dependencies:)](#dependencies) - [publish](#publish) - [json_merge](#json_merge) - [magic](#magic) - [native](#native) - [native.android](#nativeandroid) - [native.ios](#nativeios) - [native.macos](#nativemacos) - [native.web](#nativeweb) - [env](#env) - [prompts](#prompts) - [placeholders](#placeholders) - [post_install](#post_install) - [bootstrap_command](#bootstrap_command) - [Complete Example](#complete-example) - [Related Documentation](#related-documentation) ## Overview `install.yaml` is the declarative manifest that drives `plugin:install`'s manifest flow. The file is parsed by `ManifestParser` (`lib/src/installer/manifest_parser.dart`) into a typed `InstallManifest` (`lib/src/installer/install_manifest.dart`) and consumed by `ManifestInstaller`, which translates each section into deferred `InstallOperation`s on the `PluginInstaller` transaction. Nothing writes to disk until `commit(dryRun:, force:)` fires. The schema is intentionally tolerant: every section except `plugin_name` is optional, every section factory returns a sensible empty default when absent, and partial manifests parse cleanly. Schema violations raise `ManifestValidationException`; raw YAML syntax errors raise `FormatException`. ## Top-Level Fields | Field | Type | Required | Default | Source | |:---|:---|:---|:---|:---| | `plugin_name` | string | yes | (none) | `manifest_parser.dart:112` | | `dependencies` | map | no | empty | `manifest_parser.dart:124` | | `publish` | map | no | empty | `manifest_parser.dart:127` | | `json_merge` | map | no | empty | `manifest_parser.dart:128` | | `magic` | map | no | empty | `manifest_parser.dart:129` | | `native` | map | no | empty | `manifest_parser.dart:132` | | `env` | map | no | empty | `manifest_parser.dart:135` | | `prompts` | list | no | empty | `manifest_parser.dart:136` | | `placeholders` | map | no | empty | `manifest_parser.dart:137` | | `post_install` | map | no | empty | `manifest_parser.dart:138` | | `bootstrap_command` | string | no | `null` | `manifest_parser.dart:141` | ## plugin_name The plugin's pubspec package name. Used to scope `.dowel/installed/.json` reversibility records, the `plugins.json` registry entry, and the generated `lib/app/_plugins.g.dart` import line. **Validation regex** (`manifest_parser.dart:31`): ``` ^[a-z_][a-z0-9_]*$ ``` The value must start with a lowercase letter or underscore, followed by any number of lowercase letters, digits, or underscores. An empty string raises `ManifestValidationException('plugin_name must not be empty.')`. ```yaml plugin_name: example_plugin ``` ## dependencies Pubspec mutations. The `dependencies:` map (note the YAML key is plural, but the typed model is `PubspecDeps`) carries three sub-keys, all optional. | Sub-key | Type | Translates to | |:---|:---|:---| | `pubspec` | `Map` | `dependencies:` entries in the consumer's `pubspec.yaml` | | `dev_pubspec` | `Map` | `dev_dependencies:` entries | | `pubspec_assets` | `List` | Paths appended under `flutter.assets:` | Empty maps and missing keys are interchangeable; both produce an empty `PubspecDeps`. ```yaml dependencies: pubspec: intl: ^0.20.0 dev_pubspec: mocktail: ^1.0.0 pubspec_assets: - assets/example/ ``` ## publish Stub-name to target-path mapping. Each entry becomes a `publishConfig` operation: the named stub is read from the plugin's `install/` directory, placeholder-substituted, and written to the target path inside the consumer project. | Key shape | Value shape | |:---|:---| | Stub name (string) | Target path (string) | Non-string values raise `FormatException('publish entry "" must map to a String target path; got .')`. ```yaml publish: install/example_config.dart.stub: lib/config/example.dart ``` ## json_merge Map of target JSON file path to merge spec. Each entry triggers a `mergeJson` operation: the source stub is parsed as JSON, then merged into the target file according to `additive`. | Field | Type | Required | Default | Notes | |:---|:---|:---|:---|:---| | `source` | string | yes | (none) | Stub key whose content is parsed as JSON. Missing or non-string raises `FormatException('json_merge entry missing required "source" string.')`. | | `additive` | bool | no | `true` | When `true`, existing keys in the target file survive a collision. When `false`, source keys overwrite target keys. | ```yaml json_merge: assets/lang/en.json: source: install/lang/en.json additive: true ``` ## magic Magic-framework integration block. Every slot is optional; the section defaults to an empty integration when absent. | Field | Type | Translates to | |:---|:---|:---| | `provider` | string (PascalCase) | `injectProvider(provider)` | | `config_factory` | string | `injectConfigFactory(name)` | | `routes` | string | `injectRoute(name)` | **Validation regex** for `provider` (`manifest_parser.dart:34`): ``` ^[A-Z][A-Za-z0-9]*$ ``` The value, when present, must be a PascalCase identifier (uppercase first letter, alphanumerics afterwards). A non-matching value raises `ManifestValidationException('magic.provider "" does not match PascalCase regex ^[A-Z][A-Za-z0-9]*$.')`. ```yaml magic: provider: ExampleServiceProvider config_factory: exampleConfig routes: registerExampleRoutes ``` ## native Per-platform native configuration. Each platform sub-section is independently optional. ### native.android | Field | Type | Translates to | |:---|:---|:---| | `permissions` | `List` | `` entries in `AndroidManifest.xml` | | `meta_data` | `Map` | `` entries in `AndroidManifest.xml` | | `activities` | `List<{name, exported?, task_affinity?, intent_filters?}>` | `` elements inside `` in `AndroidManifest.xml` | | `gradle.plugins` | `List<{id, version?}>` | Gradle plugin block entries | | `gradle.dependencies` | `List<{scope, notation}>` | Gradle dependency block entries | Each `activities` entry needs `name` (an entry without it raises `FormatException('Android activity missing required "name".')`). `exported` is `false` when omitted. `task_affinity` sets `android:taskAffinity`; an empty string `""` is an empty affinity and omitting the key omits the attribute. Each `intent_filters` entry takes `auto_verify` (bool), `actions` and `categories` (lists of strings) and `data` (a list of `{scheme, host, path, path_prefix}` maps, one `` element each). An activity is idempotent by content. The same `name` with an equal set of intent filters (in any order) is left alone, and the same `name` with different filters is never rewritten: the install finishes and warns with the block it expected. `exported` and `task_affinity` are not part of that comparison. Gradle plugin entries without `id` raise `FormatException('Gradle plugin missing required "id".')`. Gradle dependency entries without both `scope` and `notation` raise `FormatException('Gradle dependency entry requires both "scope" and "notation".')`. ```yaml native: android: permissions: - android.permission.INTERNET meta_data: io.flutter.embedded_views_preview: "true" activities: - name: com.example.CallbackActivity exported: true task_affinity: "" intent_filters: - auto_verify: true actions: [android.intent.action.VIEW] categories: - android.intent.category.DEFAULT - android.intent.category.BROWSABLE data: - {scheme: https, host: auth.example.com, path: /callback} gradle: plugins: - id: com.example.gradle.plugin version: "1.0.0" dependencies: - scope: implementation notation: "com.example:lib:1.0.0" ``` ### native.ios | Field | Type | Notes | |:---|:---|:---| | `info_plist` | `Map` | Values may be String, bool, num, or List. Runtime-type branching happens in the dispatcher. | | `url_schemes` | `List` | Custom URL schemes registered under `CFBundleURLTypes`, written without the `://` suffix. One `CFBundleURLTypes` dict (role `Editor`) is appended per scheme; a scheme any existing dict already lists is skipped. | | `entitlements` | `Map` | Values may be String, bool, or a list of strings. Written to every entitlements file the application target signs with; see below. | | `podfile.platform_version` | string (optional) | Informational in v1: no chain method consumes it yet. | | `podfile.pods` | `List` | Pod declarations appended to `target 'Runner'`. | ```yaml native: ios: info_plist: NSExampleUsageDescription: "Reason shown in iOS permission dialog" UIBackgroundModes: ["fetch"] url_schemes: - com.example.app entitlements: com.apple.security.keychain: true com.apple.developer.applesignin: [Default] podfile: platform_version: "13.0" pods: - "ExamplePod" ``` `entitlements` keys go into every entitlements file the application target signs with, one per distinct `CODE_SIGN_ENTITLEMENTS` value across its build configurations, and the Xcode build settings are left as they are. A project that signs with none gets `Runner/Runner.entitlements` written and the target pointed at it. A list value is merged entry by entry into the array the file already carries, so entries another plugin or the app put there survive. A value that is not a string, a bool, or a list of strings fails the install. Both the plist and `project.pbxproj` writes are helper-backed and sit outside the transaction's `.tmp` rollback; see [PluginInstaller DSL](https://anilcancakir.com/dowel/docs/plugins/installer-dsl.md) for the cases that warn instead of write. ### native.macos Shape mirrors `native.ios` exactly. Same field set (`info_plist`, `url_schemes`, `entitlements`, `podfile.platform_version`, `podfile.pods`), same dispatcher rules. A macOS Flutter project signs Debug and Release with different files (`Runner/DebugProfile.entitlements`, `Runner/Release.entitlements`), so an entitlement lands in both. See the [Complete Example](#complete-example) for a macOS block in context. ### native.web | Field | Type | Translates to | |:---|:---|:---| | `head_scripts` | `List` | HTML snippets injected before `` in `web/index.html` | | `meta_tags` | `List>` | `` attribute maps appended to `web/index.html` | ```yaml native: web: head_scripts: - '' meta_tags: - {name: "description", content: "Example plugin metadata"} ``` ## env Environment variables to inject into the consumer's `.env`. Map of var name to spec. | Field | Type | Required | Notes | |:---|:---|:---|:---| | `default` | scalar | yes | Default value written into `.env`. Coerced via `toString()`. Missing raises `FormatException('env entry missing required "default".')`. | | `comment` | string | no | Advisory documentation. The v1 dispatcher does NOT emit the comment into the `.env` file. | Non-map entries raise `FormatException('env entry "" must be a map with "default" + optional "comment"; got .')`. ```yaml env: EXAMPLE_KEY: default: example_value comment: "Example plugin runtime config" ``` ## prompts Ordered list of prompts driven immediately at the start of install (before any deferred operation runs). Prompt answers populate the prompt-result map that `placeholders` interpolates. | Field | Type | Required | Notes | |:---|:---|:---|:---| | `key` | string | yes | Unique within the prompts list. Duplicates raise `ManifestValidationException('Duplicate prompt key "" in prompts list.')`. | | `type` | string | yes | One of `string`, `bool`, `choice`. | | `question` | string | yes | Human-readable question text. | | `default` | scalar | no | Stored as a String regardless of `type` for uniform placeholder substitution. | | `options` | `List` | no | For `choice` prompts: the valid option set. Empty for non-choice types. | Missing `key`, `type`, or `question` raises `FormatException('Prompt entry requires "key", "type", and "question" strings.')`. ```yaml prompts: - {key: configPath, type: string, default: "~/.example.conf", question: "Config file path?"} - {key: mode, type: choice, options: [dev, staging, prod], default: dev, question: "Deployment mode?"} - {key: enableFeature, type: bool, default: false, question: "Enable optional feature?"} ``` ## placeholders Map of placeholder key to value template. Values may reference prompt answers via `{{ prompts.KEY }}`. Whitespace around the dotted reference is optional, so `{{prompts.x}}` and `{{ prompts.x }}` are both accepted. **Reference regex** (`manifest_parser.dart:39`): ``` \{\{\s*prompts\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\} ``` Every `{{ prompts.X }}` reference must resolve to a declared prompt key. Unknown references raise `ManifestValidationException('Placeholder "" references unknown prompt key "".')`. ```yaml placeholders: configFilePath: "{{ prompts.configPath }}" runtimeMode: "{{ prompts.mode }}" ``` ## post_install Post-install shell operations plus a final info message. Every sub-field is optional. | Field | Type | Notes | |:---|:---|:---| | `run` | `List<{cmd, args?}>` | Shell commands run unconditionally at commit phase. `cmd` is required; missing raises `FormatException('Shell spec missing required "cmd".')`. | | `ask_to_run` | `List<{prompt, cmd, args?}>` | Shell commands run only after user confirmation. The prompt fires immediately at install time; on yes the shell call is deferred. Missing `prompt` or `cmd` raises `FormatException('ask_to_run entry requires "prompt" and "cmd" strings.')`. | | `message` | string | Optional info message emitted after a Success result. | ```yaml post_install: run: - cmd: dart args: [format, lib/config/example.dart] ask_to_run: - prompt: "Run pub get now?" cmd: flutter args: [pub, get] message: | example_plugin installed. ``` ## bootstrap_command Optional plugin-specific bootstrap command name AUTO-RUN after a Success result. On a non-dry-run install, once the plugin is registered (`plugins.json` + `lib/app/_plugins.g.dart` regenerated), `plugin:install` spawns the declared command as a fresh dispatcher subprocess so the just-registered plugin command actually executes. The chained command is a plugin command that was only just written to `_plugins.g.dart`, so it is not loaded in the current process; a subprocess is the only way to reach it. Dispatcher resolution: 1. `./bin/dowel --non-interactive` when a `bin/dowel` wrapper exists at the consumer project root (the AOT fast-CLI path). 2. Otherwise `dart run :dispatcher --non-interactive` when `bin/dispatcher.dart` exists (Windows, or a failed fast-CLI build). 3. When neither resolves, `plugin:install` prints the one-line bootstrap hint (`Bootstrap with: dowel `) instead, so the operator can run it manually. `--non-interactive` is ALWAYS forwarded to the chained command so an interactive bootstrap (e.g. `starter:install`) cannot hang waiting on stdin. Override and opt-out: - `--bootstrap-command=` on the `plugin:install` invocation overrides the manifest value (the override command runs instead of the declared one). - `--no-bootstrap` skips the auto-run entirely; the command falls back to printing the hint. This was hint-only before `0.0.9`: prior versions only printed `Bootstrap with: dowel ` and never executed the command. ```yaml bootstrap_command: example:install ``` ## Complete Example The example below is the canonical `_fullYaml` test fixture from `test/installer/manifest_parser_test.dart`. Every section above appears here in context. ```yaml plugin_name: example_plugin dependencies: pubspec: intl: ^0.20.0 crypto: ^3.0.0 dev_pubspec: mocktail: ^1.0.0 pubspec_assets: - assets/example/ - assets/lang/en.json publish: install/example_config.dart.stub: lib/config/example.dart install/example_routes.dart.stub: lib/routes/example.dart json_merge: assets/lang/en.json: source: install/lang/en.json additive: true magic: provider: ExampleServiceProvider config_factory: exampleConfig routes: registerExampleRoutes native: android: permissions: - android.permission.INTERNET meta_data: io.flutter.embedded_views_preview: "true" activities: - name: com.example.CallbackActivity exported: true intent_filters: - auto_verify: true actions: [android.intent.action.VIEW] categories: [android.intent.category.DEFAULT, android.intent.category.BROWSABLE] data: - {scheme: https, host: auth.example.com, path: /callback} gradle: plugins: - id: com.example.gradle.plugin version: "1.0.0" dependencies: - scope: implementation notation: "com.example:lib:1.0.0" ios: info_plist: NSExampleUsageDescription: "Reason shown in iOS permission dialog" UIBackgroundModes: ["fetch"] url_schemes: - com.example.app entitlements: com.apple.security.keychain: true podfile: platform_version: "13.0" pods: - "ExamplePod" macos: info_plist: NSExampleUsageDescription: "Reason shown in macOS permission dialog" entitlements: com.apple.security.network.client: true podfile: platform_version: "11.0" pods: - "ExamplePod" web: head_scripts: - '' meta_tags: - {name: "description", content: "Example plugin metadata"} env: EXAMPLE_KEY: default: example_value comment: "Example plugin runtime config" prompts: - {key: configPath, type: string, default: "~/.example.conf", question: "Config file path?"} - {key: mode, type: choice, options: [dev, staging, prod], default: dev, question: "Deployment mode?"} - {key: enableFeature, type: bool, default: false, question: "Enable optional feature?"} placeholders: configFilePath: "{{ prompts.configPath }}" runtimeMode: "{{ prompts.mode }}" post_install: run: - cmd: dart args: [format, lib/config/example.dart] ask_to_run: - prompt: "Run pub get now?" cmd: flutter args: [pub, get] message: | example_plugin installed. bootstrap_command: example:install ``` ## Related Documentation - [Plugin Authoring](https://anilcancakir.com/dowel/docs/plugins/authoring.md): the whole flow of writing a plugin. - [Installer DSL](https://anilcancakir.com/dowel/docs/plugins/installer-dsl.md): the procedural escape hatch for what a manifest cannot express. - [plugin:install](https://anilcancakir.com/dowel/docs/commands/plugin-install.md): how a manifest is applied. --- # PluginInstaller Fluent DSL The `PluginInstaller` class is the procedural escape hatch for plugin install logic that the declarative `install.yaml` manifest cannot express. - [When to Use](#when-to-use) - [Two Phases: IMMEDIATE vs DEFERRED](#two-phases-immediate-vs-deferred) - [DSL Method Reference](#dsl-method-reference) - [Prompts](#prompts) - [Pubspec Operations](#pubspec-operations) - [File Operations](#file-operations) - [Injection Operations](#injection-operations) - [Android Operations](#android-operations) - [iOS and macOS Operations](#ios-and-macos-operations) - [Web Operations](#web-operations) - [Environment Operations](#environment-operations) - [Shell Operations](#shell-operations) - [Lifecycle Hooks](#lifecycle-hooks) - [Atomic Commit Semantics](#atomic-commit-semantics) - [Example](#example) - [Related Documentation](#related-documentation) --- ## When to Use Prefer `install.yaml` for straightforward installs. Use the procedural DSL when: - **Complex conditional logic**: the install plan branches on runtime values (platform detection, user input, env vars) that YAML conditions cannot evaluate at parse time. - **Branching prompts**: the set of operations depends on answers collected during the install interaction (e.g. "Firebase or Amplitude?" selects different dependency blocks). - **Programmatic file generation**: installed files are assembled from captured prompt answers rather than fixed stub templates. When none of these apply, `install.yaml` + `ManifestInstaller` is simpler and preferred. --- ## Two Phases: IMMEDIATE vs DEFERRED Every chain method falls into one of two categories. This split determines whether a method's effect is visible during the chain or only after `commit()`. ### IMMEDIATE IMMEDIATE methods run synchronously the moment they appear in the chain. They do not enqueue an `InstallOperation`; they produce a side effect right now. | Method | Effect | |:---|:---| | `ask(...)` | Drives `InstallContext.prompt.ask(...)`; stores the answer in `_vars` | | `confirm(...)` | Drives `InstallContext.prompt.confirm(...)`; stores `'true'` or `'false'` | | `choice(...)` | Drives `InstallContext.prompt.choice(...)`; stores the selected option | | `startWith(hook)` | Registers a pre-commit callback (fires before op dispatch, every outcome) | | `endWith(hook)` | Registers a post-Success callback (skipped on `DryRun`/`Conflict`/`Error`) | Captured answers are readable via `installer.vars['key']` immediately, allowing the chain to branch on user input before enqueuing ops. ### DEFERRED DEFERRED methods append an `InstallOperation` to the internal queue. Nothing touches the filesystem until `commit()`. Ops execute in enqueue order. All `add*`, `inject*`, `write*`, `delete*`, `copy*`, `publish*`, `merge*`, `wrap*`, and `runShell` methods are DEFERRED. **Hybrid:** `askToRunShell` drives its prompt IMMEDIATELY but enqueues `RunShell` only when the user confirms, so the shell command still executes deferredly during commit. --- ## DSL Method Reference All methods return `this` (chainable) unless noted otherwise. ### Prompts | Method | Description | |:---|:---| | `ask(varName:, question:, [defaultValue:, validator:])` | Free-text prompt; stores answer in `vars[varName]` | | `confirm(varName:, question:, [defaultValue:])` | Yes/no prompt; stores `'true'` or `'false'` in `vars[varName]` | | `choice(varName:, question:, options:, [defaultValue:])` | Pick-one-of prompt; stores selected option string in `vars[varName]` | ### Pubspec Operations | Method | Operation | Description | |:---|:---|:---| | `addDependency(name, version)` | `AddDependency` | Adds a runtime dependency to `pubspec.yaml` | | `addDevDependency(name, version)` | `AddDependency(isDev: true)` | Adds a dev dependency to `pubspec.yaml` | | `addPathDependency(name, path)` | `AddPathDependency` | Adds a relative-path dependency to `pubspec.yaml` | | `removeDependency(name)` | `RemoveDependency` | Removes a dependency from `pubspec.yaml` (idempotent) | | `addPubspecAsset(assetPath)` | `AddPubspecAsset` | Appends an asset path to `flutter.assets` in `pubspec.yaml` (idempotent) | ### File Operations | Method | Operation | Description | |:---|:---|:---| | `publishConfig(stubName:, targetPath:, [replacements:])` | `PublishFile` | Loads a stub, applies token replacements, writes to `targetPath` | | `writeFile(targetPath:, content:)` | `WriteFile` | Writes raw programmatic content to `targetPath` | | `deleteFile(targetPath)` | `DeleteFile` | Deletes `targetPath` if it exists (idempotent) | | `copyFile(sourcePath:, targetPath:)` | `CopyFile` | Copies `sourcePath` to `targetPath` | | `mergeJson(targetPath:, sourceData:, [additive:])` | `MergeJson` | Deep-merges `sourceData` into the JSON file at `targetPath` | `mergeJson` defaults to additive mode (existing keys are preserved). Pass `additive: false` to allow source values to overwrite conflicting target keys. ### Injection Operations | Method | Operation | Description | |:---|:---|:---| | `injectImport(targetFile:, importStatement:)` | `InjectImport` | Appends an import line to any Dart file (idempotent) | | `injectBefore(targetFile:, pattern:, code:, [fallbackPattern:])` | `InjectBeforePattern` | Inserts code before the first match of `pattern` in `targetFile` | | `injectAfter(targetFile:, pattern:, code:, [fallbackPattern:])` | `InjectAfterPattern` | Inserts code after the first match of `pattern` in `targetFile` | | `injectMainDartImport(importStatement)` | `InjectMainDartImport` | Appends an import to `lib/main.dart` specifically (grouped in dry-run output) | | `injectBeforeMagicInit(code)` | `InjectIntoMainDart(beforeInit)` | Inserts code before `Magic.init(...)` in `lib/main.dart` | | `injectAfterMagicInit(code)` | `InjectIntoMainDart(afterInit)` | Inserts code after `Magic.init(...)` in `lib/main.dart` | | `wrapRunApp(wrapperName)` | `InjectIntoMainDart(wrapRunApp)` | Wraps the `runApp(...)` argument with the named widget constructor | | `injectProvider(providerClassName, [package:])` | composite | Adds import + appends `(app) => X(app),` to `lib/config/app.dart` providers list | | `injectConfigFactory(factoryName, [package:])` | composite | Adds import + appends `() => XConfig,` to `lib/main.dart` configFactories list | | `injectRoute(registerFunctionName)` | `InjectRouteRegistration` | Calls `registerFunctionName()` in `RouteServiceProvider.boot()` | `injectProvider` and `injectConfigFactory` each enqueue two operations (one `InjectImport` + one `InjectAfterPattern`). The regex starts at the list's own key (`'providers': [`, `configFactories: [`) and scans forward to the last entry before `]`, with `]` excluded from the gap so the scan cannot leave the list it opened. That anchor is load-bearing. `() => \w+,` before a `]` describes any zero-argument closure that is last in any list, and `firstMatch` scans by position, so without the key in front of it an earlier list anywhere in `lib/main.dart` won and the factory was appended to that list instead. An empty `configFactories: []` was the same defect wearing a second face: the primary matched a later list's last entry, so the fallback that exists for the empty case never ran. The lookahead skips a `//` comment between the last entry and the `]`: one trailing the entry on the same line (`(app) => AppServiceProvider(app), // core`), and comment-only or blank lines below it, which is where a scaffold placeholder lives (`// add plugin providers here`). A `/* block */` comment there is a miss and falls to the fallback. It cannot skip a real entry, since every line it consumes must be whitespace or a `//` comment through to the newline. The providers key takes either quote (`['"]providers['"]`). Anchoring on the key is what made its quoting matter at all, and `prefer_single_quotes` makes the double-quoted spelling uncommon rather than impossible. `injectProvider` accepts both spellings of the providers list entry, because the type annotation is optional in Dart and both are in use: `(app) => XServiceProvider(app),` and `(MagicApp app) => XServiceProvider(app),`. **A pattern injection that matches nothing fails the install, before anything is written.** The transaction checks every `InjectBeforePattern` and `InjectAfterPattern` against its target file ahead of the stage loop and returns `Error` naming every offending op. Before this they wrote nothing and the install reported Success, so a plugin whose pattern did not fit the host's file registered nothing and said nothing about it. The check is a preflight rather than an in-loop failure because helper-backed ops write through `dart:io` during staging, outside the `.tmp` rollback, and the install record `plugin:uninstall` reads is written later still. Failing mid-loop would leave an orphan import with nothing recorded to reverse it. An idempotent skip, where the code is already present, still counts as resolvable, so a re-run does not fail on work it has already done. **`fallbackPattern` keeps an empty list installable.** Both pattern ops take an optional second pattern, tried only when the primary matches nothing. The append-to-a-list shape anchors on the last entry before the closing bracket and so cannot match an empty list; the fallback anchors on the opening bracket instead. A single regex with an alternation cannot do this: `firstMatch` scans by position, the opening bracket always comes first, and every injection would land at the top of a populated list. It anchors the insertion rather than narrowing it to the empty case. A populated list whose last entry the primary does not describe falls through to the fallback too, and the code then lands at the top of that list instead of the end: correct Dart, wrong position. When you meet that, widen the primary rather than accept the fallback, which is what the trailing-comment tolerance above is. `install.yaml` has no pattern-injection op at all (see the manifest schema), so `fallbackPattern` is a property of the Dart DSL only. The install record persists `pattern` through `toString()` and neither pattern op has a reverse in V1, so `plugin:uninstall` logs `[skipped]` for both and reads neither field. ### Android Operations | Method | Operation | Description | |:---|:---|:---| | `injectAndroidPermission(permission)` | `InjectAndroidPermission` | Adds `` to `AndroidManifest.xml`; silently skipped on non-Android consumers | | `injectAndroidMetaData(name:, value:)` | `InjectAndroidMetaData` | Adds `` inside `` in `AndroidManifest.xml` | | `injectAndroidActivity(name:, exported:, [taskAffinity:], [intentFilters:])` | `InjectAndroidActivity` | Adds an `` with its `` elements inside `` in `AndroidManifest.xml`; idempotent by content (see below) | | `injectGradlePlugin(pluginId:, [version:])` | `InjectGradlePlugin` | Adds a plugin entry to the `plugins { }` block in `build.gradle.kts` | | `injectGradleDependency(scope:, notation:)` | `InjectGradleDependency` | Adds a dependency under `scope` in `android/app/build.gradle.kts` | ### iOS and macOS Operations | Method | Operation | Description | |:---|:---|:---| | `injectInfoPlistKey(key:, value:, [platform:])` | `InjectInfoPlistKey` | Sets a key in `ios/Runner/Info.plist` or `macos/Runner/Info.plist`; value may be `String`, `bool`, or `List` | | `injectInfoPlistUrlScheme(scheme:, [platform:])` | `InjectInfoPlistUrlScheme` | Registers a custom URL scheme under `CFBundleURLTypes` in `ios/Runner/Info.plist` or `macos/Runner/Info.plist`; `scheme` is written without the `://` suffix | | `injectEntitlement(platform:, key:, value:)` | `InjectEntitlement` | Sets a key in every entitlements file the application target signs with; when the project names none, writes `/Runner/Runner.entitlements` AND `CODE_SIGN_ENTITLEMENTS` into `/Runner.xcodeproj/project.pbxproj`; `platform` is `'ios'` or `'macos'`; value may be `String`, `bool` or `List` | | `injectPodfileLine([platform:], line:)` | `InjectPodfileLine` | Appends a CocoaPods pod declaration to the `target 'Runner'` Podfile block | Platform-scoped ops are silently skipped when the target platform directory is absent. #### injectAndroidActivity compares content `injectAndroidActivity` takes the activity's `name` and `exported`, an optional `taskAffinity` (`''` renders an empty affinity, `null` omits the attribute) and a list of `AndroidIntentFilter`, each with `autoVerify`, `actions`, `categories` and a list of `AndroidIntentData` (`scheme`, `host`, `path`, `pathPrefix`). `AndroidIntentFilter` and `AndroidIntentData` come from `package:dowel/dowel.dart`. ```dart installer.injectAndroidActivity( name: 'com.linusu.flutter_web_auth_2.CallbackActivity', exported: true, taskAffinity: '', intentFilters: [ AndroidIntentFilter( autoVerify: true, actions: ['android.intent.action.VIEW'], categories: [ 'android.intent.category.DEFAULT', 'android.intent.category.BROWSABLE', ], data: [ AndroidIntentData( scheme: 'https', host: 'auth.example.com', path: '/social/callback', ), ], ), ], ); ``` The manifest is parsed to decide, and the element is spliced in as text just before the real `` (a closing tag spelled inside an XML comment is ignored), so every other byte of the file keeps its bytes. The write is helper-backed, so the transaction's `.tmp` rollback does not cover it. | Manifest state | Result | |:---|:---| | No `` with this `android:name` under `` | The element is inserted | | One exists with an equal set of intent filters (same `autoVerify`, actions, categories and `` attributes, in any order) | No-op | | One exists with a different set | Left alone, never rewritten; the install finishes and warns with the block it expected so you can reconcile it by hand | `exported` and `taskAffinity` are not part of the comparison. A `` carrying an attribute other than `scheme`, `host`, `path` or `pathPrefix` compares as different. #### injectInfoPlistUrlScheme Appends one `CFBundleURLTypes` dict (`CFBundleTypeRole` = `Editor`, one `CFBundleURLSchemes` entry) beside the existing ones, creating the array when the plist has none. When ANY existing dict already lists the scheme the call is a no-op and the file is not rewritten. A `CFBundleURLTypes` value that is not an `` fails the install. #### injectEntitlement writes to the files Xcode signs with Xcode reads an entitlements plist only when the application target's `CODE_SIGN_ENTITLEMENTS` build setting names it. The op therefore asks the project which files the application target (the target whose `productType` is `com.apple.product-type.application`, never the test bundle, never the project-level defaults) already signs with, one per distinct `CODE_SIGN_ENTITLEMENTS` value across its build configurations, and sets the key in each of them. The build settings stay untouched. - A split project, a Debug and a Release configuration signing against different files, gets the key in both. A macOS Flutter project (`Runner/DebugProfile.entitlements` plus `Runner/Release.entitlements`) is this case. - A project whose configurations name no entitlements file at all gets `/Runner/Runner.entitlements` written AND the application target pointed at it through `CODE_SIGN_ENTITLEMENTS`. That second write edits `/Runner.xcodeproj/project.pbxproj`. - A `List` value is merged into the array the file already carries, entry by entry, so entries another plugin or the app put there survive. A value of any other type fails the install with an `Error`. All of these writes are helper-backed, so none is covered by the transaction's `.tmp` rollback. Plan for that when you stage this op. Only the last case, pointing the target at a new file, can leave the build setting alone. It prints a warning naming what to set by hand and lets the install finish instead of aborting on top of the writes that already landed: - The project has no `/Runner.xcodeproj` at all, so there is nothing to point at. - A configuration holds a non-string `CODE_SIGN_ENTITLEMENTS`, which the reader cannot resolve to a file and the editor will not overwrite. - The reader read the project and declined to edit it. Two shapes reach this. The re-emission does not match byte for byte, so it refuses to write a file it cannot reproduce exactly: Xcode escapes non-ASCII in that file as `\Uxxxx`, and an accented `PRODUCT_NAME` is enough. Or the project is readable but not addressable: no application target at all, two applications and neither named `Runner`, or a target carrying no build configurations. Both shapes answer to the same remedy, which is why they warn alike: the file is one Xcode can open, so setting the build setting there by hand works. A `project.pbxproj` that is not a valid project file at all still fails the install with an `Error`, and the project file is left untouched in every one of these cases. ### Web Operations | Method | Operation | Description | |:---|:---|:---| | `injectIntoWebHead(content)` | `InjectIntoWebHead` | Inserts raw HTML before `` in `web/index.html`; skipped on non-web consumers | | `addWebMetaTag(attributes)` | `AddWebMetaTag` | Adds a `` element with the given attribute map to `web/index.html` | ### Environment Operations | Method | Operation | Description | |:---|:---|:---| | `injectEnvVar(key:, value:, [comment:])` | `InjectEnvVar` | Writes `KEY=value` to `.env`; creates the file when absent; an optional `comment` is written as `# ` above the key line | ### Shell Operations | Method | Phase | Description | |:---|:---|:---| | `runShell(command:, [args:, workingDir:])` | DEFERRED | Enqueues a `RunShell` op that executes after all file mutations have landed | | `askToRunShell(prompt:, command:, [args:])` | HYBRID | Prompts immediately; enqueues `RunShell` only when the user confirms | Shell ops execute after all file mutations have landed. Non-zero exit surfaces as `Error`; the install record from the preceding phase stays on disk so `plugin:uninstall` can reverse file mutations independently. ### Lifecycle Hooks | Method | Phase | Description | |:---|:---|:---| | `startWith(hook)` | IMMEDIATE (registration); fires pre-commit | `void Function(InstallContext)` invoked before op dispatch, on every outcome | | `endWith(hook)` | IMMEDIATE (registration); fires post-Success only | Invoked after `Success`; never fires on `DryRun`, `Conflict`, or `Error` | --- ## Atomic Commit Semantics `commit()` delegates to `InstallTransaction.commit()` (see `lib/src/installer/install_transaction.dart:132-234`), which executes in seven phases: 1. **Dry-run short-circuit** (line 144): `dryRun: true` renders staged ops and returns `DryRun` without touching disk. 2. **Conflict pre-flight** (line 152): detects user-modified target files. `force: true` bypasses. 3. **In-memory staging** (line 162): ops are reduced into `Map` where `null` marks a delete. No disk writes yet. 4. **Atomic `.tmp` writes** (line 173): each non-null entry is written to `.tmp`. If any write throws, all successful temps are deleted and the method returns `Error(rolledBack: true)`. 5. **Rename into place** (line 193): `.tmp` files are renamed over their targets; deletes are applied. POSIX `rename(2)` is atomic, so readers never see partial state. Failures past this point surface as `Error(rolledBack: false)`. 6. **Install record** (line 213): `.dowel/installed/.json` is written BEFORE shell ops so the install is always reversible even when a shell step fails later. 7. **Shell ops** (line 228): `RunShell` ops execute last. A non-zero exit returns `Error`; the record from phase 6 stays intact. ### One-shot guard `PluginInstaller._committed` flips to `true` at the start of `commit()`. A second call throws `StateError` regardless of the first call's outcome. Construct a fresh `PluginInstaller` per install pass. ### V1 reversibility `plugin:uninstall` reverses `WriteFile`, `DeleteFile`, and `CopyFile` (hash-verified). Injection ops and helper-backed ops (pubspec, native, web, env) log `[skipped]` in V1. V1.1 will introduce anchor-bracket markers for reversible injections. --- ## Example Pattern from `assets/stubs/make_plugin/magic/install_command.dart.stub`, with a conditional backend branch illustrating IMMEDIATE prompt + DEFERRED ops: ```dart import 'package:dowel/dowel.dart'; class AnalyticsInstallCommand extends DowelInstallCommand { @override String get signature => 'analytics:install $baseFlags'; @override String get description => 'Install the Analytics plugin into the host project.'; @override String pluginName(DowelContext ctx) => 'analytics'; @override Future handle(DowelContext ctx) async { final installer = PluginInstaller(buildContext(ctx), pluginName: pluginName(ctx)); // 1. IMMEDIATE prompt: answer is readable in vars before any op is enqueued. installer.choice( varName: 'backend', question: 'Which analytics backend?', options: ['firebase', 'amplitude'], defaultValue: 'firebase', ); // 2. Common DEFERRED ops. installer .publishConfig( stubName: 'install/analytics_config.dart', targetPath: '${buildContext(ctx).projectRoot}/lib/config/analytics.dart', replacements: {'BACKEND': installer.vars['backend']!}, ) .injectProvider('AnalyticsServiceProvider') .injectEnvVar(key: 'ANALYTICS_KEY', value: '', comment: 'Analytics write key.'); // 3. Backend-specific ops (DEFERRED, conditional on captured answer). if (installer.vars['backend'] == 'firebase') { installer .addDependency('firebase_core', '^3.0.0') .addDependency('firebase_analytics', '^11.0.0') .injectAndroidPermission('android.permission.INTERNET') .mergeJson(targetPath: 'assets/lang/en.json', sourceData: {'analytics': {'title': 'Analytics'}}); } else { installer.addDependency('amplitude_flutter', '^4.0.0'); } // 4. Hybrid: prompt fires now; RunShell enqueued only when user confirms. installer.askToRunShell(prompt: 'Run "flutter pub get" now?', command: 'flutter', args: ['pub', 'get']); final result = await installer.commit(dryRun: isDryRun(ctx), force: isForce(ctx)); return switch (result) { Success() => 0, DryRun() => 0, Conflict() => 1, Error() => 2 }; } } ``` ## Related Documentation - [install-yaml.md](https://anilcancakir.com/dowel/docs/plugins/install-yaml.md): declarative manifest schema; preferred for straightforward installs. - [authoring.md](https://anilcancakir.com/dowel/docs/plugins/authoring.md): end-to-end plugin authoring guide (scaffold, provider registration, publish checklist). --- # Signature DSL The signature DSL compresses a command's name, positional arguments, and options into a single Dart string. `CommandSignature.parse` (`lib/src/console/command_signature.dart:57`) splits that string at registration time; the registry applies it to the underlying `ArgParser`. - [Overview](#overview) - [Grammar](#grammar) - [Token Types](#token-types) - [Argument Modifiers](#argument-modifiers) - [Option Modifiers](#option-modifiers) - [Description Annotation](#description-annotation) - [Name Validation](#name-validation) - [Error Handling](#error-handling) - [Examples](#examples) - [Related Documentation](#related-documentation) --- ## Overview ```dart String get signature => 'cmd:name {arg} {arg?} {arg=default} {--flag} {--option=val}'; ``` The signature string is the primary way to declare a command's surface. The alternative is `configure(ArgParser)`, used when the DSL cannot express the required structure (multi-allowed options, allowed-values lists). The built-in commands use whichever fits: some declare a `signature`, some `configure(ArgParser)` with flags, and some have no flag surface at all. --- ## Grammar ``` signature := name (whitespace token)* name := lowercase with optional colon-separated namespace token := '{' (argument | option) '}' argument := name [modifier] [' : ' description] option := '--' name [value-spec] [' : ' description] modifier := '?' | '*' | '?*' | '=' default value-spec := '=' | '=' default ``` `CommandSignature.parse` (`command_signature.dart:57`) runs four stages: 1. **Extract name** (`command_signature.dart:65`): split on the first whitespace or `{` via `RegExp(r'[\s{]')`; everything before is the name. 2. **Extract token blocks** (`command_signature.dart:82`): scan the tail with `RegExp(r'\{([^}]+)\}')`; each capture is one token body. 3. **Classify each token** (`command_signature.dart:86`): bodies starting with `--` route to `_parseOption`; others route to `_parseArgument`. 4. **Parse modifiers**: arguments check `=` first (default value), then suffix `?*` / `*` / `?`. Options check `=` to distinguish a value option from a boolean flag. A ` : ` separator (`RegExp(r'\s+:\s+')`, `command_signature.dart:218`) strips the description annotation before modifier parsing in both branches. --- ## Token Types | Token form | Type | Read via | |:---|:---|:---| | `{name}` | Positional argument | `ctx.input.argument('name')` | | `{--name}` | Boolean flag | `ctx.input.flag('name')` | | `{--name=}` | Value option, no default | `ctx.input.option('name')` | | `{--name=default}` | Value option with default | `ctx.input.option('name')` | --- ## Argument Modifiers | Syntax | `isOptional` | `isVariadic` | `defaultValue` | Behaviour | |:---|:---|:---|:---|:---| | `{name}` | false | false | null | Required positional. | | `{name?}` | true | false | null | Optional; `argument()` returns null when absent. | | `{name=default}` | true | false | `"default"` | Inline default; implies optional. | | `{name*}` | false | true | null | Variadic, one or more trailing values. | | `{name?*}` | true | true | null | Optional variadic, zero or more. | Modifiers are parsed in declaration order (`command_signature.dart:125`): `=` check first, then suffix `?*` before `*` before `?`. --- ## Option Modifiers | Syntax | `isFlag` | `defaultValue` | Behaviour | |:---|:---|:---|:---| | `{--flag}` | true | null | Boolean switch; absent means false. | | `{--option=}` | false | null | Value option; `option()` returns null when absent. | | `{--option=value}` | false | `"value"` | Value option with inline default. | `CommandSignature.applyTo` (`command_signature.dart:103`) calls `parser.addFlag` (with `negatable: false`) for booleans and `parser.addOption(defaultsTo: opt.defaultValue)` for value options. --- ## Description Annotation Append ` : text` after a token's name and modifiers to attach a description: ``` {name : Argument description} {--flag : Boolean flag description} {--option=default : Value option description} ``` `_splitDescription` (`command_signature.dart:205`) matches the first `\s+:\s+` in the body and returns `(head, description)`. The `description` flows into `ArgParser` as `help` for options and flags; for arguments it is stored on `ArgumentSpec` for introspection. --- ## Name Validation Both patterns are defined in `command_signature.dart:214-216`. **Command name** (`_namePattern`): ``` ^[a-z0-9_]+([:-][a-z0-9_]+)*$ ``` Allows colon-namespaced or hyphen-namespaced segments (e.g. `plugin:install`, `sync-monitors`). Uppercase, dots, or consecutive separators throw. **Argument and option names** (`_argNamePattern`): ``` ^[a-z0-9][a-z0-9_-]*$ ``` Must start with a lowercase letter or digit; subsequent characters may include underscores and hyphens. Both patterns throw `FormatException` at parse time with the invalid value and expected pattern quoted in the message. --- ## Error Handling When a command is invoked with an option the parser does not recognise, the dispatcher fails loudly: it writes `Unknown option: ` to stderr, prints the command help, and exits with a non-zero code. This applies to both long (`--unknown`) and short (`-x`) forms. ``` $ dowel make:command Foo --bogus ✗ Unknown option: --bogus ℹ Description: ... ``` Every other parse failure keeps its original, specific message: | Cause | Example invocation | Message | |:---|:---|:---| | Unknown option | `--bogus`, `-x` | `Unknown option: ` | | Missing option value | `--output` (value required) | `Missing argument for "--output".` | | Disallowed value | `--mode=zoom` (not in `allowed`) | `"zoom" is not an allowed value for option "--mode".` | | Value on a flag | `--force=1` | `Flag option "--force" should not be given a value.` | `--help` / `-h` and every valid invocation are unaffected: they parse and behave exactly as before. --- ## Examples ### tinker (`lib/src/commands/tinker_command.dart:22`) ```dart String get signature => 'tinker ' '{--eval= : Evaluate a single Dart expression in the running app, ' 'print the formatted result on stdout, and exit. Pipe-friendly.}'; ``` `CommandSignature` result: name `tinker`, no arguments, one value option `eval` (`isFlag: false`, `defaultValue: null`). `ctx.input.option('eval')` returns `null` in REPL mode and the expression string when `--eval` is set. --- ### plugin:install (`lib/src/commands/plugin_install_command.dart`) ```dart String get signature => 'plugin:install ' '$baseFlags' '{name : Plugin pubspec package name (e.g. magic_logger)} ' '{--bootstrap-command= : Sub-command to chain after registration} ' '{--use-yaml-only : Fail if install.yaml not found}'; ``` `CommandSignature` result: name `plugin:install`, one required positional argument `name`, two own options (`bootstrap-command` value, `use-yaml-only` flag), plus the options from `baseFlags` expansion. --- ### plugin:uninstall (`lib/src/commands/plugin_uninstall_command.dart:54`) ```dart String get signature => 'plugin:uninstall ' '$baseFlags' '{name : Plugin pubspec package name}'; ``` `CommandSignature` result: name `plugin:uninstall`, one required positional argument `name`, no own options beyond those from `baseFlags` expansion. --- ## Related Documentation - [`../commands/`](https://anilcancakir.com/dowel/docs/commands/index.md): per-command reference with full signature listings and usage examples.