What does the dartway command do?
It is the front door and the toolbox of a project: one command prints the whole setup instruction, one checks the machine, one creates a project, two install and update the agent toolkit, one runs the generator, one checks the conventions, one serves the web app and the server on one origin, one runs the server tests on a database of their own, one deploys, and one counts lines.
dart pub global activate dartway_cli
Inside a project, run the CLI the project pins. Every DartWay project carries dartway_cli as a
dev dependency of its Flutter package, at the same version as the rest of the framework it pins:
cd <project>_flutter
dart run dartway_cli:dartway generate # the project's own CLI, whatever is on PATH
A globally activated dartway is a second copy with a life of its own, and the two drift — a global
CLI from before a release has no generate at all, while the project it is standing in has no
other way to write *.dw.dart. What that looks like is "unknown command", three steps from the
cause. So generate, check, test, deploy, dev and stats
refuse to run when the CLI that started them is not the one the project pins, and say which command
would have worked. create and quickstart (there is no project yet), update and setup-ai
(they repair the pins) and doctor (it touches nothing) run either way.
The complete, current option list of any command is dartway help <command> — and for the
nested ones, dartway help secret push. That output is generated from the parser, so it
cannot drift from the code. This page does not restate every flag. It names the ones whose
meaning is not obvious from a one-line help string, and states the defaults a reader has to know
before relying on them.
The commands, as packages/dartway_cli/bin/dartway.dart registers them: quickstart, doctor,
create, setup-ai, update, generate, check, dev, deploy, secret, stats, test. A
usage error
exits 64; a refusal the command explains exits 1.
There is no migrate command. Migrations belong to the project: dart run bin/migrate.dart <apply | rollback | status | create <name> | check | rehash> in the server package, against the
database named by DW_DATABASE_*. The server also applies them as it starts. See
Migrations.
Where the CLI takes the framework from
The CLI does not carry the framework inside itself. create, setup-ai and update read the
DartWay monorepo — template/ and toolkit/ — from the first of these that applies:
- a local checkout named by
--local-repo,--framework-path(create) orDARTWAY_MONOREPO_DIR; - a channel that was chosen —
--channel,DARTWAY_BRANCH, or forupdatethe channel the project recorded: a shallow clone of that branch, cached in~/.dartway/monorepoand refreshed with a shallow fetch on every run; - the checkout the CLI runs from, when it runs from one — activated with
--source pathfrom a checkout, or with--source git(pub keeps a clone of the repository at the ref), or run withdart runinside the monorepo; - otherwise the channel
stable.
So a CLI that has the framework beside it hands out the template and the toolkit of its own
revision: activated from a checkout ahead of stable, dartway create makes a project of that
checkout rather than of whatever stable holds. A CLI with nothing beside it — installed from
pub.dev — takes the channel. The install prints which source it used and records it in
.agents/dartway-toolkit.json.
| Variable | Meaning |
|---|---|
DARTWAY_BRANCH | The channel for create, setup-ai and update when --channel is not given; chosen, so it wins over the checkout the CLI runs from |
DARTWAY_MONOREPO_DIR | A local monorepo checkout to use instead of cloning |
DARTWAY_REPO_URL | Another monorepo git URL (default https://github.com/dartway/dartway.git) |
dartway quickstart — the instruction, printed
Prints the full setup brief to stdout: what the machine needs, how to create a project, the order of the bring-up steps and why, how to verify the server answers, how to hand over the sign-in, and the checks to run before calling a change done. It writes nothing and asks nothing.
It is a printed text rather than an extension for one assistant on purpose: whatever agent you
use, its way in is two commands — dart pub global activate dartway_cli and dartway quickstart — after which the instruction is in that agent's context. A plugin for one vendor would
make the front door of an open framework depend on a format nobody here controls. A human reads the
same text; there is no second, friendlier version to drift from it. The source is
packages/dartway_cli/lib/src/quickstart_brief.dart.
dartway doctor — is this machine ready?
Six checks, each with the exact fix when it fails:
| Check | Why it is here |
|---|---|
Dart >=3.11.0 | The SDK running the CLI is the one that runs the project |
Flutter >=3.44.0 | |
git, with user.name and user.email | create commits the new project. A missing binary fails; a missing identity warns — the project is complete, but has no initial commit |
| A pub host that answers | pub get sets no deadline on a connection that opens and goes quiet, so a filtered route surfaces as a resolve step that hangs without a word. The probe asks for bytes rather than a socket (a TCP connect succeeds even when the TLS handshake after it is filtered), waits 10 seconds, and honours PUB_HOSTED_URL |
| A responding Docker daemon | Postgres and the storage come from it by default. Tests can instead use explicit --database-url / --storage-url servers. Not installed and not running are reported apart |
The pub global bin directory on PATH | The cause of dartway: command not found right after a successful install. A warning: dart pub global run dartway_cli:dartway works regardless |
Exit code 1 when anything fails, 0 otherwise (warnings included), so an agent or a CI step can
branch on it.
dartway create <name> — a project that already runs
dartway create my_app
dartway create . # the current, empty folder becomes the project
The source is template/ in the monorepo: a skeleton, not somebody's product — sign-in by a
one-time code, profiles and roles, navigation with zone guards, an admin panel, a UI kit as source,
tests on both sides, and no domain models. The full application on the same framework lives in
example/ and is a reference to read, not a project to inherit.
You get my_app_shared, my_app_server and my_app_flutter, the agent toolkit in .agents/ and the selected .claude/ integration
and docs/dev_notes/, and a git repository with an initial commit.
What the copy does beyond copying:
- renames
dartway_starter→my_app,DartwayStarter→MyApp,dartwayStarter→myAppanddartway-starter→my-app(the storage bucket names) in every path and every text file; binary files are copied verbatim; - skips
.dart_tool,build,.git,.idea,.fvm,ephemeral,node_modulesandpubspec.lock; - runs
dart formatoverbin/,lib/andtest/of every package — a rename changes the length of names, and without this the first commit would not be formatted anddart run dartway_cli:dartway generate --checkwould depend on where lines happened to break; - strips the monorepo-only
dependency_overridesblock (with the comments above it) from every package pubspec. Those overrides point at sibling folders of the monorepo; in a project they lead nowhere, and what is left resolves from pub.dev; - installs the toolkit with base branch
masterand the given--languageand--notes-tracker(seesetup-aibelow); - unless
--no-git:git init(skipped when the folder is already a repository),git add -A, and an initial commit.
The name must be a lower_snake_case Dart identifier without leading, trailing or doubled
underscores, at most 55 characters — it becomes package names, and <name>-private has to fit the
63 characters S3 allows a bucket. dartway_starter itself is refused. The target directory must not
exist; create refuses rather than merging into it.
With . the folder names the project, the way flutter create . does: dartway-demo/ becomes
dartway_demo. The folder has to be empty; a .git in it is allowed, and the initial commit then
lands in that repository.
| Option | Default | Meaning |
|---|---|---|
--channel | DARTWAY_BRANCH, else the checkout the CLI runs from, else stable | Monorepo branch to take the template and the toolkit from |
--local-repo | DARTWAY_MONOREPO_DIR | A local monorepo checkout instead of a clone |
--framework-path | — | Resolve the framework packages from a local monorepo checkout by path: each pubspec gets dependency_overrides onto <monorepo>/packages for exactly the dartway_* packages it reaches. The template and the toolkit come from the same checkout unless --local-repo names another |
--language | English | The app's one UI language — en, ru, or English, Russian: the project keeps only that .arb, makes it the template ARB and AppLocaleController.productLocale, and regenerates lib/l10n/gen (a language the skeleton has no translation for starts in English, with a note). Also the language the project writes its own texts in — feature specs, doc comments, docs/dev_notes/ |
--notes-tracker | dartway/dartway | GitHub repository where findings about the framework are filed; none files nothing outside the project |
--[no-]git | on | Initialize a repository with an initial commit |
--framework-path is how a project is built against a framework checkout that is not published —
this tree's own unreleased changes, or a fork. The rewrite's package family (^0.20.0) is on
pub.dev, so a project created normally resolves on its own; --framework-path instead resolves the
same pubspecs against the local checkout the template was written against. The block it writes says
to remove it once the versions it points at are published.
The last thing create prints points at dartway doctor and dartway quickstart and says to ask
whatever assistant you use to bring the project up. The manual sequence is in the project's
README.md.
dartway setup-ai — the toolkit in a project you already have
dartway setup-ai --base-branch develop
Installs the agent toolkit into the project: .agents/DARTWAY.md, the dartway-* skills, the
/commit and /dartway-checkup commands, a merged .claude/settings.json, and
docs/dev_notes/. What each of those is, and which files the installer owns, is
The agent toolkit.
The project root is git rev-parse --show-toplevel, or the current directory outside a
repository. The layout is detected by directory suffix: exactly one *_shared, one *_server and one
*_flutter are required; none or two of a kind stops the command with a layout error rather than a
guess.
| Option | Default | Meaning |
|---|---|---|
--agent | both | Install codex, claude or both; later installs preserve the recorded choice |
--base-branch | master | Base branch of this project, used by the commit and PR instructions |
--language | English | The language the project writes its own texts in. Package APIs and error strings stay English |
--notes-tracker | dartway/dartway | Where framework findings are filed; none keeps them in the project |
--channel | DARTWAY_BRANCH, else the checkout the CLI runs from, else stable | Monorepo branch to take the toolkit from |
--local-repo | DARTWAY_MONOREPO_DIR | A local checkout instead of a clone |
An explicit flag wins, what the project recorded comes next, the default comes last. The install
records its provenance and settings in .agents/dartway-toolkit.json — source, channel, commit, CLI
version, and the three settings above — and a re-run without --base-branch, --language or
--notes-tracker replays the recorded ones. Without that, a plain re-run would reset a project's
language and tracker, and the diff would look like any update.
A channel switch nobody asked for is refused. When the project recorded one channel, --channel
was not given, and the default is another, the command stops before fetching anything and names both
channels. Naming either one proceeds: moving between channels is a decision, and it ends up written
in the command that ran. A named local checkout ignores the channel and records none, so it is never
refused. The checkout the CLI runs from is a default like stable, and is refused the same way for a
project that recorded a channel: --channel <recorded> stays, --local-repo <checkout> switches.
Commit .agents/, selected .claude/ files, root instruction blocks and docs/dev_notes/ afterwards.
dartway update — carry the project onto a newer framework
Plan the update before editing the project:
dartway update --plan --channel master
dartway update --plan --target <full-commit-sha> --channel master
dartway update --target <full-commit-sha> --channel master
--plan is read-only for the project: no toolkit, plugin configuration, lock or migration state
is written. It selects an exact committed framework tree and reports its SHA, package gaps and
unconfirmed migration notes with their instructions. Only committed files are used, including
with --local-repo; uncommitted framework edits are excluded. Later runs use the printed full
40-character SHA through --target, which is required for any write. Keep the same source options
and installer choices. A moving channel cannot change what that target contains.
The channel defaults to the recorded toolkit channel. Installer options match setup-ai, including
--agent codex|claude|both, language, base branch and tracker; recorded choices and owner rules are
preserved. --local-repo selects a local source. If the running CLI is older than the target's CLI,
the report says so.
Package gaps compare current locks with target versions; they are dependency information, not
migration completion. A lock already at target still has unconfirmed notes. Without locks, declared
framework dependencies still select notes. Notes remain keyed to their existing affects: package
versions and are bounded by the target's versions. Unreadable notes stop the update explicitly.
No verified records means an unknown migration baseline. Review relevant notes even if the project was recently created, dependencies were raised by hand, or its toolkit was just installed. The CLI never converts toolkit provenance or lock versions into completed notes. Older notes can be verified as already satisfied or not applicable; that requires an explicit disposition.
Before installing or editing analysis_options.yaml, the CLI resolves the proposed dartway_lints
dependency through pub in a temporary package, including its transitive analyzer constraints.
Hosted pins are raised only when resolvable; existing git and path choices are retained and checked.
Diagnostic settings stay unchanged in the plugin configuration; only its dependency source is
passed to pub, using the native analyzer's dependency YAML form. Choose exactly one version,
git or path source. A custom hosted URL must be a
string alongside version. Ambiguous or invalid native source forms fail before installation,
because pub and the native analyzer can otherwise select different sources from the same map.
An unpublished plugin version, invalid source or resolution failure returns a nonzero exit code
and leaves project files untouched. Choose a resolvable hosted target or project-owned git/path
configuration. --framework-path supplies a local plugin path when adding a missing plugin; it
preserves an existing source choice. Pub cache activity occurs outside the project.
After successful preflight, update --target installs the selected toolkit integrations and applies
the planned lint configuration edit. It edits no application code, bumps no dependencies and completes
no migration notes. The dartway-update skill guides the project edits, regeneration and verification.
Record each note only after checking its edits or applicability:
dartway update --target <sha> --channel master --complete docs/migrations/<note>.md --verified --verification "Checks run and actual results"
dartway update --target <sha> --channel master --not-applicable docs/migrations/<note>.md --verified --verification "Inspected usage and why this project is unaffected"
These completion commands write only .dartway/migrations.json. They require explicit verification
and evidence, and preserve all other records. Repeat a flag to record a verified batch; invalid or
conflicting selections write nothing. The ledger stores note path, package-version metadata,
applied or not-applicable, verification evidence, exact target and verification time. Commit it
with the update. Partial completion and repeated planning retain every outstanding note; there is
no implicit fully-migrated baseline or bulk version marker. See Migration notes.
dartway generate — the generated code
cd <project>_flutter
dart run dartway_cli:dartway generate # write
dart run dartway_cli:dartway generate --check --contract-base <trusted-SHA> # read-only freshness and compatibility
dart run dartway_cli:dartway generate -v # list every file written or removed
Runs dartway_generator over the project: DTO codecs (*.dw.dart parts) and the protocol registry
(lib/generated/dw_protocol.dart) and sorted lib/generated/dw_contract.json in *_shared, table definitions and the schema
(lib/generated/dw_schema.dart) in *_server. What is generated from what is
Data objects and generation.
The CLI does not link the generator in; it runs the one the project resolved. The generator
pins an analyzer, and a globally activated CLI carrying it would force one analyzer on every
project it touches — while the generator has to match the dartway_core_shared and dartway_orm
the project builds against. So the command walks up to the directory holding the *_server and
*_shared packages, looks for dartway_generator in their package config (the server package
first, where the skeleton declares it as a dev dependency), and launches its resolved entry point directly with the server package config. Only when no package resolves it does it fall back to a globally activated
dartway_generator; without either it stops and says how to add one. Run dart pub get first: an
unresolved package has no package config to find the generator in.
The exit code is the generator's. --check is what CI runs, and what dart run dartway_cli:dartway check reports as
generatedCodeStale and projectContractVersion.
dartway check — the conventions, enforced
cd <project>_flutter
dart run dartway_cli:dartway check
dart run dartway_cli:dartway check --type forbiddenUiUsage
dart run dartway_cli:dartway check --level error
dart run dartway_cli:dartway check --dir lib/app/invoices
Runs the convention checks from the project root or from inside the *_flutter package: the
Flutter package, the declared top level of both packages, localization wiring, generated code,
migrations and framework locks. Exit 1 when any error-severity finding is reported, 0 otherwise.
| Option | Meaning |
|---|---|
--type | One check by name (case-insensitive); an unknown name is a usage error |
--level | Only checks of one severity: info, warning or error |
--dir | Only one folder of the Flutter package (relative to it); skips every project-wide pass |
What each check means, which ones fail, and why is The conventions checker.
dartway dev — the web app and the server on one origin
cd <project>_flutter
dart run dartway_cli:dartway dev web # flutter run -d web-server + the proxy
dart run dartway_cli:dartway dev web --api http://localhost:8080 -- --profile # after -- goes to `flutter run`
dart run dartway_cli:dartway dev proxy # the origin alone, in front of servers you run
dart run dartway_cli:dartway dev proxy --web http://localhost:5000 # `flutter run -d web-server --web-port 5000`
dart run dartway_cli:dartway dev proxy --web-dir build/web # a built app instead
The server answers no CORS, in development too (D-039). A deployed web app reaches /dw/* and
/health on its own origin, because the front proxy serves the app and proxies those paths beside
it — see Deploying the server. flutter run -d chrome breaks that on a laptop: the app
is served from a port of its own and every call is cross-origin. dart run dartway_cli:dartway dev restores the deployed
shape: one origin, http://localhost:8000 by default, where /dw/* (the /dw/live socket
included) and /health go to the server and everything else to the web app.
Both subcommands take:
| Option | Default | Meaning |
|---|---|---|
--port, -p | 8000 | Port of the origin to open in the browser |
--api | http://localhost:8080 | The running server |
--api-path | — | A project door (a DwHttpRoute path such as /mcp) that goes to the server too. Repeatable. In production such doors live on the API host, which proxies everything; locally they share the one origin, so the proxy has to be told |
dev web runs flutter run -d web-server --web-port <free port> --web-hostname localhost --dart-define=DW_BACKEND_URL=<the proxy origin> in the *_flutter package, starts the proxy in
front of it, and prints the address once Flutter's server is up. Open that address, not the one
Flutter prints. Hot reload works through the proxy. Ctrl+C stops Flutter and the proxy together.
The Flutter command is --flutter when given, otherwise the project's FVM SDK
(.fvm/flutter_sdk), fvm flutter when .fvmrc pins a version, and flutter from PATH.
dev proxy is the same origin without Flutter. --web (default http://localhost:5000) points
at a web dev server you run; --web-dir at a build, served the way the web image serves it —
index.html for any path that is not a file, the Cache-Control of the project's own web image
configuration (the skeleton's rules when there is none), and an ETag. The two options exclude each
other.
Why no allowed-origins setting is needed. The server lets a browser open the live socket when
the page's Origin names the host the request was sent to. The proxy passes the browser's Host
through unchanged — as the deployed Nginx does with proxy_set_header Host $http_host — so the page
origin http://localhost:8000 and the host localhost:8000 match. X-Forwarded-For, X-Real-IP
and X-Forwarded-Proto are added as Nginx adds them; upgraded sockets are tunnelled as bytes.
Two things to know:
localhostand127.0.0.1are different origins to a browser. The app is built against the origin the command prints; opened under the other name, its calls are cross-origin again.- The server is started separately. Until it listens, the proxy answers its paths with
502and says so once in the terminal; calls go through as soon as it is up.
dartway test — the server tests, on a database of their own
cd <project>_flutter
dart run dartway_cli:dartway test # finds the project from here
dart run dartway_cli:dartway test -- --name 'sign-in' # everything after -- goes to `dart test`
dart run dartway_cli:dartway test --keep # leave the containers up to look inside them
For environments without Docker, --database-url and --storage-url select explicit test servers.
Hosts must resolve to loopback unless --allow-remote-test-server is supplied; inherited service
variables never select servers. See Servers supplied by a cloud environment
for setup and run-scoped cleanup.
By default, starts a Postgres and a storage for this run on ports Docker picks, waits until both accept
connections, runs dart test in the server package with DW_DATABASE_* and DW_STORAGE_* in its
environment, and removes both containers afterwards — Ctrl+C included. Each test file then creates
a database and buckets of its own.
| Option | Default | Meaning |
|---|---|---|
--keep | off | Leave the containers running and print how to reach and remove them; container mode only |
--image | postgres:17-alpine | Postgres image — the one a deployment runs |
--[no-]storage | on | Start the storage beside Postgres; --no-storage for a server without uploads |
--storage-image | rustfs/rustfs:1.0.0 | Storage image — the one a deployment runs |
--database-url | unset | Explicit Postgres server and maintenance database, with a CREATEDB role; without a storage URL, storage is disabled |
--storage-url | unset | Explicit HTTP/HTTPS path-style S3 server and credentials |
--allow-remote-test-server | off | Permit explicit servers outside loopback, for a deliberate dedicated test server |
Why a database per run rather than a compose service, and how a suite uses it, is Testing.
dartway deploy — the server, without a folder of shell scripts
cd <project>_flutter
dart run dartway_cli:dartway deploy setup --env staging
dart run dartway_cli:dartway deploy check --env staging
dart run dartway_cli:dartway deploy run --env staging
dart run dartway_cli:dartway deploy run --env staging --revision "$GITHUB_SHA"
dartway secret set SMS_API_TOKEN --env staging
setup provisions a server and renders its Compose and Nginx configuration, run deploys, and
check asserts that a deployment would work without changing anything. run --revision <sha>
pins a CI deployment to its verified commit and refuses if the server has already moved past it.
Everything is
described by deploy/config.yaml. The whole story is Deploying the server.
All three first check that the server runs this project's stack, or none at all, and refuse when it
runs only other ones — a repository that moved without project: pinning the name its stack has on
the server (Moving the repository). deploy setup --new-stack
sets up a genuinely second project on a host that already runs others: it skips that one check and
nothing else.
dartway secret — the values that are not in Git
dartway secret list --env local # this machine: both halves, names only
dartway secret set SMS_API_TOKEN --env local # the value is read from stdin
dartway secret list --env staging # a server: the store, names only
One command for every environment, because local is an environment. What differs is where the
values live: for a server, a file on it (init, set, list, put-file, push, pull); for
local, deploy/secrets.yaml > local on this machine, where init, put-file, push and pull
have nothing to do and say so. Values are never printed — a question about a secret is answered
by its name, its file, and whether it is empty. Deploying the server has the
whole story.
dartway stats — what actually grew
cd <project>_flutter
dart run dartway_cli:dartway stats
Files, total lines, average, maximum and minimum per top-level folder of the Flutter package's
lib/ whose name starts with app, auth, common or admin, plus a total. No grades and no
opinions: the counter you read before and after a refactor. Run it from the project root or inside
the *_flutter package.
generate and check accept --contract-base <revision>, resolved once to a reported commit SHA.
generate --check defaults to the project base-branch merge-base; CI explicitly supplies its trusted
base revision. Generation with a baseline refuses incompatible/unverified results before writing.
Generation without a baseline only emits artifacts; it makes no compatibility claim.
The CLI launches the already-resolved generator with its package config directly, avoiding implicit
pub resolution or lock edits during checks. Resolve dependencies separately first.
Generator exit 1 means stale output or a generation error, exit 3 means a contract incompatibility
or unverified comparison. CLI check exposes the latter as error-severity projectContractVersion
and exits 1. See the codec-based rules and first adoption.