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:
{
"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 formatis not a change. - Tool output is not recorded: dot directories (
.dart_tool),build/,coverage/,pubspec.lock, themelos_<package>.imlfiles thatmelos bootstrapwrites, and generated code (*.g.dart,*.gen.dart,*.module.dart,*.freezed.dartand similar). - Files git ignores are not recorded.
flutter createwrites files such asandroid/gradlewandios/Flutter/Generated.xcconfigthat are missing from every other clone, so recording them would stop your teammates' runs. versionis the brick version that generated the files, andvarsare the values it ran with, soarchipelago upgradecan run it again. Internal vars (_-prefixed) and vars whose name looks like a secret (token,secret,password,apiKey,dsn...) are not recorded; pass them toupgradewith--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 inarchipelago 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.yamllists the version of each brick with a record underbrick_versions(app_blackbox: 2.10.0).bricks_installedstays 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.dartand initializers,build_prepare.yaml,archipelago.yaml, the rootpubspec.yamland 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'slib/background/background_dispatcher.dartorapp_debugger's overlay test in the app. They live in the host app, which no brick owns. - Shared files inside an owned package (
brickSharedFiles):
| Brick | File | Also written by |
|---|---|---|
app_debugger | features/app_debugger/app_debugger/lib/src/di/debug_tools.dart, features/app_debugger/app_debugger/pubspec.yaml | every app_debugger_* panel brick and app_blackbox_recorder (debug panel marker region, path dependency) |
monorepo_toolkit | devtools/monorepo_toolkit/lib/src/command_runner.dart, devtools/monorepo_toolkit/pubspec.yaml | every devtool brick (affected, coverage_manager, size_guard, asset_generator...: its command and path dependency) |
push_notification_sdk | features/push_notification/push_notification_impl/pubspec.yaml | push_notification_onesignal (di_modules depends_on) |
shared_app_config | shared/app_config/lib/src/env/env.dart, shared/app_config/lib/src/env/app_env.dart | auth_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):
| Brick | Why |
|---|---|
flutter_modular_monorepo | It 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_scaffold | It generates a host app. Same reason. |
initializer_skeleton | It adds one initializer file to the host app. |
state_management | It adds a state holder to a feature package. |
ui_kit_component | It adds a component to flutter_ui_kit's packages. |
ui_kit_theme | It 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.yamlentry 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.dartand 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:
ARCHIPELAGO_FORCE=1 archipelago generate app_blackbox_recorder
ARCHIPELAGO_FORCE=1 mason make app_blackbox_recorder --on-conflict overwriteThe 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:
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-runThe 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:
archipelago upgrade app_blackbox --dry-run # report only, write nothing
archipelago upgrade app_blackbox
archipelago upgrade feature_monorepo_skeleton/home # one record per featureIt works like this:
- 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. - 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):
| Case | Result |
|---|---|
| New upstream | Added. |
| You did not change it | Replaced with the new version. |
| Only you changed it | Kept. |
| Both changed it | git merge-file. Lines that both sides changed get conflict markers. |
| Binary file that both changed | Conflict. Your file is kept. |
| You deleted it and the new version changed it | Conflict. Nothing is written. |
| Deleted upstream | Reported 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. |
- Generated code and every file outside the brick's packages are never written. Shared files (
brickSharedFiles) keep your content: only a sharedpubspec.yamlchanges, by getting the dependencies the new version adds. Any other change the new version makes to a shared file is in its CHANGELOG. - With no conflict, it takes the new record and base and sets
brick_versionsinarchipelago.yaml. Commit everything, then runmelos bootstrap,melos run buildandmelos 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:
| Option | Use |
|---|---|
--dry-run | Report 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-deps | Also 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>.jsonorgit 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.
Commit your work.
Generate the new version in a scratch worktree. Delete the owned packages there first, so the brick sees a fresh install:
bashgit 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 -Merge each changed file.
git merge-filewrites the result into ours and adds conflict markers where both sides changed the same lines:bashf=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.dartCopy the files that are new in
../upgradeinto your project. Delete the files that the brick CHANGELOG lists as moved or removed, if you did not change them.Copy
../upgrade/.archipelago/generated/<brick>.jsonand../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.Copy the host-side wiring that the brick changed outside its packages (the app's
injector.dart,pubspec.yamlandbuild_prepare.yaml).git diff HEADin../upgradeshows it.Run
melos bootstrap,melos run buildandmelos run analyze, then remove the worktree withgit worktree remove ../upgrade.
The Blackbox guide has the Blackbox-specific notes.