Skip to content

Latest commit

 

History

736 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

Sip

Features

  • Define and run scripts from a scripts.yaml file

    • 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.yaml before running it (sip validate)

  • Work with AI coding assistants

    • --json output for list, run --print, test and validate
    • 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 nearest pubspec.yaml to the current working directory
  • ${{ scriptsRoot }}: The nearest scripts.yaml to the current working directory
  • ${{ cwd }}: The current working directory
  • ${{ dartOrFlutter }}: Either dart or flutter executable, depending on the nearest pubspec.yaml to the current working directory
  • ${{ dart }}: The dart executable
  • ${{ flutter }}: The flutter executable

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": []
}

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.

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 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.

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.

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).

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_COLOR and TERM=dumb override 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-check or SIP_NO_VERSION_CHECK=1 also turn it off.
  • --json output 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:

  • recursive
  • dev_dependencies
  • bump (breaking, major, minor, patch)
  • dry-run
  • dart-only
  • flutter-only
  • pin
  • no-pin

About

A command-line tool to manage Dart mono-repos.

Resources

Stars

5 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages