CLI Commands
The starter:* commands that scaffold, configure, diagnose, publish and remove the starter inside a Magic application, run through dart run magic:dowel.
On this page
Introduction#
The package has no executable of its own. MagicStarterDowelProvider contributes its commands to the host app's dowel registry, so they run as dart run magic:dowel starter:<command>. dart run magic:dowel plugin:install magic_starter registers the provider and, unless you pass --no-bootstrap, chains starter:install as the plugin's bootstrap command. The chain always forwards --non-interactive, so it writes every feature as false and a later starter:install --features=... skips the config file that exists by then: pass --no-bootstrap and run starter:install yourself (see Installation).
Each command extends DowelCommand (install extends DowelInstallCommand), declares a signature DSL string for its arguments and flags, and implements handle(DowelContext ctx). Every command resolves the host project root through FileHelper.findProjectRoot().
Quick Reference#
| Command | Purpose |
|---|---|
starter:install |
Scaffolds config, middleware, models, dashboard and routes, and injects the registrations into existing host files. |
starter:configure |
Turns feature flags on in lib/config/magic_starter.dart; --show prints the current state. |
starter:doctor |
Checks the installation; also exposed as the read-only MCP tool starter_doctor. |
starter:publish |
Copies views, layouts, middleware, config or translations into the host for customisation. |
starter:uninstall |
Removes the files and injections starter:install made. |
Install Command#
dart run magic:dowel starter:install
The installer validates the host project, asks which features to enable (one yes/no prompt per feature key, unless --features or --non-interactive is passed), generates files from stubs and injects registrations into existing files. It throws when lib/config/app.dart is missing, which means the host has not been scaffolded: run dart run magic:dowel magic:install first.
What It Creates#
Each file is written only when it does not exist yet, or when --force is passed:
| File | Purpose |
|---|---|
lib/config/magic_starter.dart |
Feature toggle configuration with the selected features enabled |
lib/app/middleware/ensure_authenticated.dart |
Auth guard middleware |
lib/app/middleware/redirect_if_authenticated.dart |
Guest redirect middleware |
lib/app/models/user.dart |
User model |
lib/app/models/team.dart |
Team model (only when teams is enabled) |
lib/resources/views/dashboard_view.dart |
Dashboard view scaffold |
lib/routes/app.dart |
App routes file with the dashboard inside the authenticated layout |
lib/app/providers/app_service_provider.dart |
Created from a stub; an existing file is injected into instead, unless --force replaces it |
assets/lang/en.json |
Translation strings, deep-merged into an existing file |
What It Injects#
Beyond creating files, the installer modifies existing host files. Each injection checks for its own marker first, so a re-run adds nothing twice:
lib/config/app.dart: addsMagicStarterServiceProviderto the providers list (theinstall.yamlmanifest step).lib/main.dart: adds themagicStarterConfigfactory to the config factories, aWindThemeDatawith aprimarypalette passed toMagicApplication, and theconfig/magic_starter.dartandmaterial_uiimports.lib/app/kernel.dart: registers theauth(EnsureAuthenticated) andguest(RedirectIfAuthenticated) middleware aliases.lib/app/providers/route_service_provider.dart: addsregisterMagicStarterAuthRoutes()andregisterMagicStarterProfileRoutes(), plus the team, notification and support route registrations when those features are enabled.lib/app/providers/app_service_provider.dart: injectsMagicStarter.bootstrap(...)(andMagicStarter.useNavigation(...)) intoboot()unless the provider already configures the starter.pubspec.yaml: listsassets/lang/en.jsonunderflutter: assets:, and addsmagic_notificationswhennotificationsis enabled.
// Injected into lib/config/app.dart
(app) => MagicStarterServiceProvider(app),
// Injected into lib/main.dart
() => magicStarterConfig,
Flags#
| Flag | Description |
|---|---|
--force |
Overwrite existing generated files and replace app_service_provider.dart instead of injecting into it |
--dry-run |
Stages the transactional writes (config, middleware, models, dashboard, routes, provider registration) without writing them. The in-place edits of main.dart, kernel.dart, the route provider, AppServiceProvider, the translations and pubspec.yaml still run. |
--non-interactive |
Skip the prompts; every feature stays off unless --features names it |
--no-bootstrap |
Inherited from DowelInstallCommand; read by plugin:install, which is what chains this command |
--features |
Comma-separated feature keys; passing it implies --non-interactive |
# Interactive installation (prompts for each feature)
dart run magic:dowel starter:install
# Non-interactive with specific features
dart run magic:dowel starter:install --features=teams,notifications,registration
# Force overwrite existing files
dart run magic:dowel starter:install --force
Configure Command#
dart run magic:dowel starter:configure
Updates individual feature flags in lib/config/magic_starter.dart without modifying any other file. It exits with an error when that file does not exist. With no flag it prints a warning and changes nothing.
Show Current Config#
dart run magic:dowel starter:configure --show
Prints every feature key found in the config file with its status:
Current Magic Starter Feature Configuration:
Feature Status
------------- --------
teams enabled
social_login disabled
...
Toggle Features#
# Enable teams and notifications
dart run magic:dowel starter:configure --teams --notifications
| CLI Flag | Config Key |
|---|---|
--teams |
teams |
--social-login |
social_login |
--two-factor |
two_factor |
--sessions |
sessions |
--phone-otp |
phone_otp |
--newsletter |
newsletter |
--notifications |
notifications |
--email-verification |
email_verification |
Each flag is negatable: --teams turns the feature on and --no-teams turns it off, for example dart run magic:dowel starter:configure --no-teams.
Flipping a toggle edits the config file and nothing else. A feature that needs scaffolding (teams needs the Team model, the User team accessors, the bootstrap() team callbacks and registerMagicStarterTeamRoutes()) gets it only from starter:install.
Non-Interactive Mode#
The configure command never prompts: it works entirely through flags. That makes it safe for CI pipelines and scripts.
Doctor Command#
dart run magic:dowel starter:doctor
Runs independent checks that the starter was installed correctly. It exits with code 0 when every required check passes and code 1 otherwise. The same report is available to agents as the MCP tool starter_doctor, which takes an optional verbose boolean.
Health Checks#
| Check | What It Verifies |
|---|---|
| Magic Framework | lib/config/app.dart exists |
| Starter Config | lib/config/magic_starter.dart exists |
| Provider | MagicStarterServiceProvider appears in lib/config/app.dart |
| Config Factory | magicStarterConfig appears in lib/main.dart |
| Middleware | EnsureAuthenticated appears in lib/app/kernel.dart |
| Routes | registerMagicStarterAuthRoutes appears in route_service_provider.dart |
| Facade | app_service_provider.dart calls MagicStarter.bootstrap( or one of the legacy useUserModel, useLocaleOptions, useLogout setters |
| Translations | assets/lang/en.json exists |
| Billing web origin | Reported and required only when billing is enabled: billing.web_origin must be set to a non-empty value |
Three more lines appear only when relevant and never fail the command: a Support routes: WARN line when support is enabled but registerMagicStarterSupportRoutes() is not called, a Push external id prefix line when notifications is enabled (the prefix must equal the backend's onesignal.external_id_prefix), and a Published Views list that marks a published view WARN when AppServiceProvider does not register it.
Magic Framework: OK
Starter Config: OK
Provider: OK
Config Factory: OK
Middleware: OK
Routes: OK
Facade: FAIL
Translations: OK
Missing Requirements:
FAIL MagicStarter facade not configured in app_service_provider.dart
1 check(s) failed. Run `dart run magic:dowel starter:install` to fix.
Verbose Mode#
dart run magic:dowel starter:doctor --verbose
Shows the file inspected for each check and the string searched for:
Provider: OK
File: lib/config/app.dart
Contains: MagicStarterServiceProvider
Publish Command#
dart run magic:dowel starter:publish
Copies plugin source files into the host application for customisation, the starter's equivalent of Laravel's vendor:publish. Without --tag it publishes every group. When views or layouts are published, the command registers them in AppServiceProvider (MagicStarter.view.register(...) and registerLayout(...)) so the registry serves your copy.
A published view or layout compiles where it lands. Its relative imports are rewritten as it is copied: one that the package barrel covers becomes import 'package:magic_starter/magic_starter.dart';, and any other keeps its target through a package:magic_starter/src/... URI. A view or layout that has a registry key needs only the barrel; the support screens' helper widgets, published by the unscoped views tag, still reach into src/, which a host on flutter_lints sees as implementation_imports. Config, middleware and translations are copied unchanged. The registration imports your copy under a prefix, because AppServiceProvider also imports the barrel and the barrel exports the original under the same class name:
import '../../resources/views/starter/auth/magic_starter_login_view.dart' as published;
MagicStarter.view.register('auth.login', () => const published.MagicStarterLoginView());
Publish Tags#
# Publish everything
dart run magic:dowel starter:publish
# Publish only views
dart run magic:dowel starter:publish --tag=views
# One module, one view, one layout
dart run magic:dowel starter:publish --tag=views:auth
dart run magic:dowel starter:publish --tag=views:auth.login
dart run magic:dowel starter:publish --tag=layouts:app
# Overwrite existing files
dart run magic:dowel starter:publish --force
| Tag | What It Publishes |
|---|---|
config |
lib/config/magic_starter.dart, copied from the package's template, which writes every feature false (starter:install writes the features you chose true). It matters when the host has no config yet; an existing file is skipped, and --force replaces it with this template, which turns every feature off |
views |
View files to lib/resources/views/starter/ |
layouts |
app and guest layouts to lib/resources/layouts/starter/ |
middleware |
Auth middleware files to lib/app/middleware/ |
lang |
Translation file to assets/lang/en.json |
all (default) |
All of the above |
A views:<scope> scope is either one registry key (auth.login) or a module prefix (auth) taken from the command's view map; an unknown scope prints an error. Views with no entry in that map, such as the handoff page, are published only by the unscoped views tag.
Uninstall Command#
dart run magic:dowel starter:uninstall
Performs a reverse of the install command. It removes generated files and cleans up injected lines from host config files while keeping user code intact. It lists what it will remove and asks for confirmation first.
What it removes:
lib/config/magic_starter.dart- The
magic_starterdependency frompubspec.yaml - The
MagicStarterServiceProviderimport and registration fromlib/config/app.dart - The
magicStarterConfigimport and factory fromlib/main.dart - The middleware aliases and imports from
lib/app/kernel.dart - The route imports and registrations from
route_service_provider.dart
# Skip the confirmation prompt
dart run magic:dowel starter:uninstall --force
Each removal step is wrapped in error handling, so already-clean projects and partial installations do not crash the command. Afterwards dart format . runs automatically.
Stub Templates#
Generated files are built from stub templates in the assets/stubs/install/ directory:
| Stub | Output |
|---|---|
magic_starter_config.stub |
lib/config/magic_starter.dart |
ensure_authenticated.stub |
lib/app/middleware/ensure_authenticated.dart |
redirect_if_authenticated.stub |
lib/app/middleware/redirect_if_authenticated.dart |
user.stub |
lib/app/models/user.dart |
team.stub |
lib/app/models/team.dart |
app_service_provider.stub |
lib/app/providers/app_service_provider.dart |
dashboard_view.stub |
lib/resources/views/dashboard_view.dart |
app_routes.stub |
lib/routes/app.dart |
en.stub |
assets/lang/en.json |
Stub paths are resolved relative to the plugin package at runtime. The installer searches two locations through getStubSearchPaths(): the plugin's assets/stubs found through .dart_tool/package_config.json, then assets/stubs under the current directory.
Testing CLI Commands#
Commands expose overridable methods so a test can isolate them from the real filesystem:
class TestableInstallCommand extends MagicStarterInstallCommand {
final String _testRoot;
TestableInstallCommand(this._testRoot);
@override
String getProjectRoot() => _testRoot;
@override
List<String> getStubSearchPaths() => [
'$_testRoot/stubs',
];
@override
Future<ProcessResult> runDartFormat(String rootPath) async {
return ProcessResult(0, 0, '', ''); // No-op in tests
}
}
| Method | Commands | Purpose |
|---|---|---|
getProjectRoot() |
All | Redirect to a temp directory |
getStubSearchPaths() |
Install | Point to test stub fixtures |
resolveManifestPath() |
Install | Point to a test install.yaml |
runDartFormat() |
Install, Uninstall | Skip formatting in tests |
getPluginSourceDir() |
Publish | Point to a test plugin source |
Gotchas#
| Mistake | Fix |
|---|---|
Running dart run magic_starter install |
The package has no executable. Every command is dart run magic:dowel starter:<command>. |
Running starter:configure --teams on a host installed without teams |
It only flips 'teams': true in the config; the Team model, the User team accessors, the bootstrap() team callbacks and the team routes are not written, so the app throws on startup. Commit your changes, then re-run dart run magic:dowel starter:install --features=teams --force, adding every other feature you want on, since the config is rewritten from that list. --force also replaces AppServiceProvider and overwrites the translation file. |
Expecting starter:configure to toggle every feature |
Only the flags in the table have one. registration, billing, support and the others are edited in the config file. |
Running starter:install before the host is scaffolded |
It throws when lib/config/app.dart is missing. Run dart run magic:dowel magic:install first. |
Expecting --features to leave the prompts on |
Passing --features skips them, and every key it does not name is written as false. |
Expecting starter:uninstall to remove everything |
AppServiceProvider integrations, middleware files and translation entries stay; it prints them as manual cleanup. |
Related Documentation#
- Installation: adding the package and registering the plugin.
- Configuration: the feature keys the config file carries.
- Views and Layouts: overriding a screen without publishing it.
- Service Provider: what
MagicStarterServiceProviderregisters.