Skip to content

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: run flutter gen-l10n and generate a type-safe L10nCode access layer, auto-wiring defaultToastHandle.
  • 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 fluzer in pubspec.yaml, so after global activate you can call it directly with fluzer.


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:

  1. Load fluzer.yaml (or the compatible flutter_zero_config.yaml), validating version (a non-empty string with the >= 1.0.0 lower bound) and template_name.
  2. 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.
  3. Resolve the template source pinned exactly to the project version (the same-named entry from the registry).
  4. Validate the feature name (must be snake_case, starting with a lowercase letter, e.g. user_profile).
  5. Check whether lib/features/<name>/ already exists.
  6. Render the matching brick by project version and state-management pattern: templates 3.0.x and earlier (via NewV1V2Adapter) always use the feature brick; templates 3.1.0+ (via NewV3Adapter) render feature_cubit / feature_bloc per --cubit / --bloc (defaults to feature_bloc if neither is given). Only the brick-declared name + package_name variables are passed; class-name casing is handled by Mustache filters inside the brick.
  7. Write the module into lib/core/di/injection_base.dart's registerFeatureModules() via FeatureRegistration (built on CodeMod).
  8. 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/ becomes cubit/ (no event.dart). For how to write business logic after adding a module, see Write Your First Feature.


4. create — Create a new project

Execution steps:

  1. Validate the project name (starts with a lowercase letter; only lowercase letters, digits, underscores).
  2. Render the project brick into the current directory (only the name variable), generating the ./<name> project directory directly.
  3. Run flutter create . --org --project-name.
  4. Remove the default test/widget_test.dart generated by flutter create (the template ships its own home_page_test.dart).
  5. Run flutter pub get.
  6. Run flutter gen-l10n.
fluzer create my_app

# options
#   --org <org>           organization identifier (default com.example, affects bundle ID)

Note: the current create no longer accepts --desc (project description). Edit pubspec.yaml manually 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_featureflutter 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:

  1. Validate the project config and parse l10n.yaml (arb-dir / output-dir / output-class, falling back to template conventions).
  2. Ensure the ARB directory exists and contains .arb files.
  3. Run flutter gen-l10n.
  4. Parse the generated AppLocalizations abstract class members (brace-counting class-body scanner; parameter declared types preserved).
  5. Generate three files into output-dir (default lib/l10n/gen/):
  6. l10n_code.dart — the L10nCode value object: code + parameters fields, no-arg static const constants, typed factories for parameterized keys, symmetric toString/parse serialization (encoding only at the serialization boundary), ==/hashCode.
  7. l10n_code_ext.darttypeS() / typeE() / typeI() / typeW() toast-type markers plus a toToastEffect() shortcut.
  8. l10n_toast_effect_helper.dart — a centralized switch dispatcher covering every ARB key, deserializing parameters by declared type (int.tryParse, DateTime.tryParse, ...).
  9. Auto-wire defaultToastHandle: locate the effect.l10nCode != null branch 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-l10n run; referencing them in the template would break compilation of freshly created projects. Shipping the assert fallback + auto-wiring on first gen-l10n is 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:

"@counterValue": { "placeholders": { "count": { "type": "int" } } }
// 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 ignore lib/l10n/gen/ by default). Each header carries the CLI version for traceability.


6. version — Show version & check for updates

fluzer version
  • Prints the CLI version (from the cliVersion constant, which must stay in sync with pubspec.yaml's version).
  • Queries pub.dev for a new version:
  • Queries the package name fluzer by 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 fluzer to 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 under fluzer_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 keeps version_check.json (this is the update-check cache for the version command, not part of the template cache).
  • Running fluzer cache without a subcommand throws a UsageException, printing the error plus usage and returning exit code 64 (ExitCode.usage).

To force re-pull a specific template version, run fluzer cache clean first, then create / new.


8. Template Source Resolution

Both new and create rely on TemplateSourceResolver.resolve() to decide where to load the Mason brick. Resolution priority:

  1. FLUZER_BRICKS_DIR non-empty → LocalBrickLoader (local dev / debugging, points to the bricks/ root).
  2. FLUZER_TEMPLATE_ZIP_URL non-empty → force RemoteBrickLoader of that URL (testing / debugging).
  3. Otherwise go to the remote registry: pull template_registry.json from templateRegistryUrl; create simply picks the zip URL with the largest version (always the latest template, no longer filtered by minCliVersion), while new matches the project version exactly. For both create/new the 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 templateRegistryUrl and defaultTemplateZipUrl placeholders (https://github.com/<owner>/<repo>/...) in template_config.dart with real addresses, and keep cliVersion in sync with pubspec.yaml. The registry is a templates version list (version + url); create picks the largest version, new matches the exact version — 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:

  • version is a valid non-empty string and >= 1.0.0 (the oldest template version the CLI accepts).
  • template_name must be exactly flutter_zero.
  • The root contains pubspec.yaml (reads name as package_name).

⚠️ The minCliVersion field has been removed: the CLI no longer gates on minCliVersion in the config. Template/CLI compatibility is now decided by each command's "version adapter" based on the version range (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

  • new reports "fluzer.yaml (or flutter_zero_config.yaml) not found": cd into the template project root (the one containing that file) before running.
  • create reports "directory already exists": pick another project name; the existing directory will not be deleted.
  • version keeps 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_URL to specify a specific Release's zip link.
  • cache list is 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 clean to clear the cache; the next create / new will 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_runner cache (e.g. stale freezed outputs): delete .dart_tool/build and all *.freezed.dart / *.g.dart, then regenerate.

Source of this page: docs/en/cli/README.md

Report an error on this page