# 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/<hash>/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=<path>` 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=<target>] [--port=<n>] [--vm-service-port=<n>]
                       [--[no-]dds] [--[no-]profile-static] [--cdp-port=<n>]
                       [--timeout=<n>] [--flutter-arg=<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=<n>` 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=<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/<hash>/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=<n>` / `FLUTTER=<n>` format. `start` captures those PIDs from the wrapper's stdout, then polls the log file at `~/.dowel/sessions/<hash>/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 <Platform> 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 <pid>`), 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/<token>/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=<n>` or stop the occupying process with `lsof -ti:<port> | xargs kill`.

**`mkfifo` permission denied.** `start` creates the FIFO at `~/.dowel/sessions/<hash>/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/<hash>`), or remove only that project's directory (`rm -rf ~/.dowel/sessions/<hash>`) 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 <n>) is still building after <n>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 <n>) 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/<hash>/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.<name>')` 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.<name> 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 <n>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 <n> is already in use; pass --cdp-port <free-port> or free it before running start.` Choose a free port (for example `--cdp-port=9224`) or kill the occupying process with `lsof -ti:<port> | xargs kill`.

## Related Documentation

- [stop](https://anilcancakir.com/dowel/docs/v0.0/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 <serial> shell am force-stop <applicationId>`, 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/v0.0/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/v0.0/commands/index.md): send `r\n` to the FIFO for a hot reload without a full restart.
- [hot-restart](https://anilcancakir.com/dowel/docs/v0.0/commands/index.md): send `R\n` to the FIFO for a hot restart that resets app state.
- [mcp:serve](https://anilcancakir.com/dowel/docs/v0.0/commands/mcp-serve.md): start the stdio JSON-RPC MCP server; reads this project's session to discover the running app.
