sip_cli 0.20.0
sip_cli: ^0.20.0 copied to clipboard
Manage mono-repos with `sip`, run scripts and get dependencies concurrently.
SIP #
Sip is a command-line tool that simplifies managing Dart and Flutter projects. It helps you run scripts, manage pub commands, execute tests, and more — all from a single configuration file.

Features #
-
Define and run scripts from a
scripts.yamlfile- Supports nested scripts
- Run scripts concurrently
-
Run pub commands (
pub get,pub upgrade, etc.)- Runs recursively and concurrently
-
Run Dart/Flutter tests
- Recursive mode
- Fail fast mode (stops running tests after the first failure)
- Run only Dart or only Flutter tests
-
Customize executable commands (
dart,flutter, etc.) -
Validate
scripts.yamlbefore running it (sip validate) -
Work with AI coding assistants
--jsonoutput forlist,run --print,testandvalidate- An MCP server (
sip mcp) - Reference files for the popular assistants (
sip ai)
Installation #
dart pub global activate sip_cli
Usage #
sip --help
Quick Start #
Create a scripts.yaml file in your project root:
# scripts.yaml
hello:
world: echo "Hello, World!"
Run your script:
sip run hello world
scripts.yaml Configuration #
The scripts.yaml file defines all scripts and configuration for Sip. It usually lives in your project root.
Executable Commands #
Sip uses dart and flutter by default. To override them:
(executables):
dart: fvm dart
flutter: fvm flutter
Defining a Script #
A script maps a key to a command:
build_runner: dart run build_runner build
sip run build_runner
Commands can also be lists:
build_runner:
- cd packages/core && dart run build_runner build
- cd packages/data && dart run build_runner build
Script Key Rules #
- Allowed pattern:
^_?([a-z][a-z0-9_.\-]*)?(?<=[a-z0-9_])$ - Keys wrapped in parentheses (e.g.,
(command)) are reserved - Must start with a letter or
_ - Must end with a letter, number, or
_
Nested Scripts #
You can nest scripts:
format:
ui: cd packages/ui && dart format .
core: cd packages/core && dart format .
Use (command) to specify a default command for the top level script itself:
format:
(command): dart format .
ui: cd packages/ui && dart format .
core: cd packages/core && dart format .
Listing Scripts #
sip list # or sip ls
Search:
sip list build_runner
To explore nested scripts, you can use the --help flag:
sip run build_runner --help
Machine-readable listing
--json prints every script as data instead of a tree — its dotted key, its
aliases and description, the raw commands as written, the fully resolved
commands that will actually run, and the scripts.yaml line it was declared
on:
sip list --json
sip list build --json # only scripts matching a query
sip list --json --no-resolve # skip expanding references and variables
{
"version": 1,
"scriptsYaml": "/repo/scripts.yaml",
"executables": { "dart": "fvm dart" },
"variables": { "projectRoot": "/repo" },
"scripts": [
{
"path": ["build_runner", "build"],
"key": "build_runner.build",
"name": "build",
"parent": "build_runner",
"aliases": ["b"],
"description": null,
"private": false,
"runnable": true,
"bail": false,
"commands": ["${{ build_runner._ }} build"],
"resolved": [
{ "command": "fvm dart run build_runner build", "concurrent": false }
],
"resolveError": null,
"env": null,
"location": { "file": "/repo/scripts.yaml", "line": 28, "column": 3 }
}
]
}
Pass the path back to sip run (sip run build_runner build). A script
that cannot be resolved reports why in resolveError instead of failing the
whole listing.
Referencing Other Scripts #
Use ${{ key }} to reference another script:
pub_get: dart pub get
pub_get_ui: cd packages/ui && ${{ pub_get }}
References work with nesting:
pub:
(command): dart pub
get: "${{ pub }} get"
ui: cd packages/ui && ${{ pub.get }}
Important
Nested references are separated by dots. A colon (${{ pub:get }}) is not
substituted — it is passed to the shell verbatim and fails with
bad substitution.
Flags #
Sip forwards only the flags and arguments you explicitly include using ${{ --FLAG_NAME }}:
test: dart test ${{ --coverage }}
Examples:
sip run test --coverage=coverage
sip run other --flag value1 value2 --verbose
Unspecified flags are ignored.
Private Keys #
Private keys (starting with _) cannot be run directly, but can be referenced:
format:
_hidden: dart format .
(command): cd packages/ui && ${{ format._hidden }}
Bail #
Use --bail to stop running as soon as a command fails:
sip run format --bail
Or set it in config:
format:
(bail): true
(command): dart format
Note
(bail) must be given an explicit true. An empty value ((bail):) is
parsed as null and read as false.
Concurrent Commands #
Run scripts concurrently using (+):
format:
(command):
- echo "Running format"
- (+) cd packages/ui && dart format .
- (+) cd packages/core && dart format .
- echo "Finished running format"
You can disable concurrency by passing the --no-concurrent flag.
sip run format --no-concurrent
Variables #
Sip provides built-in variables:
${{ projectRoot }}: The nearestpubspec.yamlto the current working directory${{ scriptsRoot }}: The nearestscripts.yamlto the current working directory${{ cwd }}: The current working directory${{ dartOrFlutter }}: Eitherdartorflutterexecutable, depending on the nearestpubspec.yamlto the current working directory${{ dart }}: Thedartexecutable${{ flutter }}: Theflutterexecutable
Define custom variables under (variables):
(variables):
ocarinaTune: |-
echo "Playing Song of Time..."
Use them:
play: ${{ ocarinaTune }}
Example scripts.yaml #
(variables):
flutter: fvm flutter
build_runner:
build: dart run build_runner build
watch:
(description): Run build_runner in watch mode
(command): dart run build_runner watch
(aliases): [w]
test:
(command): "${{ flutter }} test ${{ --coverage }}"
coverage: "${{ test }} --coverage=coverage"
echo:
dirs:
- echo "${{ projectRoot }}"
- echo "${{ scriptsRoot }}"
- echo "${{ cwd }}"
format:
_command: dart format .
(command):
- echo "Running format"
- (+) ${{ format.ui }}
- (+) ${{ format.data }}
- (+) ${{ format.application }}
- echo "Finished running format"
ui: cd packages/ui && ${{ format._command }}
data: cd packages/data && ${{ format._command }}
application: cd application && ${{ format._command }}
Running Scripts #
Sip always executes from the directory containing your scripts.yaml, regardless of your current working directory.
sip run build_runner build
Run sip run --help for all available flags.
--print shows the resolved commands without executing them, and --json
alongside it prints them as data:
sip run build_runner build --print
sip run build_runner build --print --json
Validating scripts.yaml #
sip validate checks the file without running anything, and reports each
problem against the line it was written on:
sip validate
sip validate --json # structured diagnostics with stable codes
sip validate --fatal-warnings # exit non-zero for warnings too
scripts.yaml:8:1: error: ${{ a:b }} is not a valid substitution, so it is passed to the shell unchanged.
Separate script names with dots: ${{ a.b }}
scripts.yaml:16:3: warning: (bail) has no value, which sip reads as false.
Write `(bail): true`.
1 error, 1 warning
It finds:
| Code | Severity | What it catches |
|---|---|---|
invalid-yaml |
error | The file does not parse |
unknown-reference |
error | ${{ x }} names no script or variable |
malformed-substitution |
error | ${{ a:b }} and friends, passed to the shell verbatim |
reference-has-no-command |
error | A reference to a group with no (command) |
circular-reference |
error | Scripts that reference each other in a loop |
invalid-key |
error | A script name sip rejects |
empty-bail |
warning | (bail): with no value, which reads as false |
unknown-reserved-key |
warning | (descriptions) and other near-misses |
duplicate-alias |
warning | An alias claimed twice, which deactivates it |
empty-script |
warning | A script with no (command) and no subscripts |
Errors exit 78. Warnings exit 0 unless --fatal-warnings is passed.
Diagnostics go to stderr, so --json output on stdout stands alone.
Environment Configuration #
You can load environment variables before running a script:
build:
(command): flutter build apk
(env): .env # or ['.env', '.env.local']
Or run a command to generate env vars:
(env):
file: .env # or ['.env', '.env.local']
command: dart run generate_env.dart # can be a list of commands
Or inline variables:
(env):
vars:
FLUTTER_BUILD_MODE: release
Parent script env overrides nested script env.
Continuous Commands #
Use --never-exit to restart a command whenever it fails:
sip run build_runner watch --never-exit
Warning
Use with caution — the command restarts indefinitely.
You can stop the script by pressing Ctrl + C.
There is a 1 second delay between each run of the command, to prevent any runaway scripts.
Running Tests #
Run all tests:
sip test --recursive
Dart-only:
sip test --dart-only
Flutter-only:
sip test --flutter-only
Fail fast:
sip test --bail
Note
sip test fails when a test fails, when the test process exits non-zero, and
when it finds no packages to test — running nothing is not a pass.
Machine-readable results:
sip test --json
{
"passed": false,
"counts": { "passing": 12, "failing": 1, "skipped": 0 },
"failures": [
{
"path": "test/a_test.dart",
"test": "fails loudly",
"error": "Expected: <2>\n Actual: <1>"
}
],
"skipped": [],
"errors": [],
"combinedOnlyFailures": []
}
errors holds failures that are not test failures — a compile error, a
crashed runner, output sip could not parse. A run with an empty failures
list and a non-empty errors list still failed, which is why passed
exists rather than leaving it to be inferred from the counts.
combinedOnlyFailures is only filled by --bucket-retry-failures (see
Flutter test bucketing).
Experimental: Flutter Test Bucketing #
Warning
Experimental and strictly opt-in — may change or be removed without notice.
For large Flutter widget-test suites, --experimental-bucket combines test files into a handful of generated bucket files (one flutter test invocation per shard) to cut per-file VM-isolate startup overhead:
sip test --experimental-bucket
Files and directories given on the command line are bucketed the same way, so sip test --experimental-bucket test/a_test.dart test/b_test.dart runs both files in one combined runner.
Files that can't be safely combined (an explicit non-default TestWidgetsFlutterBinding subtype, or a test-surface mutation like tester.view.physicalSize left unreset) always run in their own isolated wrapper, never sharing an isolate with anything else. If a bucket's combined run looks untrustworthy — a compile error or hard binding assertion cuts it short — just that bucket's files are automatically discarded and re-run individually, and this is logged clearly.
A bucket that mixes files which initialize the Flutter test binding (testWidgets, ensureInitialized, …) with plain test() files clears HttpOverrides.global around each plain file's group, so the binding's mock HttpClient, which answers every request with 400, can't break their real HTTP calls.
This does not catch arbitrary test-state leakage between files sharing an isolate (e.g. an unreset image cache) — that class of bug can't be caught statically or by the fallback above, so treat a bucketed run as a strong signal, not a guarantee of unbucketed-equivalent results. See Shared-isolate state for how to keep files from leaking into each other, and --bucket-retry-failures for telling a leak from a real failure.
For CI matrix jobs, split the generated buckets across N jobs with --bucket-shard-index/--bucket-shard-count (entirely sip-side; distinct from flutter test's own --shard-index/--total-shards, which isn't recommended for this since it still pays full test-graph discovery/compile cost per shard):
sip test --experimental-bucket --bucket-shard-index=0 --bucket-shard-count=4
--bucket-count controls how many combined bucket files are generated (default: number of processors).
To keep specific files out of the combined buckets — goldens, say, or a file whose state leaks in a way sip can't detect — give their globs with --bucket-solo (repeatable) or list them under (test): in scripts.yaml. Matching files run in their own wrapper, exactly like files sip detects as unsafe, and stay in sharding and --json output. Globs are relative to the package root; write test/golden/**, not an absolute path. Quote them on the command line so the shell doesn't expand them.
sip test --experimental-bucket --bucket-solo 'test/golden/**' --bucket-solo '**/*_leaky_test.dart'
# scripts.yaml
(test):
bucket-solo:
- test/golden/**
- "**/*_leaky_test.dart"
Both sources are merged. sip validate reports a (test) section that isn't a map, an unknown setting, or a glob that doesn't parse.
Shared-isolate state
Every file in a bucket runs in the same isolate, one after another. Anything a file leaves behind in a global — a service locator registration, a singleton holding a fake, a static cache, debugDefaultTargetPlatformOverride, HttpOverrides.global — is still there when the next file starts. Run alone, each file gets a fresh isolate and never sees it, so a file can pass on its own and fail in a bucket, or worse, pass in a bucket only because an earlier file happened to set something up.
Undo whatever a file changes in a tearDown (or addTearDown right where it is set), so the next file starts from the defaults:
setUp(() {
GetIt.I.registerSingleton<Api>(FakeApi());
});
tearDown(() async {
await GetIt.I.reset();
debugDefaultTargetPlatformOverride = null;
imageCache.clear();
});
For state every test file touches, reset it once for the whole package in test/flutter_test_config.dart, which wraps every file, bucketed or not:
// test/flutter_test_config.dart
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
tearDown(() => GetIt.I.reset());
await testMain();
}
When a failure only shows up with bucketing on, --bucket-retry-failures re-runs each file that had a failing test on its own once the bucketed run is done. A file that passes alone is reported as failing only when combined — a warning listing the file and its tests, and a combinedOnlyFailures list in --json. The run still fails: the leak is real and will bite again, so fix the reset or move the file out with --bucket-solo. A file that fails alone too stays an ordinary failure. Files that already ran on their own (solo files, or a bucket of one) are not re-run.
sip test --experimental-bucket --bucket-retry-failures
AI Coding Assistants #
Install a sip reference file so your AI assistant knows how scripts.yaml and
the CLI work:
sip ai agents # AGENTS.md
sip ai claude # CLAUDE.md
sip ai cursor # .cursor/rules/sip-*.mdc
sip ai copilot # .github/copilot-instructions.md
sip ai windsurf # .windsurfrules
sip ai cline # .clinerules
sip ai all # every file above
Existing files are left alone; pass --force to overwrite them.
MCP server #
sip mcp runs sip as an MCP server over
stdio, so an assistant discovers your scripts as tools instead of having to
remember a CLI convention:
| Tool | What it does |
|---|---|
list_scripts |
Every script, with the commands it actually runs |
dry_run |
What a script expands to, without running it |
run_script |
Run a declared script; returns exit code, stdout and stderr |
validate |
Check scripts.yaml for problems |
Point your assistant at it:
{
"mcpServers": {
"sip": { "command": "sip", "args": ["mcp"] }
}
}
run_script takes a script name, never a command, so it cannot run anything
that is not declared in scripts.yaml. What a declared script does is of
course up to your project, so the tool is marked destructive.
Output for scripts and assistants #
sip's output is meant to be read by whoever is reading it:
- Colour follows the terminal. Piped or redirected output is plain text.
--color/--no-color,NO_COLOR,FORCE_COLORandTERM=dumboverride the guess. - The update notice never touches stdout. It goes to stderr, and is
skipped entirely (along with its network request) when stdout is not a
terminal.
--no-version-checkorSIP_NO_VERSION_CHECK=1also turn it off. --jsonoutput stands alone on stdout. Every message, warning and error goes to stderr.
Pub Commands #
Pub Get #
sip pub get
Automatically detects whether to use dart or flutter.
Recursive:
sip pub get --recursive
Pub Upgrade #
sip pub upgrade
Upgrade all or specific packages:
sip pub upgrade provider shared_preferences
Pub Downgrade #
sip pub downgrade
Pub Deps #
sip pub deps --json
Pub Constrain #
Constrain versions to your current resolution:
sip pub constrain
Constrain only selected packages:
sip pub constrain provider shared_preferences:2.3.0
Pin versions:
sip pub constrain provider --pin
Unpin:
sip pub constrain provider --no-pin
Supported flags:
recursivedev_dependenciesbump(breaking,major,minor,patch)dry-rundart-onlyflutter-onlypinno-pin