fluzer — Scaffolding CLI¶
fluzer is the command-line tool for the Flutter Zero template, providing five groups of commands:
create: generate a brand-new Flutter project from the template (with complete core infrastructure, example modules, and configuration).new: add a feature-module skeleton in an existing template project, and auto-register it in DI.gen-l10n: runflutter gen-l10nand generate a type-safeL10nCodeaccess layer, auto-wiringdefaultToastHandle.cache: view or clear locally downloaded template caches (cache list/cache clean).version: show the CLI's own version and check for updates.
The template and CLI are decoupled: the CLI renders code through Mason bricks (under
flutter_zero_template/bricks/); the brick's variable contract and generated structure evolve independently, so the template can release frequently without upgrading the CLI.
1. Quick Start¶
Dev mode (inside the flutter_zero_cli directory)¶
dart run bin/fluzer.dart new user
dart run bin/fluzer.dart create my_app
dart run bin/fluzer.dart cache list
dart run bin/fluzer.dart version
Global install¶
dart pub global activate fluzer
fluzer new user # add a feature module
fluzer create my_app # create a new project
fluzer cache list # list cached template versions
fluzer version # show version + check for updates
The CLI registers the executable name
fluzerinpubspec.yaml, so afterglobal activateyou can call it directly withfluzer.
2. Command Overview¶
| Command | Purpose | Common options |
|---|---|---|
fluzer new <feature_name> |
Add a feature module in the current template project and register DI | --bloc / --cubit (state-management pattern, bloc by default) |
fluzer create <project_name> |
Create a brand-new Flutter project from the template | --org |
fluzer gen-l10n |
Generate the L10nCode access layer and auto-wire toast dispatch | --skip-handle-patch / --force-handle-patch |
fluzer cache list |
List downloaded cached template versions | — |
fluzer cache clean |
Clear all cached template versions | — |
fluzer version |
Print the CLI version and check pub.dev for updates | — |
Global options (apply to every command)¶
fluzer has two global switches, placed before the subcommand (e.g. fluzer --locale en create my_app):
| Option | Purpose |
|---|---|
--locale / -L |
Switch the CLI's own interface language (unrelated to the template project's localization). Supports zh / en / ja. Four accepted forms: --locale en, --locale=en, -L en, -Len. When omitted, resolution order is: --locale > env vars LANG/LC_ALL/LANGUAGE > Platform.localeName > default zh; unrecognized values silently fall back to zh. |
--log / -l |
Verbose (debug) mode: streams subprocess output live, shows the download progress bar, prints full stack traces, and disables the spinner. For any CLI issue, re-run with -l first. |
Interface strings are generated by
slang(pure-Dart mode).
3. new — Add a feature module¶
Must be run in the root directory of a Flutter Zero template project (the one containing fluzer.yaml; legacy projects using flutter_zero_config.yaml are still recognized).
Execution flow:
- Load
fluzer.yaml(or the compatibleflutter_zero_config.yaml), validatingversion(a non-empty string with the>= 1.0.0lower bound) andtemplate_name. - Pick a version adapter by the project's
version(AdapterCommand): if the version is outside the current CLI adapter's supported range, it errors "please upgrade fluzer" or "please upgrade the template/CLI"; otherwise it proceeds. - Resolve the template source pinned exactly to the project
version(the same-named entry from the registry). - Validate the feature name (must be
snake_case, starting with a lowercase letter, e.g.user_profile). - Check whether
lib/features/<name>/already exists. - Render the matching brick by project
versionand state-management pattern: templates3.0.xand earlier (viaNewV1V2Adapter) always use thefeaturebrick; templates3.1.0+(viaNewV3Adapter) renderfeature_cubit/feature_blocper--cubit/--bloc(defaults tofeature_blocif neither is given). Only the brick-declaredname+package_namevariables are passed; class-name casing is handled by Mustache filters inside the brick. - Write the module into
lib/core/di/injection_base.dart'sregisterFeatureModules()viaFeatureRegistration(built onCodeMod). - Run
build_runner, building only the new module (--build-filter lib/features/<name>/**.dart).
fluzer new user # BLoC pattern by default (same as legacy templates)
fluzer new user --bloc # explicitly BLoC pattern (presentation/bloc/)
fluzer new user --cubit # Cubit pattern (MVVM, presentation/cubit/, no event)
The generated module contains
data/,domain/,presentation/skeletons and auto-generates<name>_module.dart; with--cubit,presentation/becomescubit/(noevent.dart). For how to write business logic after adding a module, see Write Your First Feature.
4. create — Create a new project¶
Execution steps:
- Validate the project name (starts with a lowercase letter; only lowercase letters, digits, underscores).
- Render the
projectbrick into the current directory (only thenamevariable), generating the./<name>project directory directly. - Run
flutter create . --org --project-name. - Remove the default
test/widget_test.dartgenerated byflutter create(the template ships its ownhome_page_test.dart). - Run
flutter pub get. - Run
flutter gen-l10n.
fluzer create my_app
# options
# --org <org> organization identifier (default com.example, affects bundle ID)
Note: the current
createno longer accepts--desc(project description). Editpubspec.yamlmanually after generation for the project description.
If the target directory already exists, it reports an error and cleans up the half-made directory it created itself (it will not delete the content of your existing same-named directory — it only cleans up if the directory was created by this command; if the directory already existed, create will simply ask you to pick another name and delete nothing).
After a successful creation it suggests next steps: cd my_app → (optional) fluzer new my_feature → flutter run.
5. gen-l10n — Type-safe localization access layer¶
Must be run in the root directory of a Flutter Zero template project. On top of flutter gen-l10n, it generates a type-safe localization access layer so BLoCs can carry localized messages without a BuildContext.
Execution flow:
- Validate the project config and parse
l10n.yaml(arb-dir/output-dir/output-class, falling back to template conventions). - Ensure the ARB directory exists and contains
.arbfiles. - Run
flutter gen-l10n. - Parse the generated
AppLocalizationsabstract class members (brace-counting class-body scanner; parameter declared types preserved). - Generate three files into
output-dir(defaultlib/l10n/gen/): l10n_code.dart— theL10nCodevalue object:code+parametersfields, no-argstatic constconstants, typed factories for parameterized keys, symmetrictoString/parseserialization (encoding only at the serialization boundary),==/hashCode.l10n_code_ext.dart—typeS()/typeE()/typeI()/typeW()toast-type markers plus atoToastEffect()shortcut.l10n_toast_effect_helper.dart— a centralized switch dispatcher covering every ARB key, deserializing parameters by declared type (int.tryParse,DateTime.tryParse, ...).- Auto-wire
defaultToastHandle: locate theeffect.l10nCode != nullbranch via AST and replace it with the helper call (see "Auto-wiring" below).
fluzer gen-l10n
# options
# --skip-handle-patch skip the defaultToastHandle auto-wiring
# --force-handle-patch overwrite even if the l10nCode branch was customized (prints the replaced source first)
Usage in BLoC¶
// No-arg + type marker — emit a ToastEffect in one step
emitEffect(L10nCode.homeRefreshSuccess.typeI().toToastEffect());
// Parameterized (types match the ARB placeholder declarations)
emitEffect(L10nCode.requestFailed('E1001').typeE().toToastEffect());
// Equivalent manual form
emitEffect(ToastEffect(l10nCode: L10nCode.homeRefreshSuccess.typeI().toString()));
No more per-feature ToastEffect handlers — every l10nCode is dispatched by L10nToastEffectHelper to ToastService's showSuccess / showError / showInfo / showWarning.
Three-state detection of auto-wiring¶
gen-l10n modifies lib/core/effect/effect_handle/default_toast_effect_handle.dart and adds the missing imports. To protect developer modifications, the branch state is detected before patching:
| State | Detection | Behavior |
|---|---|---|
| Template | branch contains the assert( fallback (template default) |
Replace; the replaced source is printed in the log |
| Already wired | branch already contains L10nToastEffectHelper |
Idempotent skip (safe on repeated runs) |
| Customized | anything else (developer rewrote the branch) | Skip with a warning; --force-handle-patch to overwrite |
Other code in the file (e.g. your own errorCode cases in _handleErrorCode) is untouched — the patch locates the exact branch via AST and only replaces that block.
Why doesn't the template ship pre-wired? The three gen files don't exist before the first
gen-l10nrun; referencing them in the template would break compilation of freshly created projects. Shipping the assert fallback + auto-wiring on firstgen-l10nis the only self-consistent option.
ARB placeholder types¶
Typed placeholders declared in ARB are preserved end-to-end in the L10nCode factory signature and the helper's deserialization:
// Generated factory keeps the int type
L10nCode.counterValue(5);
// Helper restores the value by type
l.counterValue(int.tryParse(l10nCode.parameters['count'] ?? '') ?? 0)
Supported types: Object / String / int / double / num / bool / DateTime (DateTime is serialized as ISO-8601).
The three generated files are fully regenerated outputs (add them to
.gitignore; template projects ignorelib/l10n/gen/by default). Each header carries the CLI version for traceability.
6. version — Show version & check for updates¶
- Prints the CLI version (from the
cliVersionconstant, which must stay in sync withpubspec.yaml'sversion). - Queries pub.dev for a new version:
- Queries the package name
fluzerby default; if unpublished, pub.dev returns 404, and it silently degrades to "cannot check for updates", not affecting the main flow. - Results are cached per package name for 24 hours (unavailable results cached for 10 minutes) to avoid hitting the API on every startup.
- Network exceptions / rate limits also degrade silently.
- If a new version is found, it prompts: run
dart pub global activate fluzerto upgrade.
7. cache — Manage template cache¶
fluzer caches the remotely pulled template zip under fluzer_cache/ in the system temp directory (directory name template_<version> or the fallback fluzer_<hash>). The cache command is for viewing and cleaning:
fluzer cache list # list all cached template versions
fluzer cache clean # clear all cached template versions
cache list: prints all cached version directories underfluzer_cache/(sorted by name); if the directory doesn't exist or is empty, it prints a hint instead of erroring.cache clean: deletes all cached version subdirectories, but keepsversion_check.json(this is the update-check cache for theversioncommand, not part of the template cache).- Running
fluzer cachewithout a subcommand throws aUsageException, printing the error plus usage and returning exit code 64 (ExitCode.usage).
To force re-pull a specific template version, run
fluzer cache cleanfirst, thencreate/new.
8. Template Source Resolution¶
Both new and create rely on TemplateSourceResolver.resolve() to decide where to load the Mason brick. Resolution priority:
FLUZER_BRICKS_DIRnon-empty →LocalBrickLoader(local dev / debugging, points to thebricks/root).FLUZER_TEMPLATE_ZIP_URLnon-empty → forceRemoteBrickLoaderof that URL (testing / debugging).- Otherwise go to the remote registry: pull
template_registry.jsonfromtemplateRegistryUrl;createsimply picks the zip URL with the largestversion(always the latest template, no longer filtered byminCliVersion), whilenewmatches the projectversionexactly. For bothcreate/newthe direct URL and every mirror prefix are raced concurrently, the first success wins and the rest are cancelled (timeouts: 30s for text / 180s for files). There is no "wait for the direct connection to time out, then fall back to mirrors" behavior.
# Local debug: read the local bricks directory directly
export FLUZER_BRICKS_DIR=../flutter_zero_template/bricks
fluzer new user
# Debug: force a specific remote zip
export FLUZER_TEMPLATE_ZIP_URL=https://github.com/<owner>/<repo>/releases/download/1.0.0/bricks.zip
fluzer create demo
RemoteBrickLoader caches the zip to the temp directory after download: when the registry version number is available it names the cache directory template_<version>; on env-var override / fallback it degrades to a URL-hash name; different versions don't overwrite each other, and path validation is done on extraction (Zip Slip protection).
Required before release: replace the
templateRegistryUrlanddefaultTemplateZipUrlplaceholders (https://github.com/<owner>/<repo>/...) intemplate_config.dartwith real addresses, and keepcliVersionin sync withpubspec.yaml. The registry is atemplatesversion list (version+url);createpicks the largestversion,newmatches the exactversion— see CLI Versioning.
9. Config File fluzer.yaml (compatible with flutter_zero_config.yaml)¶
The new / gen-l10n commands depend on fluzer.yaml in the template project root (the v2 config name; legacy projects using flutter_zero_config.yaml are still recognized). ProjectConfig.load() searches upward for either file name and validates.
version: 1.0.0 # template version (the template version this project was born from)
template_name: flutter_zero
Validation:
versionis a valid non-empty string and>= 1.0.0(the oldest template version the CLI accepts).template_namemust be exactlyflutter_zero.- The root contains
pubspec.yaml(readsnameaspackage_name).
⚠️ The
minCliVersionfield has been removed: the CLI no longer gates onminCliVersionin the config. Template/CLI compatibility is now decided by each command's "version adapter" based on theversionrange (see Version Constraint Rules). Internal structure (lib/,lib/core/di/injection_base.dart, ...) is also no longer validated here — the CLI aims to support all template versions, and structural differences are handled by each version adapter.
Any unmet item throws CliException and aborts, prompting you to run it in the correct template project root.
10. Directory Structure¶
fluzer/
├── bin/
│ └── fluzer.dart # Entry point
├── lib/
│ └── src/
│ ├── fluzer.dart # CLI root controller (CommandRunner assembly + root exception fallback + UsageException help print)
│ ├── commands/
│ │ ├── base_command.dart # command base (arg collect + buildContext + execute + injection points)
│ │ ├── command_context.dart # command context base (carries version info)
│ │ ├── command_adapter.dart # version-adapter interface (spec / canHandle / run)
│ │ ├── adapter_command.dart # version-aware command base (read version → pick adapter → delegate)
│ │ ├── new/ # new command
│ │ │ ├── new_command.dart # entry (AdapterCommand, picks adapter by version)
│ │ │ ├── new_context.dart # context (holds FeaturePattern enum)
│ │ │ └── adapters/
│ │ │ ├── base_new_adapter.dart
│ │ │ ├── new_v1v2_adapter.dart # 1.0.0 ~ 3.0.x adapter (feature brick)
│ │ │ └── new_v3_adapter.dart # 3.1.0+ adapter (picks feature_bloc/feature_cubit by --bloc/--cubit)
│ │ ├── gen_l10n/ # gen-l10n command
│ │ │ ├── gen_l10n_command.dart
│ │ │ ├── gen_l10n_context.dart
│ │ │ └── adapters/
│ │ │ ├── base_gen_l10n_adapter.dart
│ │ │ └── gen_l10n_v1v2_adapter.dart # 1.0.0+ shared adapter
│ │ ├── create/ # create command
│ │ │ ├── create_command.dart # 6-step flow + injection executor
│ │ │ └── create_context.dart
│ │ ├── cache/ # cache command
│ │ │ ├── cache_command.dart
│ │ │ └── cache_context.dart
│ │ └── version/ # version command
│ │ ├── version_command.dart
│ │ ├── version_context.dart
│ │ └── version_spec.dart # RangeSpec / AnySpec version ranges
│ ├── gen_l10n/
│ │ ├── l10n_config.dart # l10n.yaml parsing (arb-dir/output-dir/output-class)
│ │ ├── l10n_parser.dart # AppLocalizations parsing (class-body brace scanner + typed L10nParam)
│ │ ├── l10n_code_generator.dart # pure-function generators for the three gen files (dart_style formatted)
│ │ └── toast_handle_patcher.dart # defaultToastHandle AST wiring (three-state detection)
│ │ └── l10n_param_type.dart # L10nParamType parameter-type registry
│ ├── codemod/
│ │ ├── code_mod.dart # AST editing core (CodeMod: sorted addImport / idempotent insertAtMethodEnd)
│ │ ├── codemod_file_editor.dart # generic file-edit wrapper
│ │ ├── feature_registration.dart # DI registration wrapper (depends on CodeMod)
│ │ ├── insert_at_method_end_transform.dart # insert-at-method-end transform
│ │ └── ordered_import_transform.dart # ordered import insert transform
│ ├── config/
│ │ └── project_config.dart # project config loading + CliException
│ ├── template/
│ │ ├── brick_loader.dart # BrickLoader abstraction + Local / Remote loaders
│ │ ├── brick_renderer.dart # Mason render wrapper (BrickRenderer.generate)
│ │ ├── feature_generator.dart # feature module generator (render + call FeatureRegistration)
│ │ ├── template_source.dart # template source resolution: TemplateSourceResolver
│ │ ├── template_version_reader.dart # project template version reader (TemplateVersionReader)
│ │ ├── template_config.dart # centralized config (actually under lib/src/config/): registry/zip URL, mirror prefixes, cache dir name
│ │ └── semantic_version.dart # SemVer parse & compare (actually under lib/src/util/)
│ ├── http/
│ │ ├── http_client.dart # FluzerHttpClient: unified Dio instance + mirror-raced download
│ │ └── race_http_client.dart # concurrent race downloader (direct + mirror prefixes fired together)
│ ├── i18n/
│ │ ├── i18n.dart # interface strings entry (MessagesProvider, supports zh/en/ja)
│ │ ├── resources/*.i18n.json # slang source strings (zh/en/ja)
│ │ └── gen/strings*.g.dart # slang-generated type-safe access layer
│ ├── logging/
│ │ └── spinner.dart # runWithSpinner: spinner wrapper + verbose-mode degradation
│ ├── process/
│ │ └── process_runner.dart # ProcessRunner: unified process execution (flutter / dart)
│ ├── util/
│ │ ├── string_case.dart # naming conversion utilities
│ │ └── regular_utils.dart # general utilities (e.g. extract version from URL)
│ └── version/
│ ├── version_check.dart # pub.dev update check (available 24h / unavailable 10min cache)
│ └── version_update_notifier.dart # VersionUpdateNotifier: non-blocking startup update notice (new/gen-l10n/create opt in explicitly)
├── test/
│ ├── fluzer_test.dart # command layer (create/new/version/cache) + version check + brick render
│ ├── brick_test.dart # brick render smoke test
│ ├── gen_l10n_test.dart # l10n parsing & code generation unit tests
│ ├── toast_handle_patcher_test.dart # auto-wiring three-state / idempotency / safety tests
│ ├── template_source_test.dart # template source resolution (registry / exact pin / fallback)
│ ├── version_check_test.dart # update-check caching and degradation
│ ├── version_update_notifier_test.dart # startup update-notice behavior (VersionUpdateNotifier)
│ ├── http_client_test.dart # HTTP download unit tests
│ ├── race_http_client_test.dart # concurrent race download unit tests
│ ├── i18n_test.dart # interface strings loading & fallback
│ ├── process_runner_test.dart # subprocess execution (stdin / exit codes)
│ ├── project_config_test.dart # config loading & validation
│ ├── semantic_version_test.dart # version comparison
│ ├── spinner_test.dart # spinner behavior
│ ├── util_test.dart # utility functions
│ ├── text_url_extract_test.dart # version extraction from URL
│ ├── debug_flag_test.dart # --log debug flag
│ └── test_utils.dart # test helpers (safe temp-dir removal)
└── pubspec.yaml
11. Tech Stack¶
| Category | Solution |
|---|---|
| Argument parsing / CLI framework | args (CommandRunner + Command) |
| Log output | mason_logger (colored console) |
| Template rendering | mason (brick + Mustache filters) |
| Template download / unzip | dio + archive |
| AST code modification | analyzer + codemod_recipe (wrapped as CodeMod) |
| Generated-code formatting | dart_style (in-process, no subprocess needed) |
| YAML parsing | yaml |
| Path operations | path |
| Interface localization | slang ^4.19.0 (pure-Dart mode, flutter_integration: false) + intl ^0.20.3 |
12. Development & Testing¶
Local debugging¶
Inside the flutter_zero_cli directory use dart run bin/fluzer.dart ..., and point to a local or specified remote template via the env vars FLUZER_BRICKS_DIR / FLUZER_TEMPLATE_ZIP_URL, avoiding the registry on every run.
Inject executors for testability¶
Both commands and the version check inject external implementations via typedef, for easy unit testing:
CreateCommand:ProcessRunner(unified flutter/dart subprocess execution) /BrickLoader/VersionCheckService/Translations.NewCommand:ProcessRunner+BrickLoader.VersionCommand:VersionCheckService(peekCachedUpdate()/checkForUpdate(), queries pub.dev).
Run tests¶
dart analyze # 0 issues
dart test # includes command-layer (create/new/version) and network-fallback paths
Coverage highlights: project-name / feature-name validation, target directory already exists, the full generation flow, cleanup on flutter create failure, the version-check's three branches (update available / up to date / unavailable), and cache list / clean.
13. Common Troubleshooting¶
newreports "fluzer.yaml (or flutter_zero_config.yaml) not found":cdinto the template project root (the one containing that file) before running.createreports "directory already exists": pick another project name; the existing directory will not be deleted.versionkeeps saying "cannot check for updates": the package is not yet published to pub.dev, or the network is restricted — this is a normal degradation and does not affect other commands.- Template pull is slow / want to pin a version: use
FLUZER_TEMPLATE_ZIP_URLto specify a specific Release's zip link. cache listis empty: you haven't created a project or pulled a remote template yet; an empty cache directory is normal.- Want to force-refresh the template: first
fluzer cache cleanto clear the cache; the nextcreate/newwill re-download. - Any command failing / download stuck: re-run with
-l(or--log) to see the real subprocess output, download progress and full stack traces. - Corrupted
build_runnercache (e.g. stale freezed outputs): delete.dart_tool/buildand all*.freezed.dart/*.g.dart, then regenerate.
Source of this page: docs/en/cli/README.md