# ⛩️ Portal - Git Worktree Manager

[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
[![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
[![Type Checked: mypy](https://img.shields.io/badge/type%20checked-mypy-blue)](https://github.com/python/mypy)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

<p align="center">
  <img src="docs/assets/demo.gif" alt="Portal Demo" width="800">
</p>

Portal is a modern CLI tool for managing Git worktrees with automatic color coding and editor integration. Each worktree gets a deterministic color that syncs across your terminal, IDE, and CLI output, making it easy to identify which worktree you're working in.

## Features

### Color Coding System

- **Deterministic colors**: Each worktree gets a consistent color based on its name
- **Cross-tool sync**: Colors appear in iTerm tabs, VS Code/Cursor themes, and CLI output
- **Visual identification**: Quickly identify which worktree you're working in

### Automation & Hooks

- **8 hook types**: `command`, `copy`, `template`, `mkdir`, `symlink`, `env_update`, `script`, `git`
- **Project-level overrides**: Use `.portal` files for project-specific automation
- **Conditional execution**: Run hooks based on file existence, platform, environment
- **Security-hardened**: Built-in protection against injection attacks and path traversal
- **Variable substitution**: Dynamic values like `{{worktree_name}}`, `{{color_hex}}`, `auto` ports

### Developer Tools

- **Interactive menu**: Arrow-key navigation with color-highlighted worktree entries
- **iTerm integration**: Open worktrees in new tabs with colored tab indicators
- **Claude integration**: Start AI-assisted coding sessions in worktree context
- **Shell completions**: Support for Bash and Zsh

### Technical Design

- **Type-safe**: Fully type-checked with mypy strict mode
- **Async operations**: Built with Python's async/await for better performance
- **Event-driven**: Extensible architecture using event bus pattern
- **Cross-platform**: Works on macOS and Linux (Windows via WSL)

## Installation

### Prerequisites

- Python 3.12 or higher
- Git 2.5+ (for worktree support)
- iTerm2 (optional, for terminal integration on macOS)
- VS Code or Cursor (optional, for editor integration)

### Quick Install

```bash
# With uv (recommended)
uv tool install git-portal[cli]

# With pipx
pipx install git-portal[cli]

# With Homebrew (macOS)
brew tap aureliensibiril/portal
brew install portal
```

### From Source

```bash
git clone https://github.com/aureliensibiril/portal.git
cd portal
uv venv --python 3.12
source .venv/bin/activate
uv pip install -e ".[cli]"
```

### Shell Integration

Install shell completions and functions:

```bash
# Install shell completions and portal cd function
portal shell install
```

This adds tab completion for Portal commands and the `pw` alias for quick worktree switching.

## Quick Start

### Create Your First Worktree

```bash
# Interactive mode - shows numbered list with color indicators
portal list

# Create a new worktree
portal new feature/awesome-feature

# Create from specific base branch
portal new hotfix/urgent-fix --base release-v2.0
```

### Example Workflow

```bash
# 1. Open the interactive menu (arrow-key navigation with true-color entries)
$ portal

⛩️ Portal - Git Worktree Manager
↑/↓ Navigate • Enter: Select • N: New worktree • Q: Quit

▶ main (base)              # highlighted, colored per worktree
  feature/auth
  hotfix/bug

# Pressing Enter on a worktree opens an action submenu:
# ↵  Open terminal in new tab
# 📝 Open in Cursor
# 🤖 Open with Claude
# 🎨 Reset IDE colors
# 🗑️  Delete worktree

# 2. Create new worktree with hooks
$ portal new feature/payments
✅ Created worktree: feature/payments
   Path: ../portal_worktrees/feature_payments
   Color: Purple (#9C27B0)
✅ Hook: Copy environment file
✅ Hook: Install dependencies
✅ Opened in Cursor with Purple theme

# 3. Switch between worktrees
$ portal switch feature/auth
✅ Switched to feature/auth

# 4. Open in iTerm with tab color
$ portal terminal feature/payments
✅ Opened feature/payments in new iTerm tab

# 5. Start AI coding session
$ portal claude feature/payments
✅ Opened feature/payments with Claude
```

## Command Reference

### Core Worktree Management

| Command                    | Description                           | Example                                  |
| -------------------------- | ------------------------------------- | ---------------------------------------- |
| `portal`                   | Open interactive menu                 | `portal`                                 |
| `portal list`              | Interactive worktree list with colors | `portal list --format table`             |
| `portal new <branch>`      | Create new worktree                   | `portal new feature/auth --base develop` |
| `portal delete [worktree]` | Delete a worktree                     | `portal delete feature/old --force`      |
| `portal switch [worktree]` | Switch to a worktree                  | `portal switch main --open`              |
| `portal info [worktree]`   | Show worktree information             | `portal info feature/auth`               |
| `portal branches`          | List available branches               | `portal branches --fetch`                |

**Options detail:**

| Command         | Option                                          | Description                              |
| --------------- | ----------------------------------------------- | ---------------------------------------- |
| `portal new`    | `-b/--base <branch>`                            | Base branch for the new worktree         |
| `portal new`    | `-t/--template <name>`                          | Template to use                          |
| `portal new`    | `--fetch`                                       | Fetch remote branches before creating    |
| `portal new`    | `--no-hooks`                                    | Skip hook execution                      |
| `portal new`    | `--no-open`                                     | Don't open in editor after creation      |
| `portal delete` | `-f/--force`                                    | Force deletion                           |
| `portal delete` | `--with-branch`                                 | Also delete the associated branch        |
| `portal list`   | `--format [interactive\|simple\|table\|json]`   | Output format (default: interactive)     |
| `portal list`   | `--no-interactive`                               | Disable interactive mode                 |
| `portal switch` | `--open`                                        | Open in editor after switching           |

### Editor Integration

| Command                    | Description                         | Example                      |
| -------------------------- | ----------------------------------- | ---------------------------- |
| `portal cursor [worktree]` | Open in Cursor IDE with color theme | `portal cursor feature/auth` |
| `portal vscode [worktree]` | Open in VS Code with color theme    | `portal vscode main`         |

### Terminal Integration

| Command                      | Description               | Example                      |
| ---------------------------- | ------------------------- | ---------------------------- |
| `portal terminal [worktree]` | Open iTerm tab with color | `portal terminal feature/ui` |
| `portal claude [worktree]`   | Open iTerm with Claude AI | `portal claude hotfix/bug`   |

### Hook Management

| Command                    | Description                      | Example                        |
| -------------------------- | -------------------------------- | ------------------------------ |
| `portal hooks list`        | Show hook configuration guidance | `portal hooks list`            |
| `portal hooks run <stage>` | Run hooks for a specific stage   | `portal hooks run post_create --dry-run` |

### Configuration

| Command                      | Description                                | Example                       |
| ---------------------------- | ------------------------------------------ | ----------------------------- |
| `portal config show`         | Display current configuration with sources | `portal config show`          |
| `portal config show --json`  | Show configuration as JSON                 | `portal config show --json`   |
| `portal config show --global`| Show only global configuration             | `portal config show --global` |
| `portal config edit`         | Edit configuration file                    | `portal config edit --global` |
| `portal config set`          | Set a configuration value                  | `portal config set editor.default vscode --global` |

### Integration Management

| Command                        | Description                          | Example                        |
| ------------------------------ | ------------------------------------ | ------------------------------ |
| `portal integrations list`     | List available integrations          | `portal integrations list`     |
| `portal integrations status`   | Show status of all integrations      | `portal integrations status`   |
| `portal integrations test`     | Test all available integrations      | `portal integrations test`     |

### Utility Commands

| Command                | Description                  | Example                 |
| ---------------------- | ---------------------------- | ----------------------- |
| `portal shell install` | Install shell completions    | `portal shell install`  |
| `portal --version`     | Show Portal version          | `portal --version`      |

## Configuration

Portal uses YAML configuration files with sensible defaults. You can inspect the current configuration and see exactly what values Portal will use with the `config show` command.

### Viewing Configuration

The `portal config show` command displays the merged configuration from all sources:

```bash
# Show current configuration with resolved paths and sources
portal config show

# Output as JSON for scripting
portal config show --json

# Show only global configuration
portal config show --global
```

This command shows:

- **Repository Context**: Current Git repository, project name, and branch
- **Configuration Values**: All settings with resolved paths showing exactly where worktrees will be created
- **Configuration Sources**: Which config files are loaded (project `.portal.yml`, global `~/.portal/config.yml`, and defaults)

### Example Configuration (`~/.portal/config.yml`)

```yaml
# Portal Configuration
version: "1.0"
base_dir: "../{project}_worktrees" # Where worktrees are created

# Color settings
colors:
  enabled: true
  sync_iterm: true # Sync colors to iTerm
  sync_cursor: true # Sync colors to Cursor
  sync_claude: true # Generate Claude context
  high_contrast: false # Use high-contrast palette

# Editor settings
editor:
  default: "cursor" # Default editor (cursor/vscode/vim)
  auto_open: true # Auto-open on creation

# Shell integration
shell:
  completions_enabled: true # Enable tab completion
  cd_function: true # Install 'portal cd' function
  prompt_integration: false # Show worktree in prompt

# Global hooks (can be overridden per-project)
hooks:
  post_create:
    - type: mkdir
      config:
        paths: ["logs", "tmp"]
    - type: command
      config:
        command: "echo 'Worktree created'"

# Branch pattern mappings
branch_patterns:
  "feature/*": "feature"
  "hotfix/*": "hotfix"
  "release/*": "release"
  "bugfix/*": "bugfix"
```

## Hook System

Portal's powerful hook system automates worktree setup, teardown, and environment configuration. Hooks execute at specific lifecycle stages and support multiple operation types.

### Hook Types

Portal supports 8 different hook types for comprehensive automation:

| Hook Type        | Purpose                  | Configuration                                    |
| ---------------- | ------------------------ | ------------------------------------------------ |
| **`command`**    | Execute shell commands   | `command: "npm install"`                         |
| **`copy`**       | Copy files/directories   | `from: ".env.example"`, `to: ".env"`             |
| **`template`**   | Process Jinja2 templates | `template: "config.j2"`, `output: "config.json"` |
| **`mkdir`**      | Create directories       | `paths: ["logs", "tmp", "cache"]`                |
| **`symlink`**    | Create symbolic links    | `source: "../shared"`, `target: "public"`        |
| **`env_update`** | Update environment files | `file: ".env"`, `updates: {...}`                 |
| **`script`**     | Run custom scripts       | `script: "setup.sh"`                             |
| **`git`**        | Git operations           | `operation: "fetch"`                             |

### Hook Stages

Hooks execute at these lifecycle stages:

- **`pre_create`**: Before worktree creation
- **`post_create`**: After worktree creation
- **`pre_delete`**: Before worktree deletion
- **`post_delete`**: After worktree deletion
- **`pre_switch`**: Before switching worktrees
- **`post_switch`**: After switching worktrees
- **`pre_list`**: Before listing worktrees
- **`post_list`**: After listing worktrees

> **Note:** The `portal hooks run` command supports manually running `post_create`, `pre_delete`, `pre_switch`, and `post_switch`. The other stages are triggered automatically by their respective operations.

### Configuration Hierarchy

Portal merges configuration from multiple sources (in order of priority):

1. **`.portal.yml`** - Project configuration (highest priority)
2. **`~/.portal/config.yml`** - Global configuration
3. **Default configuration** (lowest priority)

Each level merges with the previous, allowing you to override specific values without redefining everything.

### Basic Hook Configuration

**Global Configuration** (`~/.portal/config.yml`):

```yaml
version: "1.0"
base_dir: "../{project}_worktrees"

hooks:
  post_create:
    - type: command
      config:
        command: "echo 'Setting up worktree...'"
    - type: mkdir
      config:
        paths: ["logs", "tmp"]
```

**Project Configuration** (`.portal.yml` in project root):

```yaml
# These hooks merge with (and override) global hooks
hooks:
  post_create:
    # Copy files from project root to new worktree
    - type: copy
      config:
        from: ".env.local.example"
        to: ".env"
      name: "Setup local environment"

    # Install dependencies conditionally
    - type: command
      config:
        command: "npm install --frozen-lockfile"
      condition: "file_exists:package.json"
      name: "Install Node.js dependencies"

    # Auto-configure environment with dynamic values
    - type: env_update
      config:
        file: ".env"
        updates:
          DATABASE_NAME: "{{project}}_{{worktree_name}}_dev"
          API_PORT: "auto" # Automatically finds available port
          REDIS_PREFIX: "{{worktree_name}}:"
      name: "Configure development environment"

  pre_delete:
    # Cleanup before worktree deletion
    - type: command
      config:
        command: "docker-compose down -v"
      condition: "file_exists:docker-compose.yml"
      on_error: "warn"
      name: "Stop Docker services"
```

### Advanced Hook Features

#### Variable Substitution

All hooks support `{{variable}}` substitution:

```yaml
hooks:
  post_create:
    - type: env_update
      config:
        file: ".env"
        updates:
          DB_NAME: "{{project}}_{{worktree_name}}_dev"
          WORKTREE_PATH: "{{worktree_path}}"
          ASSIGNED_COLOR: "{{color_hex}}"
```

**Available Variables:**

- `{{project}}` - Project name (derived from worktree parent directory)
- `{{worktree_name}}` - Worktree name
- `{{worktree_path}}` - Full worktree path
- `{{worktree}}` - Alias for `{{worktree_path}}`
- `{{branch}}` - Git branch name
- `{{color_hex}}` - Assigned color hex code (e.g., `#3F51B5`)
- `{{home}}` - User home directory
- `{{user}}` - Current username

#### Conditional Execution

Execute hooks only when conditions are met:

```yaml
hooks:
  post_create:
    - type: command
      config:
        command: "npm install"
      condition: "file_exists:package.json"

    - type: command
      config:
        command: "pip install -r requirements.txt"
      condition: "file_exists:requirements.txt"

    - type: command
      config:
        command: "brew services start postgresql"
      condition: "platform:darwin"
```

**Available Conditions:**

- `file_exists:filename` - File exists in worktree
- `file_not_exists:filename` - File doesn't exist
- `dir_exists:dirname` - Directory exists
- `env:VAR_NAME` - Environment variable is set
- `env:VAR_NAME=value` - Environment variable equals value
- `platform:darwin` - Platform check (darwin/linux/win32)
- `command_success:command` - Command executes successfully

**Negation:** Prefix any condition with `!` to negate it (e.g., `!file_exists:package.json` runs the hook only when `package.json` does not exist).

#### Error Handling

Control what happens when hooks fail:

```yaml
hooks:
  post_create:
    - type: command
      config:
        command: "critical-setup"
      on_error: "fail" # Stop execution on failure

    - type: command
      config:
        command: "optional-setup"
      on_error: "warn" # Continue with warning (default)

    - type: command
      config:
        command: "nice-to-have"
      on_error: "ignore" # Continue silently
```

### Common Hook Examples

#### Node.js Project Setup

```yaml
# .portal.yml file
hooks:
  post_create:
    - type: copy
      config:
        from: ".env.example"
        to: ".env"

    - type: command
      config:
        command: "npm install"
      condition: "file_exists:package.json"

    - type: env_update
      config:
        file: ".env"
        updates:
          NODE_ENV: "development"
          PORT: "auto"
```

#### Rust Project Setup

```yaml
# .portal.yml file
hooks:
  post_create:
    - type: env_update
      config:
        file: ".env"
        updates:
          RUST_LOG: "{{project}}={{worktree_name}}=debug"
          DATABASE_URL: "postgres://localhost/{{project}}_{{worktree_name}}"
      name: "Configure Rust environment"

    - type: command
      config:
        command: "cargo build --workspace"
      condition: "file_exists:Cargo.toml"
      name: "Build workspace"

    - type: command
      config:
        command: "cargo sqlx database setup"
      condition: "file_exists:migrations"
      on_error: "warn"
      name: "Setup database"
```

#### Full-Stack Development

```yaml
# .portal.yml file
hooks:
  post_create:
    # Setup environment files
    - type: template
      config:
        template: "docker-compose.template.yml"
        output: "docker-compose.yml"
        variables:
          db_port: "{{color_hex}}"

    # Start services
    - type: command
      config:
        command: "docker-compose up -d postgres redis"

    # Install dependencies
    - type: command
      config:
        command: "npm run setup:dev"

  pre_delete:
    # Cleanup services
    - type: command
      config:
        command: "docker-compose down -v"
      on_error: "warn"
```

### Security Features

Portal's hook system includes built-in security protections:

- **Command injection prevention**: Dangerous shell operators blocked by default
- **Path traversal protection**: Prevents `../` attacks in file operations
- **Input validation**: All hook configurations validated
- **Secure defaults**: Safe error handling and timeout protection

#### Shell Command Security

```yaml
# ❌ This is blocked for security:
- type: command
  config:
    command: "echo test; rm -rf /"

# ✅ Explicit bypass when needed:
- type: command
  config:
    command: "echo test && echo done"
    allow_shell: true # Explicitly allow shell operators
```

### Project-Level Configuration

Use `.portal.yml` file in your project root for project-specific configurations:

```bash
# Project structure
my-project/
├── .git/
├── .portal.yml      # Project configuration
└── src/
```

The `.portal.yml` file merges with and overrides global settings for project-specific automation.

## Tips & Tricks

### Productivity Aliases

After running `portal shell install`, you get:

- **`pw <worktree>`** - Quick switch to a worktree and cd into it

Additional aliases you can add to your shell config:

```bash
# Quick worktree commands
alias pnew="portal new"
alias plist="portal list"
alias pdel="portal delete"

# Open in editor
alias pcursor="portal cursor"
alias pvscode="portal vscode"
```

### Example Hook Scripts

**Auto-install dependencies:**

```yaml
hooks:
  post_create:
    - type: command
      name: "Install deps"
      config:
        command: |
          if [ -f package.json ]; then npm install
          elif [ -f requirements.txt ]; then pip install -r requirements.txt
          elif [ -f Gemfile ]; then bundle install
          fi
        allow_shell: true
```

**Rust project with workspace setup:**

```yaml
hooks:
  post_create:
    - type: command
      name: "Build workspace"
      config:
        command: "cargo build --workspace"
    - type: command
      name: "Setup git hooks"
      config:
        command: "cargo husky install"
```

## Troubleshooting

### Worktrees Created in Wrong Location

Use `portal config show` to verify where worktrees will be created:

```bash
# Check current configuration and resolved paths
portal config show

# Look for this section in the output:
#   Worktree Settings:
#     Base Directory Template: ../{project}_worktrees
#     Resolved Base Directory: ../myproject_worktrees
#     Actual Worktree Path: /Users/you/code/myproject_worktrees
```

If the path is incorrect, check your `.portal.yml` file:

```yaml
# Default: Creates worktrees as siblings to main repo (recommended)
base_dir: "../{project}_worktrees"

# This would create worktrees inside the main repo (not recommended)
base_dir: "{project}_worktrees"

# This would create worktrees two levels up from the main repo
base_dir: "../../{project}_worktrees"
```

The `base_dir` path is relative to your main repository. Using `../` creates a sibling folder, which keeps worktrees organized and separate from your main codebase.

### Configuration Not Loading

Verify which configuration files are being loaded:

```bash
portal config show
# Check the "Configuration Sources" section to see which files are found
```

### Debugging Configuration Issues

```bash
# View merged configuration as JSON for detailed inspection
portal config show --json | jq '.'

# Check if running from correct Git repository
git rev-parse --show-toplevel
```

## License

MIT License - see [LICENSE](LICENSE) file for details.

---

<p align="center">
  <a href="https://github.com/aureliensibiril/portal">⛩️ Portal</a> - Modern Git Worktree Manager
</p>
