Skip to content

CLI Reference

Upgrading generated packages ​

Generated packages are yours to change. When you run a brick again to get a newer version, it must not delete those changes without telling you. Mason writes every template file over the old one and deletes nothing. So a brick that keeps a generation record checks your packages first. If they changed, it stops before it writes anything.

Every brick that generates its own packages does this: 70 bricks, from the versions listed in their CHANGELOGs (app_blackbox 2.10.0 and app_blackbox_recorder 2.0.0 were first). The six bricks without a record are listed in Which files a brick owns.

When a package moves from one brick to another, the old brick hands its fingerprints of that package to the new brick's record the next time it runs, and stops checking the package itself. The new brick then checks your changes against what the old one generated, instead of stopping once for a missing record. app_blackbox_recorder 2.0.0 does this for the four packages that moved to app_blackbox_capture.

The record ​

After a brick generates its packages, its post_gen hook writes .archipelago/generated/<brick>.json. The file lists every file in the packages the brick owns, with a fingerprint of its content:

json
{
  "brick": "app_blackbox",
  "version": "2.10.0",
  "vars": { "appName": "kirana", "stateManagement": "bloc" },
  "dirs": [
    "features/app_blackbox/app_blackbox_api",
    "features/app_blackbox/app_blackbox_noop"
  ],
  "files": {
    "features/app_blackbox/app_blackbox_api/lib/app_blackbox_api.dart": "6f1c2a9e",
    "features/app_blackbox/app_blackbox_api/pubspec.yaml": "0b7d41c3"
  }
}
  • The fingerprint ignores whitespace and trailing commas, so dart format is not a change.
  • Tool output is not recorded: dot directories (.dart_tool), build/, coverage/, pubspec.lock, the melos_<package>.iml files that melos bootstrap writes, and generated code (*.g.dart, *.gen.dart, *.module.dart, *.freezed.dart and similar).
  • Files git ignores are not recorded. flutter create writes files such as android/gradlew and ios/Flutter/Generated.xcconfig that are missing from every other clone, so recording them would stop your teammates' runs.
  • version is the brick version that generated the files, and vars are the values it ran with, so archipelago upgrade can run it again. Internal vars (_-prefixed) and vars whose name looks like a secret (token, secret, password, apiKey, dsn...) are not recorded; pass them to upgrade with --config.
  • The brick also copies every recorded file, as it generated it, to .archipelago/base/<brick>/. That copy is the base of the 3-way merge in archipelago upgrade. Tools skip it, because it is in a dot directory. While you do not change a file, its copy costs nothing in your repository: git stores identical content once.
  • archipelago.yaml lists the version of each brick with a record under brick_versions (app_blackbox: 2.10.0). bricks_installed stays a list of names.
  • Commit the record and the base together with the packages. Your team then shares them.

Which files a brick owns ​

A brick owns the packages it generates in full: every directory with a pubspec.yaml in its template, and nothing else. brickOwnedDirs in bricks/shared_hooks/lib/src/brick_records.dart lists them, and a test fails when that list and a brick's template differ.

A brick does not own a file that another brick, or a marker region, also writes. These are never fingerprinted:

  • Host and workspace files: the app's pubspec.yaml, lib/di/injector.dart and initializers, build_prepare.yaml, archipelago.yaml, the root pubspec.yaml and the CI files. Every brick edits them through marker regions. They are outside every package a brick owns.
  • Host app files a brick adds: for example background_sdk's lib/background/background_dispatcher.dart or app_debugger's overlay test in the app. They live in the host app, which no brick owns.
  • Shared files inside an owned package (brickSharedFiles):
BrickFileAlso written by
app_debuggerfeatures/app_debugger/app_debugger/lib/src/di/debug_tools.dart, features/app_debugger/app_debugger/pubspec.yamlevery app_debugger_* panel brick and app_blackbox_recorder (debug panel marker region, path dependency)
monorepo_toolkitdevtools/monorepo_toolkit/lib/src/command_runner.dart, devtools/monorepo_toolkit/pubspec.yamlevery devtool brick (affected, coverage_manager, size_guard, asset_generator...: its command and path dependency)
push_notification_sdkfeatures/push_notification/push_notification_impl/pubspec.yamlpush_notification_onesignal (di_modules depends_on)
shared_app_configshared/app_config/lib/src/env/env.dart, shared/app_config/lib/src/env/app_env.dartauth_sdk with the Supabase backend (its env keys)

When the owner brick runs again, it keeps these shared files as they are, so the other bricks' wiring stays. It also keeps generated code in its packages (*.g.dart, *.module.dart...), which build_runner owns. A newer owner version therefore does not change a shared file either, with one exception: a kept pubspec.yaml gets every dependencies and dev_dependencies entry the new template has and your file lacks (your own entries and versions stay as they are). Any other change to a shared file is in the CHANGELOG, to add by hand.

Some bricks add files to a package another brick owns. The owner's record reports those files as Added, which is correct: the owner's next run would overwrite the barrel that exports them. For example, ui_kit_component adds components to flutter_ui_kit's packages, and state_management adds a state holder to a feature package.

feature_monorepo_skeleton keeps one record per feature, .archipelago/generated/feature_monorepo_skeleton/<feature>.json, for features/<feature>.

These bricks have no record (bricksWithoutRecord):

BrickWhy
flutter_modular_monorepoIt generates the workspace root and the host app, which every brick and you edit, so a record would stop almost every run. Its few own packages (shared/feature_sdk, shared/router_registry) are written only when you create the project. The packages of the bricks it generates (monorepo_toolkit's devtools/, network_sdk, the starter features...) keep their own records.
app_scaffoldIt generates a host app. Same reason.
initializer_skeletonIt adds one initializer file to the host app.
state_managementIt adds a state holder to a feature package.
ui_kit_componentIt adds a component to flutter_ui_kit's packages.
ui_kit_themeIt adds a theme to flutter_ui_kit's ui kit package.

Bricks that generate other bricks ​

A brick that needs another brick generates it first (SubBrickGenerator). That brick runs its own hooks, so it checks and records its own packages:

  • In a new project, the dependency's packages do not exist yet. The check passes and the dependency writes its record.
  • In an existing project, the dependency is generated only when its package or its archipelago.yaml entry is missing. If its package exists but has no record, the dependency stops as described in Projects generated before the record, and the whole run stops with it.

The check runs first in pre_gen, before the brick generates any dependency, so a stop never leaves a half-done run.

flutter_modular_monorepo changes its sub-bricks' packages after they recorded them: flutter create adds the widgetbook app's platform directories, and codegen and dart format run over the workspace. As its last step it records every record again (refreshGeneratedRecords). Nothing in a new project is your edit yet, so re-running any brick right after archipelago create passes.

The drift stop ​

Before the brick generates anything (also before it generates its dependencies), its pre_gen hook compares the packages on disk with the record. It stops in these cases:

  • Changed: a recorded file has different content.
  • Added: a file in the owned packages is not in the record. For example, you added lib/src/bundle/bundle_info.dart and exported it from the barrel. A new version of the barrel would drop that export.
  • Deleted: a recorded file is gone. The brick would put it back.

The brick prints the files, tells you to run archipelago upgrade <brick>, gives the manual merge steps for runs without the CLI, prints Nothing was changed and exits with an error.

To overwrite your changes anyway, run the brick with ARCHIPELAGO_FORCE=1:

bash
ARCHIPELAGO_FORCE=1 archipelago generate app_blackbox_recorder
ARCHIPELAGO_FORCE=1 mason make app_blackbox_recorder --on-conflict overwrite

The brick then overwrites its template files and records the packages as they are after the run. Files you added stay on disk and are part of the new record.

Projects generated before the record ​

A project generated by an older version has no record. The brick cannot tell your changes from its own files, so the first run stops too, and says so. This happens once for each brick you upgrade: after that run, the brick has a record.

  • You never changed the packages: run the brick again with ARCHIPELAGO_FORCE=1. This run writes the first record.
  • You changed them, and git still has the files as the brick generated them: adopt that revision as the record, then use archipelago upgrade.
  • You changed them and have no such revision: merge by hand.

Adopt a revision as the record ​

archipelago adopt writes the record and the base from a git revision. It does not touch your files. The revision defaults to the commit that first added the brick's package directories, and the command prints it. The first commit of the repository is often wrong: it may predate the directories. Pass --base-ref to use another one. The package directories come from the brick (the CLI reads them from the brick's list, so it needs a login or --local-bricks); pass --dir once per directory to override them:

bash
archipelago adopt shared_dependencies --dry-run   # lists your changes
archipelago adopt shared_dependencies
archipelago adopt shared_dependencies --base-ref <rev>   # only to override the default
archipelago upgrade shared_dependencies --config vars.json --dry-run

The record holds the revision's files, not yours. upgrade then sees your edits as changes against that base and merges them with the new version instead of overwriting them. Use a revision from before you edited the packages: if you adopt the current files, upgrade takes the new version wholesale. Generated code, build/ and dot directories are left out, as in any record. The record has no vars, so pass them with --config. adopt --dry-run previews the adoption: the files it will record, and either the files where your project differs from the revision or "No local edits" when it matches them all (so you can tell the revision is pristine). upgrade on a brick with no record stops and prints the adopt command to run, with the brick's package directories filled in and no revision. It does not adopt for you. Check the revision adopt --dry-run prints: a commit that already holds your edits records wrong bases. A brick whose directories depend on a name the project does not record (a feature made by feature_monorepo_skeleton) needs --dir. adopt also accepts --local-bricks <repo> to read the directories from that repo's current list, not from a brick bundle that may be older. When none of the brick's directories exist in your project, adopt stops, names the directory that does exist and lists its contents. A feature that is one package (features/home, not home_api and home_impl) needs --dir features/home. With --local-bricks, neither command prints a login or 401 message.

Upgrade with archipelago upgrade ​

archipelago upgrade <brick> merges a newer brick version into the packages you changed:

bash
archipelago upgrade app_blackbox --dry-run   # report only, write nothing
archipelago upgrade app_blackbox
archipelago upgrade feature_monorepo_skeleton/home   # one record per feature

It works like this:

  1. It copies your project into .dart_tool/archipelago_upgrade/ without the brick's packages, record and base. There, the new version generates as a fresh install, with the vars from the record and the rest of your project around it. The drift stop does not apply, because the packages are not there.
  2. For each file the old or the new version generates, inside the brick's packages, it compares your file (ours), the base (.archipelago/base/<brick>/) and the new output (theirs):
CaseResult
New upstreamAdded.
You did not change itReplaced with the new version.
Only you changed itKept.
Both changed itgit merge-file. Lines that both sides changed get conflict markers.
Binary file that both changedConflict. Your file is kept.
You deleted it and the new version changed itConflict. Nothing is written.
Deleted upstreamReported and kept. Delete it yourself if you do not use it. When the new version records where the content went, the report names it, for example shared/dependencies/lib/bloc.dart (bloc is exported by state.dart ...): move your imports there, then delete the file.
  1. Generated code and every file outside the brick's packages are never written. Shared files (brickSharedFiles) keep your content: only a shared pubspec.yaml changes, by getting the dependencies the new version adds. Any other change the new version makes to a shared file is in its CHANGELOG.
  2. With no conflict, it takes the new record and base and sets brick_versions in archipelago.yaml. Commit everything, then run melos bootstrap, melos run build and melos run analyze.

With conflicts, the files keep the standard conflict markers (<<<<<<< yours, >>>>>>> app_blackbox 2.11.0), the command lists them and exits with 1, and the record and base stay at the old version. Resolve the markers, then run archipelago upgrade <brick> again: the changes you took from the new version now match it, so the second run merges cleanly and records the new version. A file that still has conflict markers is reported again and not merged.

Options:

OptionUse
--dry-runReport what would be added, replaced, merged and in conflict. Writes nothing.
--config <file.json>Vars that override the recorded ones, for example a secret that is not recorded. Required for a record written before records kept vars.
--base-ref <rev>A git revision with the files as the previous version generated them. For a record written before the base existed: without it, a file you did not change is still recognised by its fingerprint, but a file you changed has no base and conflicts as a whole.
--with-depsAlso upgrade, first, the bricks with a record that this brick generates for itself. Without it, only the named brick's packages are merged.
--local-bricks <repo>Load the new version from a local Archipelago repo, as with generate.
-o, --output <dir>The project directory (default .).

git must be on the PATH. The host wiring a new version changes outside its packages (the app's injector.dart, pubspec.yaml, build_prepare.yaml) is not merged; the brick's CHANGELOG lists it.

When the layout changed ​

upgrade prints a warning when the new version changed the layout so much that the run is a migration, not a patch: at least half of the recorded files are gone upstream, or the package directories were replaced (one package that became _api and _impl, for example). The lists above the warning are the whole story. Run upgrade --dry-run first and read them, expect to move your code into the new packages by hand, and update imports and pubspec.yaml dependencies yourself. A file reported as deleted upstream is kept, so nothing is lost. If the change is too large to review, keep your packages as they are and take only the new version's CHANGELOG items you need.

Merge by hand ​

Use this for a project with no record, or without the CLI.

You need three versions of each changed file:

  • ours: the file in your project now.
  • base: the file as the previous brick version generated it. Use the commit in which you generated the brick (git log -- .archipelago/generated/<brick>.json or git log -- <package dir> finds it), or generate the previous brick version into an empty scratch project.
  • theirs: the file as the new brick version generates it.
  1. Commit your work.

  2. Generate the new version in a scratch worktree. Delete the owned packages there first, so the brick sees a fresh install:

    bash
    git worktree add ../upgrade HEAD
    cd ../upgrade
    rm -rf features/app_blackbox/app_blackbox features/app_blackbox/app_blackbox_metrics
    archipelago generate app_blackbox_recorder
    cd -
  3. Merge each changed file. git merge-file writes the result into ours and adds conflict markers where both sides changed the same lines:

    bash
    f=features/app_blackbox/app_blackbox/lib/app_blackbox.dart
    git show <base-commit>:$f > base.dart
    git merge-file $f base.dart ../upgrade/$f
    rm base.dart
  4. Copy the files that are new in ../upgrade into your project. Delete the files that the brick CHANGELOG lists as moved or removed, if you did not change them.

  5. Copy ../upgrade/.archipelago/generated/<brick>.json and ../upgrade/.archipelago/base/<brick>/ into your project, replacing the old ones. The record and base now match the new version's own output, so the next upgrade still sees your changes.

  6. Copy the host-side wiring that the brick changed outside its packages (the app's injector.dart, pubspec.yaml and build_prepare.yaml). git diff HEAD in ../upgrade shows it.

  7. Run melos bootstrap, melos run build and melos run analyze, then remove the worktree with git worktree remove ../upgrade.

The Blackbox guide has the Blackbox-specific notes.