Skip to main content

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 --source git https://github.com/dartway/dartway --git-path packages/dartway_cli --git-ref stable

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:

  1. a local checkout named by --local-repo, --framework-path (create) or DARTWAY_MONOREPO_DIR;
  2. a channel that was chosen — --channel, DARTWAY_BRANCH, or for update the channel the project recorded: a shallow clone of that branch, cached in ~/.dartway/monorepo and refreshed with a shallow fetch on every run;
  3. the checkout the CLI runs from, when it runs from one — activated with --source path from a checkout, or with --source git (pub keeps a clone of the repository at the ref), or run with dart run inside the monorepo;
  4. 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.

VariableMeaning
DARTWAY_BRANCHThe 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_DIRA local monorepo checkout to use instead of cloning
DARTWAY_REPO_URLAnother monorepo git URL (default https://github.com/dartway/dartway)

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 --source git https://github.com/dartway/dartway --git-path packages/dartway_cli --git-ref stable 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?​

Checks, each with the exact fix when it fails:

CheckWhy it is here
Dart >=3.11.0The SDK running the CLI is the one that runs the project
Flutter >=3.44.0
git, with user.name and user.emailcreate 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 answerspub 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
Test servers (tests, when a test URL variable is set)Validates DW_TEST_DATABASE_URL / DW_TEST_STORAGE_URL and reports their hosts without credentials. Doctor requires loopback and has no remote-server override
A responding Docker daemonPostgres and the storage come from it by default. Doctor drops this check when nonempty DW_TEST_DATABASE_URL is set; DW_TEST_STORAGE_URL alone still needs Docker for Postgres. Flags passed only to the test command (dart run dartway_cli:dartway test) do not change doctor's checks. Not installed and not running are reported apart
The pub global bin directory on PATHThe 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 → myApp and dartway-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_modules and pubspec.lock;
  • runs dart format over bin/, lib/ and test/ of every package — a rename changes the length of names, and without this the first commit would not be formatted and dart run dartway_cli:dartway generate --check would depend on where lines happened to break;
  • replaces the monorepo-only dependency_overrides block with git overrides for exactly the dartway_* packages each pubspec reaches, pinned to the full commit that supplied the template;
  • installs the toolkit with base branch master and the given --language and --notes-tracker (see setup-ai below);
  • 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.

OptionDefaultMeaning
--channelDARTWAY_BRANCH, else the checkout the CLI runs from, else stableMonorepo branch to take the template and the toolkit from
--local-repoDARTWAY_MONOREPO_DIRA 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
--languageEnglishThe 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-trackerdartway/dartwayGitHub repository where findings about the framework are filed; none files nothing outside the project
--[no-]gitonInitialize a repository with an initial commit

--framework-path is how a project is built against a local framework checkout — this tree's own uncommitted changes, or a fork. Normally create writes immutable git overrides at the template commit; dartway update --target <sha> moves them. --framework-path writes path overrides instead.

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.

OptionDefaultMeaning
--agentbothInstall codex, claude or both; later installs preserve the recorded choice
--base-branchmasterBase branch of this project, used by the commit and PR instructions
--languageEnglishThe language the project writes its own texts in. Package APIs and error strings stay English
--notes-trackerdartway/dartwayWhere framework findings are filed; none keeps them in the project
--channelDARTWAY_BRANCH, else the checkout the CLI runs from, else stableMonorepo branch to take the toolkit from
--local-repoDARTWAY_MONOREPO_DIRA 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, moves every framework Git override and the lint plugin Git ref to the exact target, and applies the planned lint configuration edit. It edits no application code, bumps no hosted dependency 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
dart run dartway_cli:dartway generate --release # the release: raise the contract version once

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.

A project with a release branch raises the contract version at the release, not in each task (D-158). Install the toolkit with --release-branch <branch> (recorded as releaseBranch): tasks merge into the base branch, and generation and --check without --contract-base take the merge-base with the release branch as the baseline, generate at the current version: and report the raise the contract changes need instead of failing. The release runs generate --release: it raises the shared package version: — the next major (the next minor below 1.0) for an incompatible change, the next minor (the next patch below 1.0) for a new data object — generates at it and runs pub get in every package. The release CI then checks with an explicit --contract-base against the release branch, strictly, as before.

dartway sync — the base branch in, nothing derived to resolve​

dart run dartway_cli:dartway sync # merge origin/<baseBranch>
dart run dartway_cli:dartway sync develop # or any ref
dart run dartway_cli:dartway sync --continue # after resolving a source conflict

Merges the base branch into the current one from a clean tree. A conflict in a derived file — lib/generated/, *.dw.dart, pubspec.lock, the flutter gen-l10n outputs of l10n.yaml — takes either side; the migrations registry (migrations.dart headed Maintained by \migrate create`) is rewritten from the migration files beside it; an .arbmerges by key, and only a key both sides changed differently stays a conflict. Thenpub get(andgen-l10n) in every package and dart run dartway_cli:dartway generateremake every derived file from the merged sources, and the merge is committed. A conflict in a source stops it: resolve,git add, then --continue`.

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.

OptionMeaning
--typeOne check by name (case-insensitive); an unknown name is a usage error
--levelOnly checks of one severity: info, warning or error
--dirOnly 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/* (including /dw/health) on its own origin, because the front proxy serves the app and proxies /dw/ 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) go to the server and everything else to the web app.

Both subcommands take:

OptionDefaultMeaning
--port, -p8000Port of the origin to open in the browser
--apihttp://localhost:8080The 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:

  • localhost and 127.0.0.1 are 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 502 and 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, or their test-only defaults DW_TEST_DATABASE_URL and DW_TEST_STORAGE_URL, select explicit test servers. Flags win; empty variables count as unset. --no-storage ignores DW_TEST_STORAGE_URL but refuses an explicit --storage-url. Hosts must resolve to loopback unless --allow-remote-test-server is supplied; there is no environment form of that opt-in. Inherited application DW_DATABASE_* / DW_STORAGE_* variables never select servers. The suite and service workers receive only this run's service coordinates, with both test URL variables removed. 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.

OptionDefaultMeaning
--keepoffLeave the containers running and print how to reach and remove them; container mode only
--imagepostgres:17-alpinePostgres image — the one a deployment runs
--[no-]storageonStart the storage beside Postgres; --no-storage for a server without uploads
--storage-imagerustfs/rustfs:1.0.0Storage image — the one a deployment runs
--database-urlDW_TEST_DATABASE_URL (unset if empty)Explicit Postgres server and maintenance database, with a CREATEDB role; without a storage URL, storage is disabled
--storage-urlDW_TEST_STORAGE_URL (unset if empty)Explicit HTTP/HTTPS path-style S3 server and credentials; ignored by --no-storage when supplied only by the variable
--allow-remote-test-serveroffPermit 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 kamal # generate Studio/Kamal files
dart run dartway_cli:dartway deploy kamal --check # reject stale generated files
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

kamal generates stage/prod Kamal configs, the Studio manifest and a CI image workflow from deploy/config.yaml, literal project routes and the resolved CLI lock. kamal --check rejects stale output without writing; web release builds call Uri.base.origin, so one image serves both destinations. See Kamal and GHCR.

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.