Thanks to visit codestin.com
Credit goes to github.com

Skip to content

artisanexperiences/arbor

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

219 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Arbor

Arbor is a self-contained binary for managing git worktrees to assist with agentic development of applications. It is cross-project, cross-language, and cross-environment compatible.

Development

All development occurs inside a worktree:

# Create a worktree for development
arbor work feature/new-feature
cd feature-new-feature

# Make changes, test, commit
go test ./...
arbor work another-feature  # Create another if needed

# When done with a worktree
cd ..
arbor remove feature-new-feature

Installation

Via Homebrew (Recommended for macOS/Linux)

brew tap artisanexperiences/tap
brew install arbor

Upgrade:

brew upgrade arbor

Via Direct Download

Download the latest release for your platform from the releases page.

Via Go Install

go install github.com/artisanexperiences/arbor/cmd/arbor@latest

Note: Installing via go install builds without version information. Use Homebrew or download releases for proper version metadata.

Build from Source

# Clone the repository
git clone https://github.com/artisanexperiences/arbor.git
cd arbor

# Build for your platform
go build -o arbor ./cmd/arbor

# Or build with version information
VERSION=$(git describe --tags --always)
COMMIT=$(git rev-parse --short HEAD)
DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
go build -ldflags "-X main.Version=$VERSION -X main.Commit=$COMMIT -X main.BuildDate=$DATE" -o arbor ./cmd/arbor

Quick Start

# Check arbor version
arbor version

# Initialise a new Laravel project (choose worktree or copy-on-write mode interactively)
arbor init [email protected]:user/my-laravel-app.git

# Initialise with a specific workspace mode
arbor init [email protected]:user/my-laravel-app.git --mode cow
arbor init [email protected]:user/my-laravel-app.git --mode worktree

# Create a feature workspace
arbor work feature/user-auth

# Create a workspace from a specific base branch
arbor work feature/user-auth -b develop

# Create a workspace without running scaffold steps
arbor work feature/user-auth --skip-scaffold

# Sync current workspace with upstream (defaults to main, uses rebase)
arbor sync

# Sync with a specific upstream branch
arbor sync --upstream develop

# Sync using merge instead of rebase
arbor sync --strategy merge

# Save sync settings to arbor.yaml for future use
arbor sync --upstream develop --strategy rebase --save

# List all workspaces with their status
arbor list

# Remove a workspace when done
arbor remove feature/user-auth

# Clean up merged workspaces
arbor prune

# Switch workspace mode (worktree ↔ copy-on-write)
arbor switch cow
arbor switch worktree

# Run scaffold steps on an existing workspace
arbor scaffold main
arbor scaffold feature/user-auth

# Destroy the entire project (removes all workspaces and project structure)
arbor destroy

# Pull updated config from the default branch workspace into the project root
arbor pull-config

Documentation

See AGENTS.md for development guide.

  • Command reference
  • Configuration files
  • Scaffold presets
  • Testing strategy

Commands

arbor sync

Synchronizes the current worktree branch with an upstream branch by fetching the latest changes and rebasing or merging.

Auto-Stashing (Default):

By default, arbor sync automatically stashes changes before syncing, including:

  • Tracked modifications
  • Untracked files

Note: Ignored files (like node_modules, vendor, .env) are not stashed for performance reasons. This is safe because git does not modify ignored files during rebase/merge operations, and skipping them makes sync much faster on large projects.

After a successful sync, the stashed changes are automatically restored.

# Sync with default settings (upstream: main, strategy: rebase, auto-stash: on)
arbor sync

# Sync with a specific upstream branch
arbor sync --upstream develop
arbor sync -u develop

# Sync using merge instead of rebase
arbor sync --strategy merge
arbor sync -s merge

# Use a specific remote
arbor sync --remote upstream
arbor sync -r upstream

# Disable auto-stashing (not recommended)
arbor sync --no-auto-stash

# Skip all confirmations
arbor sync --yes
arbor sync -y

# Save sync settings to arbor.yaml for future use
arbor sync --save

# Combination of options
arbor sync --upstream main --strategy rebase --save

Configuration:

Sync settings can be persisted in arbor.yaml:

sync:
  upstream: main
  strategy: rebase
  remote: origin
  auto_stash: true  # Default: true, set to false to disable

The command resolves settings in this order:

  1. CLI flags (--upstream, --strategy, --remote, --no-auto-stash)
  2. Project config (arbor.yaml)
  3. Project default_branch
  4. Interactive selection (if in interactive mode)

Notes:

  • Must be run from within a worktree (not project root)
  • Fails if worktree is on detached HEAD
  • Auto-stashes all changes by default (can be disabled with --no-auto-stash)
  • If stash pop fails due to conflicts, the stash is preserved and instructions are provided
  • Detects and blocks if rebase or merge is already in progress
  • Provides guidance when conflicts occur

arbor switch [MODE]

Converts the project between worktree and copy-on-write modes. Feature workspaces are removed during the switch (they can be recreated with arbor work <branch>).

# Switch a worktree-mode project to CoW
arbor switch cow

# Switch a CoW project back to worktree mode
arbor switch worktree

# Skip confirmation prompts
arbor switch cow --force

What happens during a switch:

  1. Lists any feature workspaces that will be removed
  2. Prompts for confirmation (skip with --force)
  3. Removes all feature workspaces
  4. Converts the project structure (.bare ↔ .arbor)
  5. Updates arbor.yaml with the new workspace_mode

arbor scaffold [PATH]

Run scaffold steps for an existing worktree. This is useful when:

  • You used arbor init --skip-scaffold to clone without running scaffold
  • You want to re-run scaffold steps on an existing worktree
  • You need to scaffold a worktree you're not currently in
# Scaffold a specific worktree by path
arbor scaffold main
arbor scaffold feature/user-auth

# When inside a worktree, scaffold current (prompts for confirmation)
arbor scaffold

# When at project root without args, interactively select worktree
arbor scaffold

# Skip confirmation prompts
arbor scaffold main --force
arbor scaffold main -f

arbor pull-config

Updates the project-level arbor.yaml (at the project root) with the one from the default branch worktree.

Use this when the repository arbor.yaml (committed in the default branch) has been updated by your team and you want to pull those changes into your local project config.

This replaces the project arbor.yaml entirely with the one from the default branch worktree.

# Pull config from the default branch worktree (prompts for confirmation)
arbor pull-config

# Skip confirmation prompt
arbor pull-config --force
arbor pull-config -f

# Preview what would happen without making changes
arbor pull-config --dry-run

# Show detailed output
arbor pull-config --verbose
arbor pull-config -v

# Suppress all non-essential output
arbor pull-config --quiet
arbor pull-config -q

--skip-scaffold

Both arbor init and arbor work support --skip-scaffold to defer scaffold steps and run them manually later:

# Clone without scaffolding
arbor init [email protected]:user/repo.git --skip-scaffold

# Create a worktree without scaffolding
arbor work feature/my-feature --skip-scaffold

# Scaffold when ready
arbor scaffold main
arbor scaffold feature/my-feature

Workspace Modes

Arbor supports two ways to manage parallel feature workspaces. Choose at arbor init time, or switch later with arbor switch.

Worktree Mode (default)

The traditional mode. Uses a single bare git repository (.bare/) with linked git worktrees for each branch. The object store is shared, so additional workspaces cost only the working-tree files.

my-project/
  .bare/          # Shared bare git repo
  arbor.yaml
  main/           # Default branch worktree
  feature-foo/    # Feature branch worktree

Copy-on-Write (CoW) Mode

Each workspace is a full, independent git clone. On filesystems that support copy-on-write (APFS on macOS, Btrfs/XFS on Linux), new workspaces are created using filesystem-level cloning — they take zero additional disk space until files are actually modified.

my-project/
  .arbor/         # CoW project marker
  arbor.yaml
  main/           # Normal git clone (default branch)
  feature-foo/    # CoW clone of main

Benefits:

  • Near-instant workspace creation (cloning is a filesystem operation)
  • Shared disk blocks for unmodified files (vendor/, node_modules/, source code)
  • Fully independent git repos — no shared .bare lock contention
  • Compatible with all git tools and IDEs (normal .git/ directories)

Requirements for full CoW benefits:

  • macOS with APFS filesystem (default since macOS High Sierra)
  • Linux with Btrfs or XFS (with reflink support enabled)
  • Falls back to regular copy on unsupported filesystems (works, just without disk savings)
# Choose CoW at init time
arbor init [email protected]:user/repo.git --mode cow

# Or switch an existing project
arbor switch cow       # worktree → CoW
arbor switch worktree  # CoW → worktree

Note: arbor switch removes feature workspaces that cannot be migrated in-place. Recreate them with arbor work <branch> after switching.

workspace_mode Config

The mode is stored in arbor.yaml at the project root:

workspace_mode: cow       # or "worktree" (default when absent)

Configuration

Arbor uses a three-tier configuration system to separate team configuration from local state.

Configuration Hierarchy

1. Project Config (<project-root>/arbor.yaml)

Located at the project root (alongside .bare/ or .arbor/), this file contains:

  • Scaffold steps and cleanup steps
  • Preset selection
  • Tool configurations
  • Project-wide settings

This file is not versioned (the project root is not a git repository).

During arbor init, if an arbor.yaml file is found in the repository, you'll be prompted to copy it to the project root.

2. Repository Config (<worktree>/arbor.yaml)

Located inside each worktree and committed to git, this file contains:

  • Team default scaffold steps
  • Shared cleanup steps
  • Tool configurations

This file serves as the source of truth for team configuration and is copied to the project root during arbor init.

3. Local State (<worktree>/.arbor.local)

Located inside each worktree and NOT versioned (should be in .gitignore), this file contains:

  • db_suffix - unique database suffix for the worktree
  • Other worktree-specific runtime state

This file is automatically created by Arbor and should never be committed.

Example .gitignore entry:

.arbor.local

Example .arbor.local file:

db_suffix: "sunset"

Sharing Team Configuration

To share scaffold configuration with your team:

  1. Create arbor.yaml in your repository with scaffold steps:
preset: laravel
scaffold:
  steps:
    - name: file.copy
      from: .env.example
      to: .env
    - name: db.create
    - name: php.composer
      args: ["install"]
  1. Commit and push to git:
git add arbor.yaml
git commit -m "Add Arbor scaffold configuration"
git push
  1. Team members run arbor init:
arbor init user/repo
# → Found arbor.yaml in repository. Copy to project root for team config? [Y/n]
# → Press Enter to use team config

The config will be automatically copied to their project root and used for all worktrees.

Scaffold Steps

Scaffold steps define actions to run when creating a new worktree. Each step can:

  • Run commands (bash, binary, composer, npm, etc.)
  • Manage databases (create/destroy)
  • Read/write environment variables
  • Copy files
  • Execute Laravel Artisan commands

Pre-Flight Checks

Pre-flight checks validate dependencies before any scaffold steps execute. This prevents worktrees from being left in a broken state due to missing requirements.

Configuration:

scaffold:
  pre_flight:
    condition:
      # Check environment variables are set
      env_exists:
        - OP_VAULT
        - OP_ITEM
      
      # Check commands/binaries are installed
      command_exists:
        - op        # 1Password CLI
        - herd      # Laravel Herd
        - composer
      
      # Check required files exist
      file_exists:
        - .env.op
        - package.json
  
  steps:
    # Your scaffold steps here

Supported Conditions:

All condition types support both single values and arrays:

Condition Single Value Array Description
env_exists env_exists: API_KEY env_exists: [API_KEY, API_SECRET] Check OS environment variables are set
command_exists command_exists: docker command_exists: [docker, docker-compose] Check commands are available in PATH
file_exists file_exists: .env file_exists: [.env, composer.json] Check files exist in worktree
os os: darwin os: [darwin, linux] Check operating system
context_var context_var: {key: skip_migrations, value: "true"} — Check a runtime context variable set by a previous step

You can combine multiple condition types:

pre_flight:
  condition:
    env_exists:
      - OP_VAULT
      - OP_ITEM
    command_exists: op
    file_exists: .env.op
    os: darwin

Error Messages:

When pre-flight checks fail, you'll see a detailed breakdown:

âś— Running pre-flight checks

Pre-flight checks failed:

Missing environment variables:
  - OP_VAULT
  - OP_ITEM

Missing commands:
  - op

Missing files:
  - .env.op

Please resolve these issues and try again.

Example: 1Password Integration

scaffold:
  pre_flight:
    condition:
      env_exists:
        - OP_VAULT
        - OP_ITEM
      command_exists: op
      file_exists: .env.op
  
  steps:
    - name: bash.run
      command: "op inject -i .env.op -o .env"
      
    - name: php.composer
      args: ["install"]

This ensures that before any steps run:

  • The op CLI is installed
  • Environment variables OP_VAULT and OP_ITEM are set
  • The .env.op template file exists

Notes:

  • Pre-flight checks are skipped when using --skip-scaffold
  • File paths in file_exists are relative to the worktree (no template variables)
  • All checks must pass for scaffold to proceed

Configuration Structure

scaffold:
  steps:
    - name: step.name
      enabled: true
      args: ["--option"]
      condition:
        env_file_contains:
          file: .env
          key: DB_CONNECTION

cleanup:
  steps:
    - name: cleanup.step

Template Variables

All steps support template variables that are replaced at runtime:

Variable Description Example
{{ .Path }} Worktree directory name feature-auth
{{ .RepoPath }} Project directory name myapp
{{ .RepoName }} Repository name myapp
{{ .SiteName }} Site/project name myapp
{{ .Branch }} Git branch name feature-auth
{{ .DbSuffix }} Database suffix (from db.create) swift_runner
{{ .VarName }} Custom variable from env.read or captured output Custom values

Built-in Steps

Database Steps

db.create - Create a database with unique name

- name: db.create
  type: mysql       # or pgsql, auto-detected from DB_CONNECTION if omitted
  args: ["--prefix", "app"]  # optional: customize database prefix
  • Generates unique name: {prefix}_{adjective}_{noun} or {site_name}_{adjective}_{noun}
  • Suffix is generated once per init or work invocation and shared across all db.create steps
  • Auto-detects engine from DB_CONNECTION in .env
  • Retries up to 5 times on collision
  • Persists suffix to .arbor.local for cleanup

Interactive Features (MySQL/PostgreSQL):

In interactive mode, db.create offers database reuse and migration control:

  • Database Reuse: When other worktrees exist with databases, you can choose to reuse an existing database instead of creating a new one. This is useful for stacked PRs or related feature branches that share the same data.

    • The suffix from the selected worktree is copied to your current worktree
    • Both worktrees point to the same database
    • Non-interactive mode (CI, --no-interactive, --force) always creates new databases
  • Migration Prompt: After database creation/selection, you'll be asked whether to run migrate:fresh --seed:

    • Confirm: Migrations run as part of the scaffold
    • Decline: The migration step is skipped
    • Non-interactive mode always runs migrations

Multiple databases with shared suffix:

scaffold:
  steps:
    - name: db.create
      args: ["--prefix", "app"]
    - name: db.create
      args: ["--prefix", "quotes"]
    - name: db.create
      args: ["--prefix", "knowledge"]

Result: Creates app_cool_engine, quotes_cool_engine, knowledge_cool_engine (same suffix, different prefixes)

db.destroy - Clean up databases matching suffix pattern

- name: db.destroy
  type: mysql  # matches db.create type
  • Drops all databases matching the suffix pattern
  • Runs automatically during arbor remove

Interactive Cleanup Confirmation:

In interactive mode, before dropping databases, you'll be shown a list of databases that will be affected and asked to confirm:

  • Confirm: All databases matching the suffix are dropped
  • Decline: Cleanup is skipped (databases are preserved)
  • Non-interactive mode (CI, --no-interactive, --force) drops databases without asking

This prevents accidental destruction of shared databases when working with stacked PRs.

Environment Steps

env.read - Read from .env and store as variable

- name: env.read
  key: DB_HOST
  store_as: DbHost  # optional, defaults to key name
  file: .env        # optional, defaults to .env
  • Stores value as {{ .DbHost }} for later steps
  • Fails if key not found

env.write - Write to .env file

- name: env.write
  key: DB_DATABASE
  value: "{{ .SiteName }}_{{ .DbSuffix }}"
  file: .env  # optional, defaults to .env
  • Creates .env if missing
  • Replaces existing values in-place
  • Preserves comments, blank lines, and ordering
  • Supports template variables

env.copy - Copy keys from another worktree's .env file

# Copy a single key
- name: env.copy
  source: ../main           # Source worktree path (relative or absolute)
  key: API_KEY

# Copy multiple keys
- name: env.copy
  source: ../main
  keys:
    - API_KEY
    - API_SECRET
    - STRIPE_KEY
  source_file: .env         # optional, defaults to .env
  file: .env                # optional target file, defaults to .env
  • Copies environment variables from a source worktree to the current worktree
  • Useful for copying API keys, secrets, or other values from main to feature branches
  • Creates target .env if missing
  • Updates existing keys in-place
  • Supports relative paths (resolved from worktree) or absolute paths

Node.js Steps

node.npm - npm package manager

- name: node.npm
  args: ["install"]

node.yarn - Yarn package manager

- name: node.yarn
  args: ["install"]

node.pnpm - pnpm package manager

- name: node.pnpm
  args: ["install"]

node.bun - Bun package manager

- name: node.bun
  args: ["install"]

PHP Steps

php.composer - Composer dependency manager

- name: php.composer
  args: ["install"]

php.laravel - Laravel Artisan commands

- name: php.laravel
  args: ["migrate:fresh", "--no-interaction"]

Capture command output:

- name: php.laravel
  args: ["--version"]
  store_as: LaravelVersion

- name: env.write
  key: APP_FRAMEWORK_VERSION
  value: "{{ .LaravelVersion }}"

herd.link - Laravel Herd link

- name: herd.link

Utility Steps

bash.run - Run bash commands

- name: bash.run
  command: echo "Setting up {{ .Path }}"

Capture output for use in later steps:

- name: bash.run
  command: "git rev-parse --short HEAD"
  store_as: GitCommit

- name: env.write
  key: BUILD_COMMIT
  value: "{{ .GitCommit }}"

file.copy - Copy files with template replacement

- name: file.copy
  from: .env.example
  to: .env

command.run - Run any command

- name: command.run
  command: npm
  args: ["run", "build"]

Capture output for use in later steps:

- name: command.run
  command: "date +%Y-%m-%d"
  store_as: BuildDate

- name: env.write
  key: BUILD_DATE
  value: "{{ .BuildDate }}"

Step Options

All steps support these configuration options:

Option Type Description
enabled boolean Enable/disable step (default: true)
condition object Conditional execution rules
args array Arguments passed to the step (e.g., ["--prefix", "app"])
store_as string Store command output as template variable (trimmed, on success only)

Steps execute in the order they appear in the configuration file.

Conditions

Steps can be conditionally executed based on environment. Conditions support both single values and arrays:

# Single value conditions
condition:
  env_file_contains:
    file: .env
    key: DB_CONNECTION

# Array conditions - check multiple items at once
condition:
  env_exists:
    - API_KEY
    - API_SECRET
  command_exists:
    - docker
    - docker-compose
  file_exists:
    - .env
    - composer.json

# Runtime context variable set by a previous step
condition:
  context_var:
    key: skip_migrations
    value: "true"

# Negate a condition
condition:
  not:
    context_var:
      key: skip_migrations
      value: "true"

Example Configuration

Complete example for a Laravel project:

scaffold:
  steps:
    # Create database if DB_CONNECTION is set
    - name: db.create
      condition:
        env_file_contains:
          file: .env
          key: DB_CONNECTION

    # Write database name to .env
    - name: env.write
      key: DB_DATABASE
      value: "{{ .SiteName }}_{{ .DbSuffix }}"

    # Install dependencies
    - name: php.composer
      args: ["install"]

    - name: node.npm
      args: ["install"]

    # Run migrations
    - name: php.laravel
      args: ["migrate:fresh", "--no-interaction"]

    # Set domain based on worktree path
    - name: env.write
      key: APP_DOMAIN
      value: "app.{{ .Path }}.test"

    # Generate application key
    - name: php.laravel
      args: ["key:generate"]

cleanup:
  steps:
    # Clean up databases
    - name: db.destroy

Example: Multiple databases with shared suffix

For applications that need multiple databases (e.g., main app, quotes, knowledge):

scaffold:
  steps:
    # Create three databases with different prefixes but shared suffix
    - name: db.create
      args: ["--prefix", "app"]

    - name: db.create
      args: ["--prefix", "quotes"]

    - name: db.create
      args: ["--prefix", "knowledge"]

    # Write the main database name to .env
    - name: env.write
      key: DB_DATABASE
      value: "app_{{ .DbSuffix }}"

    # Write other database names to .env (optional)
    - name: env.write
      key: DB_QUOTES_DATABASE
      value: "quotes_{{ .DbSuffix }}"

    - name: env.write
      key: DB_KNOWLEDGE_DATABASE
      value: "knowledge_{{ .DbSuffix }}"

This creates: app_cool_engine, quotes_cool_engine, knowledge_cool_engine

What We Handle For You

Database Naming

  • Automatically generates unique, readable database names
  • Suffix is generated once per init or work invocation
  • Format: {prefix}_{adjective}_{noun} or {site_name}_{adjective}_{noun} (e.g., myapp_swift_runner, app_cool_engine)
  • Multiple db.create steps share the same suffix, allowing consistent database naming
  • Handles collisions with automatic retries
  • Enforces PostgreSQL/MySQL length limits

Database Cleanup

  • Automatically drops databases when worktree is removed
  • Uses pattern matching to find all databases with same suffix
  • Integrates with arbor remove command

Template Variables

  • All template syntax uses Go's text/template
  • Handles whitespace variations: {{ .Path }}, {{ .Path }}, {{ .Path }}
  • Fails fast on unknown variables with clear error messages
  • Supports dynamic variables from previous steps

File Operations

  • Atomic writes for environment files
  • Preserves file permissions
  • Maintains existing formatting (comments, blank lines, ordering)
  • Creates directories as needed

Error Handling

  • Graceful degradation where appropriate
  • Clear error messages for configuration issues
  • Non-fatal warnings for optional operations

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages