diff --git a/config/aerospace/aerospace.toml b/config/aerospace/aerospace.toml
new file mode 100644
index 0000000..46faa3c
--- /dev/null
+++ b/config/aerospace/aerospace.toml
@@ -0,0 +1,93 @@
+# AeroSpace configuration for multi-project workspaces
+# See: https://nikitabobko.github.io/AeroSpace/guide
+
+start-at-login = true
+
+# Normalizations
+enable-normalization-flatten-containers = true
+enable-normalization-opposite-orientation-for-nested-containers = true
+
+# Mouse follows focus
+on-focused-monitor-changed = ['move-mouse monitor-lazy-center']
+
+# Run workspace launcher on workspace change
+exec-on-workspace-change = [
+ '/bin/bash', '-c',
+ '~/.config/workspaces/workspace.sh ensure "$AEROSPACE_FOCUSED_WORKSPACE" 2>/dev/null &'
+]
+
+[gaps]
+inner.horizontal = 8
+inner.vertical = 8
+outer.left = 8
+outer.right = 8
+outer.top = 8
+outer.bottom = 8
+
+[mode.main.binding]
+# ── Workspace switching ──────────────────────────────────────
+alt-1 = 'workspace 1'
+alt-2 = 'workspace 2'
+alt-3 = 'workspace 3'
+alt-4 = 'workspace 4'
+alt-5 = 'workspace 5'
+
+# Toggle between last two workspaces (feels like Cmd+Tab between projects)
+alt-tab = 'workspace-back-and-forth'
+
+# ── Move window to workspace ────────────────────────────────
+alt-shift-1 = 'move-node-to-workspace 1'
+alt-shift-2 = 'move-node-to-workspace 2'
+alt-shift-3 = 'move-node-to-workspace 3'
+alt-shift-4 = 'move-node-to-workspace 4'
+alt-shift-5 = 'move-node-to-workspace 5'
+
+# ── Focus movement (vim-style) ──────────────────────────────
+alt-h = 'focus left'
+alt-j = 'focus down'
+alt-k = 'focus up'
+alt-l = 'focus right'
+
+# Cycle windows within current workspace
+alt-grave = 'focus --boundaries-action wrap-around-the-workspace dfs-next'
+alt-shift-grave = 'focus --boundaries-action wrap-around-the-workspace dfs-prev'
+
+# ── Move windows ─────────────────────────────────────────────
+alt-shift-h = 'move left'
+alt-shift-j = 'move down'
+alt-shift-k = 'move up'
+alt-shift-l = 'move right'
+
+# ── Resize ───────────────────────────────────────────────────
+alt-minus = 'resize smart -50'
+alt-equal = 'resize smart +50'
+
+# ── Layout ───────────────────────────────────────────────────
+alt-slash = 'layout tiles horizontal vertical'
+alt-comma = 'layout accordion horizontal vertical'
+alt-f = 'fullscreen'
+alt-shift-f = 'layout floating tiling'
+
+# ── Service mode (for less common operations) ────────────────
+alt-shift-semicolon = 'mode service'
+
+[mode.service.binding]
+esc = ['reload-config', 'mode main']
+r = ['flatten-workspace-tree', 'mode main']
+alt-shift-h = ['join-with left', 'mode main']
+alt-shift-j = ['join-with down', 'mode main']
+alt-shift-k = ['join-with up', 'mode main']
+alt-shift-l = ['join-with right', 'mode main']
+
+# ── Window rules ─────────────────────────────────────────────
+[[on-window-detected]]
+if.app-id = 'com.apple.finder'
+run = 'layout floating'
+
+[[on-window-detected]]
+if.app-id = 'com.apple.systempreferences'
+run = 'layout floating'
+
+[[on-window-detected]]
+if.app-id = 'com.apple.calculator'
+run = 'layout floating'
diff --git a/config/raycast/scripts/list-workspaces.sh b/config/raycast/scripts/list-workspaces.sh
new file mode 100755
index 0000000..5670ccc
--- /dev/null
+++ b/config/raycast/scripts/list-workspaces.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# Required parameters:
+# @raycast.schemaVersion 1
+# @raycast.title List Workspaces
+# @raycast.mode fullOutput
+# @raycast.packageName Workspaces
+
+# Optional parameters:
+# @raycast.icon :desktopcomputer:
+
+# Documentation:
+# @raycast.description List all configured project workspaces
+# @raycast.author workspace-restoration
+# @raycast.authorURL https://github.com
+
+~/.config/workspaces/workspace.sh list
+echo ""
+~/.config/workspaces/workspace.sh status
diff --git a/config/raycast/scripts/switch-workspace.sh b/config/raycast/scripts/switch-workspace.sh
new file mode 100755
index 0000000..66779a9
--- /dev/null
+++ b/config/raycast/scripts/switch-workspace.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+# Required parameters:
+# @raycast.schemaVersion 1
+# @raycast.title Switch Workspace
+# @raycast.mode fullOutput
+# @raycast.packageName Workspaces
+
+# Optional parameters:
+# @raycast.icon :desktopcomputer:
+# @raycast.argument1 { "type": "text", "placeholder": "project name" }
+
+# Documentation:
+# @raycast.description Switch to a project workspace (AeroSpace + iTerm2 + browser)
+# @raycast.author workspace-restoration
+# @raycast.authorURL https://github.com
+
+~/.config/workspaces/workspace.sh open "$1"
diff --git a/config/swiftbar/workspaces.5s.sh b/config/swiftbar/workspaces.5s.sh
new file mode 100755
index 0000000..ae593f4
--- /dev/null
+++ b/config/swiftbar/workspaces.5s.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+
+# SwiftBar plugin: workspace switcher menu bar item
+# Refreshes every 5 seconds (per filename convention)
+# Install: brew install --cask swiftbar
+# Place in SwiftBar plugin directory
+
+# true
+
+PROJECTS_DIR="${HOME}/.config/workspaces/projects"
+WORKSPACE_BIN="${HOME}/bin/workspace"
+EDIT_NOTE_SCRIPT="${HOME}/.config/workspaces/lib/edit-note.sh"
+
+# Get current AeroSpace workspace
+CURRENT_WS=$(aerospace list-workspaces --focused 2>/dev/null || echo "?")
+
+# Find which project maps to the current workspace
+CURRENT_PROJECT=""
+for f in "${PROJECTS_DIR}"/*.toml; do
+ [[ -f "$f" ]] || continue
+ name=$(basename "$f" .toml)
+ ws=$(grep "^workspace" "$f" | sed 's/.*= *//;s/"//g' | tr -d ' ')
+ if [[ "$ws" == "$CURRENT_WS" ]]; then
+ CURRENT_PROJECT="$name"
+ break
+ fi
+done
+
+# ── Menu bar title ───────────────────────────────────────────
+
+if [[ -n "$CURRENT_PROJECT" ]]; then
+ echo ":desktopcomputer: ${CURRENT_PROJECT} | symbolize=true sfsize=13"
+else
+ echo ":desktopcomputer: ws:${CURRENT_WS} | symbolize=true sfsize=13"
+fi
+
+echo "---"
+
+# ── Show note for current workspace (if any) ────────────────
+
+if [[ -n "$CURRENT_PROJECT" ]]; then
+ note_text=""
+ in_notes=false
+ while IFS= read -r line; do
+ if [[ "$line" =~ ^\[notes\] ]]; then
+ in_notes=true
+ continue
+ fi
+ if $in_notes; then
+ [[ "$line" =~ ^\[.*\] ]] && break
+ if [[ "$line" =~ ^text[[:space:]]*= ]]; then
+ note_text=$(echo "$line" | sed 's/^[^=]*=[[:space:]]*//;s/^"//;s/"$//')
+ break
+ fi
+ fi
+ done < "${PROJECTS_DIR}/${CURRENT_PROJECT}.toml"
+
+ if [[ -n "$note_text" ]]; then
+ echo ":note.text: ${note_text} | symbolize=true sfsize=11 color=systemYellow bash=${EDIT_NOTE_SCRIPT} param1=${CURRENT_PROJECT} terminal=false refresh=true"
+ else
+ echo ":note.text: Add note... | symbolize=true sfsize=11 color=systemSecondaryLabel bash=${EDIT_NOTE_SCRIPT} param1=${CURRENT_PROJECT} terminal=false refresh=true"
+ fi
+ echo "---"
+fi
+
+# ── Project list ─────────────────────────────────────────────
+
+for f in "${PROJECTS_DIR}"/*.toml; do
+ [[ -f "$f" ]] || continue
+ name=$(basename "$f" .toml)
+ ws=$(grep "^workspace" "$f" | sed 's/.*= *//;s/"//g' | tr -d ' ')
+
+ if [[ "$name" == "$CURRENT_PROJECT" ]]; then
+ echo ":checkmark.circle.fill: ${name} (workspace ${ws}) | symbolize=true color=systemGreen sfsize=13"
+ else
+ echo ":circle: ${name} (workspace ${ws}) | symbolize=true sfsize=13 bash=${WORKSPACE_BIN} param1=open param2=${name} terminal=false refresh=true"
+ fi
+done
+
+echo "---"
+echo "Refresh | refresh=true"
diff --git a/config/workspaces/lib/edit-note.sh b/config/workspaces/lib/edit-note.sh
new file mode 100755
index 0000000..c414473
--- /dev/null
+++ b/config/workspaces/lib/edit-note.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+# Edit a workspace note via a macOS input dialog
+# Called by SwiftBar menu bar plugin
+# Usage: edit-note.sh
+
+set -euo pipefail
+
+PROJECT="${1:?project name required}"
+WORKSPACE_BIN="${HOME}/bin/workspace"
+
+# Get current note text
+current_note=$("$WORKSPACE_BIN" note "$PROJECT" 2>/dev/null | sed "s/^${PROJECT}: //" | sed 's/(no note set)//')
+
+# Show macOS input dialog
+new_note=$(osascript <
+-- osascript iterm.scpt save
+-- osascript iterm.scpt close
+
+on run argv
+ if (count of argv) < 2 then
+ log "Usage: osascript iterm.scpt "
+ return
+ end if
+
+ set cmd to item 1 of argv
+ set arrangementName to item 2 of argv
+
+ if cmd is "restore" then
+ restoreArrangement(arrangementName)
+ else if cmd is "save" then
+ saveArrangement(arrangementName)
+ else if cmd is "close" then
+ closeArrangement(arrangementName)
+ else
+ log "Unknown command: " & cmd
+ end if
+end run
+
+on restoreArrangement(arrangementName)
+ tell application "iTerm2"
+ activate
+ try
+ -- Check if arrangement exists before restoring
+ set arrangementNames to name of every window arrangement
+ if arrangementName is in arrangementNames then
+ restore window arrangement arrangementName
+ else
+ -- Arrangement not saved yet — just open a new window
+ create window with default profile
+ end if
+ on error errMsg
+ log "Error restoring arrangement: " & errMsg
+ -- Fallback: just activate iTerm2
+ activate
+ end try
+ end tell
+end restoreArrangement
+
+on saveArrangement(arrangementName)
+ tell application "iTerm2"
+ try
+ save window arrangement arrangementName
+ on error errMsg
+ log "Error saving arrangement: " & errMsg
+ end try
+ end tell
+end saveArrangement
+
+on closeArrangement(arrangementName)
+ tell application "iTerm2"
+ try
+ -- Close all windows that belong to this arrangement
+ -- iTerm2 doesn't track which windows came from which arrangement,
+ -- so we close windows whose tabs match the arrangement's profile pattern
+ set windowCount to count of windows
+ repeat with i from windowCount to 1 by -1
+ set w to window i
+ try
+ set tabCount to count of tabs of w
+ set firstTab to tab 1 of w
+ set sessionName to name of current session of firstTab
+ if sessionName contains arrangementName then
+ close w
+ end if
+ end try
+ end repeat
+ on error errMsg
+ log "Error closing arrangement: " & errMsg
+ end try
+ end tell
+end closeArrangement
diff --git a/config/workspaces/projects/api.toml b/config/workspaces/projects/api.toml
new file mode 100644
index 0000000..b9d95bf
--- /dev/null
+++ b/config/workspaces/projects/api.toml
@@ -0,0 +1,15 @@
+[project]
+name = "api"
+workspace = 2
+directory = "~/code/api"
+
+[terminal]
+arrangement = "api"
+
+[browser]
+url = "http://localhost:8000/docs"
+
+[notes]
+# Editable notes displayed when switching to this workspace.
+# Edit this file or run: workspace note api "your note here"
+text = ""
diff --git a/config/workspaces/projects/frontend.toml b/config/workspaces/projects/frontend.toml
new file mode 100644
index 0000000..75ecdcd
--- /dev/null
+++ b/config/workspaces/projects/frontend.toml
@@ -0,0 +1,15 @@
+[project]
+name = "frontend"
+workspace = 1
+directory = "~/code/frontend"
+
+[terminal]
+arrangement = "frontend"
+
+[browser]
+url = "http://localhost:3000"
+
+[notes]
+# Editable notes displayed when switching to this workspace.
+# Edit this file or run: workspace note frontend "your note here"
+text = ""
diff --git a/config/workspaces/workspace.sh b/config/workspaces/workspace.sh
new file mode 100755
index 0000000..28eba76
--- /dev/null
+++ b/config/workspaces/workspace.sh
@@ -0,0 +1,360 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+WORKSPACES_DIR="${HOME}/.config/workspaces"
+PROJECTS_DIR="${WORKSPACES_DIR}/projects"
+STATE_DIR="${WORKSPACES_DIR}/.state"
+
+# ── Helpers ──────────────────────────────────────────────────
+
+usage() {
+ cat <<'USAGE'
+Usage: workspace [options]
+
+Commands:
+ open Switch to project workspace and launch apps
+ close Tear down project workspace (kill servers, close windows)
+ list List configured projects
+ save Save current iTerm2 window arrangement for project
+ status Show which workspace/project is active
+ note [text] View or set a note for a project workspace
+ notes Show notes for all projects
+ ensure Ensure workspace has its project initialized (used by AeroSpace callback)
+
+Options:
+ --no-switch Skip AeroSpace workspace switch (used when called from AeroSpace binding)
+
+Examples:
+ workspace open frontend
+ workspace close api
+ workspace list
+ workspace status
+ workspace note frontend "TODO: fix auth bug before deploy"
+ workspace note frontend # view current note
+ workspace note frontend "" # clear the note
+ workspace notes # show all project notes
+USAGE
+}
+
+# Parse TOML value under a specific section
+# Usage: toml_get
+# For top-level keys, use "" as section
+toml_get() {
+ local file="$1" section="$2" key="$3"
+ local in_section=false result=""
+
+ if [[ -z "$section" ]]; then
+ # Top-level key (before any section header)
+ while IFS= read -r line; do
+ # Stop at first section header
+ [[ "$line" =~ ^\[.*\] ]] && break
+ if [[ "$line" =~ ^${key}[[:space:]]*= ]]; then
+ result=$(echo "$line" | sed 's/^[^=]*=[[:space:]]*//;s/^"//;s/"$//')
+ break
+ fi
+ done < "$file"
+ else
+ while IFS= read -r line; do
+ if [[ "$line" =~ ^\[${section}\] ]]; then
+ in_section=true
+ continue
+ fi
+ if $in_section; then
+ # New section starts — stop
+ [[ "$line" =~ ^\[.*\] ]] && break
+ if [[ "$line" =~ ^${key}[[:space:]]*= ]]; then
+ result=$(echo "$line" | sed 's/^[^=]*=[[:space:]]*//;s/^"//;s/"$//')
+ break
+ fi
+ fi
+ done < "$file"
+ fi
+
+ echo "$result"
+}
+
+# Find project config by workspace number
+find_project_for_workspace() {
+ local ws="$1"
+ for f in "${PROJECTS_DIR}"/*.toml; do
+ [[ -f "$f" ]] || continue
+ local project_ws
+ project_ws=$(toml_get "$f" "project" "workspace")
+ if [[ "$project_ws" == "$ws" ]]; then
+ basename "$f" .toml
+ return 0
+ fi
+ done
+ return 1
+}
+
+# ── Commands ─────────────────────────────────────────────────
+
+cmd_open() {
+ local project="$1"
+ local no_switch="${2:-false}"
+ local config="${PROJECTS_DIR}/${project}.toml"
+
+ if [[ ! -f "$config" ]]; then
+ echo "Error: No config found for project '${project}'"
+ echo "Expected: ${config}"
+ echo ""
+ echo "Available projects:"
+ cmd_list
+ exit 1
+ fi
+
+ local ws dir url arrangement
+ ws=$(toml_get "$config" "project" "workspace")
+ dir=$(toml_get "$config" "project" "directory")
+ url=$(toml_get "$config" "browser" "url")
+ arrangement=$(toml_get "$config" "terminal" "arrangement")
+ dir="${dir/#\~/$HOME}"
+
+ echo "-> Switching to workspace ${ws} (${project})"
+
+ # 1. Switch AeroSpace workspace (unless called from AeroSpace itself)
+ if [[ "$no_switch" != "true" ]]; then
+ if command -v aerospace &>/dev/null; then
+ aerospace workspace "$ws" 2>/dev/null || true
+ else
+ echo " [skip] aerospace not found"
+ fi
+ fi
+
+ # 2. Restore iTerm2 arrangement (if configured)
+ if [[ -n "$arrangement" ]]; then
+ if command -v osascript &>/dev/null; then
+ osascript "${WORKSPACES_DIR}/lib/iterm.scpt" restore "$arrangement" 2>/dev/null || true
+ echo " [ok] iTerm2 arrangement '${arrangement}'"
+ else
+ echo " [skip] osascript not available"
+ fi
+ fi
+
+ # 3. Open browser preview (if configured)
+ if [[ -n "$url" ]]; then
+ if command -v open &>/dev/null; then
+ open "$url" 2>/dev/null || true
+ echo " [ok] Browser: ${url}"
+ else
+ echo " [skip] 'open' command not available"
+ fi
+ fi
+
+ # 4. Mark workspace as initialized
+ mkdir -p "$STATE_DIR"
+ echo "$project" > "${STATE_DIR}/ws-${ws}"
+
+ echo "=> Workspace '${project}' active on workspace ${ws}"
+
+ # 5. Show workspace note (if any)
+ local note
+ note=$(toml_get "$config" "notes" "text")
+ if [[ -n "$note" ]]; then
+ echo ""
+ echo " Note: ${note}"
+ fi
+}
+
+cmd_close() {
+ local project="$1"
+ local config="${PROJECTS_DIR}/${project}.toml"
+
+ if [[ ! -f "$config" ]]; then
+ echo "Error: No config found for project '${project}'"
+ exit 1
+ fi
+
+ local ws dir
+ ws=$(toml_get "$config" "project" "workspace")
+ dir=$(toml_get "$config" "project" "directory")
+ dir="${dir/#\~/$HOME}"
+
+ echo "-> Closing workspace ${ws} (${project})"
+
+ # Kill dev server processes running in project directory
+ if [[ -n "$dir" && -d "$dir" ]]; then
+ # Find processes with cwd in project directory (common dev server ports)
+ local pids
+ pids=$(lsof -ti tcp:3000,3001,4000,5000,5173,8000,8080 2>/dev/null || true)
+ if [[ -n "$pids" ]]; then
+ for pid in $pids; do
+ local proc_cwd
+ proc_cwd=$(lsof -p "$pid" -Fn 2>/dev/null | grep '^n.*'"$dir" || true)
+ if [[ -n "$proc_cwd" ]]; then
+ kill "$pid" 2>/dev/null && echo " [ok] Killed process ${pid}" || true
+ fi
+ done
+ fi
+ fi
+
+ # Close iTerm2 windows for this arrangement
+ if command -v osascript &>/dev/null; then
+ local arrangement
+ arrangement=$(toml_get "$config" "terminal" "arrangement")
+ if [[ -n "$arrangement" ]]; then
+ osascript "${WORKSPACES_DIR}/lib/iterm.scpt" close "$arrangement" 2>/dev/null || true
+ echo " [ok] Closed iTerm2 windows"
+ fi
+ fi
+
+ # Clear state
+ rm -f "${STATE_DIR}/ws-${ws}"
+
+ echo "=> Workspace '${project}' closed"
+}
+
+cmd_list() {
+ echo "Configured projects:"
+ for f in "${PROJECTS_DIR}"/*.toml; do
+ [[ -f "$f" ]] || continue
+ local name ws dir
+ name=$(basename "$f" .toml)
+ ws=$(toml_get "$f" "project" "workspace")
+ dir=$(toml_get "$f" "project" "directory")
+ echo " ${name} (workspace ${ws}) ${dir}"
+ done
+}
+
+cmd_save() {
+ local project="$1"
+ if command -v osascript &>/dev/null; then
+ osascript "${WORKSPACES_DIR}/lib/iterm.scpt" save "$project"
+ echo "Saved iTerm2 arrangement '${project}'"
+ else
+ echo "Error: osascript not available (macOS only)"
+ exit 1
+ fi
+}
+
+cmd_status() {
+ local current_ws
+ if command -v aerospace &>/dev/null; then
+ current_ws=$(aerospace list-workspaces --focused 2>/dev/null || echo "unknown")
+ else
+ current_ws="unknown"
+ fi
+ echo "Active workspace: ${current_ws}"
+
+ for f in "${PROJECTS_DIR}"/*.toml; do
+ [[ -f "$f" ]] || continue
+ local name ws
+ name=$(basename "$f" .toml)
+ ws=$(toml_get "$f" "project" "workspace")
+ if [[ "$ws" == "$current_ws" ]]; then
+ echo "Active project: ${name}"
+ return
+ fi
+ done
+ echo "Active project: (none mapped)"
+}
+
+cmd_ensure() {
+ local ws="$1"
+ local state_file="${STATE_DIR}/ws-${ws}"
+
+ # Already initialized this session — skip
+ if [[ -f "$state_file" ]]; then
+ return 0
+ fi
+
+ # Find which project owns this workspace
+ local project
+ if project=$(find_project_for_workspace "$ws"); then
+ cmd_open "$project" "true"
+ fi
+}
+
+cmd_note() {
+ local project="$1"
+ local config="${PROJECTS_DIR}/${project}.toml"
+
+ if [[ ! -f "$config" ]]; then
+ echo "Error: No config found for project '${project}'"
+ exit 1
+ fi
+
+ if [[ $# -lt 2 ]]; then
+ # View mode: show current note
+ local note
+ note=$(toml_get "$config" "notes" "text")
+ if [[ -n "$note" ]]; then
+ echo "${project}: ${note}"
+ else
+ echo "${project}: (no note set)"
+ fi
+ return
+ fi
+
+ # Set mode: update the note in the TOML file
+ local new_note="$2"
+
+ if grep -q '^\[notes\]' "$config"; then
+ # [notes] section exists — update text line
+ if grep -q '^text' "$config"; then
+ sed -i.bak "s|^text[[:space:]]*=.*|text = \"${new_note}\"|" "$config"
+ else
+ sed -i.bak "/^\[notes\]/a\\
+text = \"${new_note}\"" "$config"
+ fi
+ else
+ # No [notes] section — append it
+ printf '\n[notes]\ntext = "%s"\n' "$new_note" >> "$config"
+ fi
+ rm -f "${config}.bak"
+
+ if [[ -n "$new_note" ]]; then
+ echo "Note set for ${project}: ${new_note}"
+ else
+ echo "Note cleared for ${project}"
+ fi
+}
+
+cmd_notes() {
+ local found=false
+ for f in "${PROJECTS_DIR}"/*.toml; do
+ [[ -f "$f" ]] || continue
+ local name note
+ name=$(basename "$f" .toml)
+ note=$(toml_get "$f" "notes" "text")
+ if [[ -n "$note" ]]; then
+ echo " ${name}: ${note}"
+ found=true
+ fi
+ done
+ if ! $found; then
+ echo " (no notes set for any project)"
+ fi
+}
+
+# ── Main ─────────────────────────────────────────────────────
+
+NO_SWITCH=false
+ARGS=()
+for arg in "$@"; do
+ case "$arg" in
+ --no-switch) NO_SWITCH=true ;;
+ *) ARGS+=("$arg") ;;
+ esac
+done
+set -- "${ARGS[@]+"${ARGS[@]}"}"
+
+case "${1:-}" in
+ open) cmd_open "${2:?project name required}" "$NO_SWITCH" ;;
+ close) cmd_close "${2:?project name required}" ;;
+ list) cmd_list ;;
+ save) cmd_save "${2:?project name required}" ;;
+ status) cmd_status ;;
+ note)
+ if [[ -z "${3+x}" ]]; then
+ cmd_note "${2:?project name required}"
+ else
+ cmd_note "${2:?project name required}" "$3"
+ fi
+ ;;
+ notes) cmd_notes ;;
+ ensure) cmd_ensure "${2:?workspace number required}" ;;
+ -h|--help) usage ;;
+ *) usage ;;
+esac
diff --git a/install.sh b/install.sh
new file mode 100755
index 0000000..6af3586
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,131 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Workspace Restoration System — Installer
+# Symlinks config files from this repo into ~/.config and ~/bin
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+CONFIG_SRC="${SCRIPT_DIR}/config"
+
+GREEN='\033[0;32m'
+YELLOW='\033[0;33m'
+RED='\033[0;31m'
+NC='\033[0m'
+
+info() { echo -e "${GREEN}[ok]${NC} $1"; }
+warn() { echo -e "${YELLOW}[skip]${NC} $1"; }
+err() { echo -e "${RED}[error]${NC} $1"; }
+
+# Create a symlink, backing up existing files
+link_file() {
+ local src="$1" dst="$2"
+
+ if [[ -L "$dst" ]]; then
+ local existing
+ existing=$(readlink "$dst")
+ if [[ "$existing" == "$src" ]]; then
+ info "Already linked: ${dst}"
+ return
+ fi
+ rm "$dst"
+ elif [[ -e "$dst" ]]; then
+ mv "$dst" "${dst}.backup"
+ warn "Backed up existing: ${dst} -> ${dst}.backup"
+ fi
+
+ mkdir -p "$(dirname "$dst")"
+ ln -s "$src" "$dst"
+ info "Linked: ${dst} -> ${src}"
+}
+
+echo "=== Workspace Restoration System ==="
+echo ""
+
+# ── 1. AeroSpace config ─────────────────────────────────────
+
+echo "-- AeroSpace --"
+link_file "${CONFIG_SRC}/aerospace/aerospace.toml" "${HOME}/.config/aerospace/aerospace.toml"
+echo ""
+
+# ── 2. Workspace configs + scripts ──────────────────────────
+
+echo "-- Workspaces --"
+link_file "${CONFIG_SRC}/workspaces/workspace.sh" "${HOME}/.config/workspaces/workspace.sh"
+chmod +x "${HOME}/.config/workspaces/workspace.sh"
+link_file "${CONFIG_SRC}/workspaces/lib/iterm.scpt" "${HOME}/.config/workspaces/lib/iterm.scpt"
+link_file "${CONFIG_SRC}/workspaces/lib/edit-note.sh" "${HOME}/.config/workspaces/lib/edit-note.sh"
+chmod +x "${HOME}/.config/workspaces/lib/edit-note.sh"
+
+# Link project configs (don't overwrite user's existing ones)
+mkdir -p "${HOME}/.config/workspaces/projects"
+for f in "${CONFIG_SRC}/workspaces/projects"/*.toml; do
+ [[ -f "$f" ]] || continue
+ name=$(basename "$f")
+ dst="${HOME}/.config/workspaces/projects/${name}"
+ if [[ -e "$dst" && ! -L "$dst" ]]; then
+ warn "Keeping existing project config: ${dst}"
+ else
+ link_file "$f" "$dst"
+ fi
+done
+echo ""
+
+# ── 3. ~/bin/workspace symlink ───────────────────────────────
+
+echo "-- CLI --"
+mkdir -p "${HOME}/bin"
+link_file "${HOME}/.config/workspaces/workspace.sh" "${HOME}/bin/workspace"
+chmod +x "${HOME}/bin/workspace"
+
+# Check if ~/bin is in PATH
+if [[ ":${PATH}:" != *":${HOME}/bin:"* ]]; then
+ warn "~/bin is not in your PATH. Add this to your shell profile:"
+ echo " export PATH=\"\$HOME/bin:\$PATH\""
+fi
+echo ""
+
+# ── 4. SwiftBar plugin ──────────────────────────────────────
+
+echo "-- SwiftBar --"
+if command -v swiftbar &>/dev/null || [[ -d "/Applications/SwiftBar.app" ]]; then
+ # SwiftBar expects plugins in its configured directory
+ SWIFTBAR_DIR="${HOME}/.config/swiftbar"
+ mkdir -p "$SWIFTBAR_DIR"
+ link_file "${CONFIG_SRC}/swiftbar/workspaces.5s.sh" "${SWIFTBAR_DIR}/workspaces.5s.sh"
+ chmod +x "${SWIFTBAR_DIR}/workspaces.5s.sh"
+else
+ warn "SwiftBar not installed. Install with: brew install --cask swiftbar"
+fi
+echo ""
+
+# ── 5. Raycast scripts ──────────────────────────────────────
+
+echo "-- Raycast --"
+RAYCAST_DIR="${HOME}/.config/raycast/scripts"
+mkdir -p "$RAYCAST_DIR"
+for f in "${CONFIG_SRC}/raycast/scripts"/*.sh; do
+ [[ -f "$f" ]] || continue
+ name=$(basename "$f")
+ link_file "$f" "${RAYCAST_DIR}/${name}"
+ chmod +x "${RAYCAST_DIR}/${name}"
+done
+echo ""
+
+# ── 6. State directory ──────────────────────────────────────
+
+mkdir -p "${HOME}/.config/workspaces/.state"
+
+# ── Summary ──────────────────────────────────────────────────
+
+echo "=== Installation complete ==="
+echo ""
+echo "Next steps:"
+echo " 1. Install AeroSpace: brew install --cask nikitabobko/tap/aerospace"
+echo " 2. Edit project configs in: ~/.config/workspaces/projects/"
+echo " 3. Save iTerm2 arrangements: workspace save "
+echo " 4. Switch workspaces: workspace open "
+echo " 5. (Optional) Install SwiftBar: brew install --cask swiftbar"
+echo ""
+echo "Quick test:"
+echo " workspace list"
+echo " workspace status"
diff --git a/workspace-restoration-plan.md b/workspace-restoration-plan.md
new file mode 100644
index 0000000..b953f8a
--- /dev/null
+++ b/workspace-restoration-plan.md
@@ -0,0 +1,695 @@
+# macOS Workspace Restoration Plan
+
+A scriptable system for saving, restoring, and switching between multi-project developer workspaces on macOS — combining terminal sessions, browser previews, and tooling like Claude Code.
+
+---
+
+## Problem
+
+When working on multiple projects in parallel, each project needs:
+- Multiple terminal tabs/panes (Claude Code, dev server, logs, tests)
+- A browser window showing a live preview (`localhost:3000`, etc.)
+- Possibly other app windows (editor, database GUI, docs)
+
+Switching between projects means manually rearranging all of this. The goal is **one keystroke** to switch the entire screen to a different project's workspace.
+
+---
+
+## Recommended Architecture
+
+```
+┌─────────────────────────────────────────────────────┐
+│ Keyboard Shortcut │
+│ (alt-1, alt-2, alt-3, ...) │
+└────────────────────────┬────────────────────────────┘
+ │
+ ┌──────────▼──────────┐
+ │ AeroSpace │ ← virtual workspace manager
+ │ (tiling WM, TOML) │ no SIP required
+ └──────────┬──────────┘
+ │
+ ┌──────────────┼──────────────┐
+ │ │ │
+ ┌───────▼──────┐ ┌────▼─────┐ ┌──────▼──────┐
+ │ iTerm2 │ │ Browser │ │ Other Apps │
+ │ (profiles, │ │ (Chrome/ │ │ (editors, │
+ │ arrangements│ │ Safari) │ │ DBs, etc) │
+ │ per project)│ │ │ │ │
+ └──────────────┘ └──────────┘ └─────────────┘
+ │
+ ┌───────▼──────────────────────┐
+ │ Project launcher script │ ← `workspace open project-a`
+ │ (shell script per project) │
+ └──────────────────────────────┘
+```
+
+### Why This Stack
+
+| Component | Role | Why this one |
+|-----------|------|-------------|
+| **AeroSpace** | Virtual workspace switching | No SIP required, instant switching (no animation), TOML config, CLI-scriptable, works on managed machines |
+| **iTerm2** | Terminal sessions | Rich AppleScript/Python API, saved window arrangements, per-project profiles with startup commands |
+| **Shell scripts** | Project launchers | Portable, versionable, no dependencies beyond the tools above |
+| **Raycast** (optional) | Command palette trigger | Invoke project scripts by name from anywhere |
+
+### Why Not the Alternatives
+
+- **Native macOS Spaces**: No public API. Cannot programmatically create/switch spaces. Animation delay. Spaces reorder themselves.
+- **Yabai**: Requires disabling SIP on macOS 15.2+ for space manipulation. Security and corporate policy concerns.
+- **Hammerspoon alone**: Spaces module is experimental and flaky. Better as a complement than a foundation.
+
+---
+
+## Implementation Plan
+
+### Phase 1: AeroSpace Setup
+
+Install and configure AeroSpace as the workspace manager.
+
+```bash
+brew install --cask nikitabobko/tap/aerospace
+```
+
+Create `~/.config/aerospace/aerospace.toml`:
+
+```toml
+# AeroSpace configuration for multi-project workspaces
+
+start-at-login = true
+
+# Disable macOS Spaces animations (they interfere)
+[gaps]
+inner.horizontal = 8
+inner.vertical = 8
+outer.left = 8
+outer.right = 8
+outer.top = 8
+outer.bottom = 8
+
+# Workspace keybindings
+[mode.main.binding]
+# Project workspaces (one per project)
+alt-1 = 'workspace 1'
+alt-2 = 'workspace 2'
+alt-3 = 'workspace 3'
+alt-4 = 'workspace 4'
+alt-5 = 'workspace 5'
+
+# Move focused window to a workspace
+alt-shift-1 = 'move-node-to-workspace 1'
+alt-shift-2 = 'move-node-to-workspace 2'
+alt-shift-3 = 'move-node-to-workspace 3'
+alt-shift-4 = 'move-node-to-workspace 4'
+alt-shift-5 = 'move-node-to-workspace 5'
+
+# Tiling layout controls
+alt-slash = 'layout tiles horizontal vertical'
+alt-comma = 'layout accordion horizontal vertical'
+alt-f = 'fullscreen'
+alt-shift-f = 'layout floating tiling'
+
+# Focus movement
+alt-h = 'focus left'
+alt-j = 'focus down'
+alt-k = 'focus up'
+alt-l = 'focus right'
+
+# Resize
+alt-shift-h = 'resize width -50'
+alt-shift-l = 'resize width +50'
+alt-shift-j = 'resize height +50'
+alt-shift-k = 'resize height -50'
+
+# Auto-assign apps to workspaces (optional defaults)
+[[on-window-detected]]
+if.app-id = 'com.apple.finder'
+run = 'layout floating'
+```
+
+**Key design choice**: Each numbered workspace (1-5) maps to a project. `alt-1` through `alt-5` switch instantly between them. No animation, no SIP, no macOS Spaces involved.
+
+### Phase 2: iTerm2 Project Profiles
+
+Create an iTerm2 profile for each project. Each profile sets:
+- **Working directory** to the project root
+- **Badge text** to the project name (visual identifier)
+- **Tab color** unique per project (instant visual feedback)
+- **Initial command** (optional, e.g., `claude` or a startup script)
+
+Profiles can be created in iTerm2 Settings > Profiles, or via the Python API.
+
+Example profiles:
+| Profile Name | Working Dir | Badge | Tab Color | Startup Command |
+|-------------|-------------|-------|-----------|-----------------|
+| `project-a` | `~/code/project-a` | `PROJECT-A` | Blue | `claude` |
+| `project-a-server` | `~/code/project-a` | `PROJECT-A:dev` | Blue | `npm run dev` |
+| `project-b` | `~/code/project-b` | `PROJECT-B` | Green | `claude` |
+| `project-b-server` | `~/code/project-b` | `PROJECT-B:dev` | `yarn dev` | Green |
+
+Then save iTerm2 Window Arrangements per project:
+1. Open tabs/panes with the right profiles
+2. **Window > Save Window Arrangement** as `project-a`, `project-b`, etc.
+
+### Phase 3: Project Launcher Scripts
+
+Create a `workspace` CLI tool. This is a shell script that orchestrates everything.
+
+#### Directory structure
+
+```
+~/.config/workspaces/
+├── workspace.sh # Main CLI script (symlinked to ~/bin/workspace)
+├── projects/
+│ ├── project-a.toml # Project config
+│ ├── project-b.toml # Project config
+│ └── project-c.toml # Project config
+└── lib/
+ └── iterm.scpt # Shared AppleScript helpers
+```
+
+#### Project config format (`projects/project-a.toml`)
+
+```toml
+[project]
+name = "project-a"
+workspace = 1 # AeroSpace workspace number
+directory = "~/code/project-a"
+
+[terminal]
+arrangement = "project-a" # iTerm2 saved arrangement name
+# OR define tabs inline:
+tabs = [
+ { profile = "project-a", command = "claude" },
+ { profile = "project-a-server", command = "npm run dev" },
+ { profile = "project-a", command = "npm run test -- --watch" },
+]
+
+[browser]
+url = "http://localhost:3000"
+app_mode = true # Open as standalone window (no browser chrome)
+
+[extras]
+# Additional apps to open/focus
+apps = ["Visual Studio Code"]
+```
+
+#### Main launcher script (`workspace.sh`)
+
+```bash
+#!/usr/bin/env bash
+set -euo pipefail
+
+WORKSPACES_DIR="${HOME}/.config/workspaces"
+PROJECTS_DIR="${WORKSPACES_DIR}/projects"
+
+usage() {
+ echo "Usage: workspace [project]"
+ echo ""
+ echo "Commands:"
+ echo " open Switch to project workspace and launch apps"
+ echo " list List configured projects"
+ echo " save Save current iTerm2 arrangement for project"
+ echo " status Show which workspace is active"
+ echo ""
+ echo "Examples:"
+ echo " workspace open project-a"
+ echo " workspace list"
+}
+
+# Parse TOML value (basic — for simple key = "value" pairs)
+toml_get() {
+ local file="$1" key="$2"
+ grep "^${key}" "$file" | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' '
+}
+
+cmd_open() {
+ local project="$1"
+ local config="${PROJECTS_DIR}/${project}.toml"
+
+ if [[ ! -f "$config" ]]; then
+ echo "Error: No config found for project '${project}'"
+ echo "Expected: ${config}"
+ exit 1
+ fi
+
+ local ws=$(toml_get "$config" "workspace")
+ local dir=$(toml_get "$config" "directory")
+ local url=$(toml_get "$config" "url")
+ local arrangement=$(toml_get "$config" "arrangement")
+ dir="${dir/#\~/$HOME}"
+
+ echo "→ Switching to workspace ${ws} (${project})"
+
+ # 1. Switch AeroSpace workspace
+ aerospace workspace "$ws" 2>/dev/null || true
+
+ # 2. Restore iTerm2 arrangement (if configured)
+ if [[ -n "$arrangement" ]]; then
+ osascript </dev/null || echo "unknown")
+ echo "Active workspace: ${current_ws}"
+
+ for f in "${PROJECTS_DIR}"/*.toml; do
+ [[ -f "$f" ]] || continue
+ local name=$(basename "$f" .toml)
+ local ws=$(toml_get "$f" "workspace")
+ if [[ "$ws" == "$current_ws" ]]; then
+ echo "Active project: ${name}"
+ return
+ fi
+ done
+ echo "Active project: (none mapped)"
+}
+
+# Main
+case "${1:-}" in
+ open) cmd_open "${2:?project name required}" ;;
+ list) cmd_list ;;
+ save) cmd_save "${2:?project name required}" ;;
+ status) cmd_status ;;
+ *) usage ;;
+esac
+```
+
+### Phase 4: Keyboard Shortcut Integration
+
+Bind workspace switching to keyboard shortcuts that also trigger the launcher. Two approaches:
+
+#### Option A: AeroSpace callbacks (simpler)
+
+In `aerospace.toml`, combine workspace switch with a script:
+
+```toml
+[mode.main.binding]
+# alt-1 switches workspace AND ensures project-a is initialized
+alt-1 = ['workspace 1', 'exec-and-forget ~/.config/workspaces/workspace.sh open project-a --no-switch']
+```
+
+(Add a `--no-switch` flag to skip the `aerospace workspace` call when triggered from AeroSpace itself.)
+
+#### Option B: Raycast script commands
+
+Create Raycast script commands in `~/.config/raycast/scripts/`:
+
+```bash
+#!/bin/bash
+# Required parameters:
+# @raycast.schemaVersion 1
+# @raycast.title Project A
+# @raycast.mode silent
+# @raycast.packageName Workspaces
+
+~/.config/workspaces/workspace.sh open project-a
+```
+
+This lets you type "Project A" in Raycast to switch workspaces.
+
+### Cmd+Tab and Workspace Switching
+
+macOS Cmd+Tab shows apps from **all** AeroSpace workspaces (since AeroSpace uses a single macOS Space, not native Spaces). No Cmd+Tab replacement (AltTab, Contexts, Witch) currently supports AeroSpace workspace filtering.
+
+#### Recommended: Alt+Tab bindings in AeroSpace
+
+Add these to your `aerospace.toml`:
+
+```toml
+[mode.main.binding]
+# Toggle between last two workspaces (feels like Cmd+Tab between projects)
+alt-tab = 'workspace-back-and-forth'
+
+# Cycle windows within the current workspace
+alt-grave = 'focus --boundaries-action wrap-around-the-workspace dfs-next'
+alt-shift-grave = 'focus --boundaries-action wrap-around-the-workspace dfs-prev'
+```
+
+This gives you two levels of switching:
+- **`alt-tab`**: Flip between your two most recent project workspaces
+- **`alt-backtick`**: Cycle through windows within the current workspace
+
+#### If you want literal Cmd+Tab
+
+macOS intercepts Cmd+Tab at the system level before AeroSpace sees it. To reclaim it, use [Karabiner-Elements](https://karabiner-elements.pqrs.org/) to remap Cmd+Tab to a key AeroSpace can handle:
+
+```json
+{
+ "description": "Remap cmd+tab to alt+tab for AeroSpace",
+ "manipulators": [{
+ "type": "basic",
+ "from": { "key_code": "tab", "modifiers": { "mandatory": ["command"], "optional": ["shift"] } },
+ "to": [{ "key_code": "tab", "modifiers": ["option"] }]
+ }]
+}
+```
+
+Then AeroSpace's `alt-tab = 'workspace-back-and-forth'` binding handles Cmd+Tab presses.
+
+> **Note**: This disables the native macOS app switcher. Most AeroSpace users find `alt-tab` sufficient and skip Karabiner.
+
+### Phase 5: Git Worktrees for Parallel Claude Work
+
+When working on multiple branches of the same repo simultaneously:
+
+```bash
+# Create isolated worktrees for each branch/feature
+cd ~/code/my-repo
+git worktree add ../my-repo-feature-x feature-x
+git worktree add ../my-repo-bugfix-y bugfix-y
+
+# Each worktree gets its own workspace
+# project config points directory to the worktree path
+```
+
+This pairs with the workspace system — each worktree becomes a separate "project" with its own terminal layout, Claude Code session, and browser preview.
+
+---
+
+## Example: Full Setup for Two Projects
+
+### Project A: React frontend
+
+```
+Workspace 1 (alt-1)
+┌──────────────────────┬──────────────────────┐
+│ │ │
+│ iTerm2: claude │ Chrome: localhost │
+│ (Claude Code) │ :3000 │
+│ │ │
+│ │ │
+├──────────────────────┤ │
+│ │ │
+│ iTerm2: npm run dev │ │
+│ (dev server) │ │
+│ │ │
+└──────────────────────┴──────────────────────┘
+```
+
+Config: `~/.config/workspaces/projects/frontend.toml`
+```toml
+[project]
+name = "frontend"
+workspace = 1
+directory = "~/code/frontend"
+
+[terminal]
+arrangement = "frontend"
+
+[browser]
+url = "http://localhost:3000"
+```
+
+### Project B: Python API
+
+```
+Workspace 2 (alt-2)
+┌──────────────────────┬──────────────────────┐
+│ │ │
+│ iTerm2: claude │ Chrome: localhost │
+│ (Claude Code) │ :8000/docs │
+│ │ (API docs / Swagger)│
+│ │ │
+├──────────────────────┤ │
+│ │ │
+│ iTerm2: uvicorn │ │
+│ (API server) │ │
+│ │ │
+└──────────────────────┴──────────────────────┘
+```
+
+Config: `~/.config/workspaces/projects/api.toml`
+```toml
+[project]
+name = "api"
+workspace = 2
+directory = "~/code/api"
+
+[terminal]
+arrangement = "api"
+
+[browser]
+url = "http://localhost:8000/docs"
+```
+
+### Switching between them
+
+```
+alt-1 → instant switch to frontend (terminal + browser + layout)
+alt-2 → instant switch to API (terminal + browser + layout)
+```
+
+---
+
+## Phase 6: Menu Bar UI for Workspace Switching
+
+A persistent, clickable UI in the macOS menu bar showing the active workspace and a dropdown to switch between projects.
+
+### Approach Comparison
+
+| Approach | Setup | Persistent Menu Bar | Search/Filter | Language | Extra App |
+|----------|-------|-------------------|---------------|----------|-----------|
+| **SwiftBar plugin** | Low | Yes | No | Bash | SwiftBar |
+| **Raycast extension** | Medium | No (invoke to use) | Yes | TypeScript | Raycast |
+| **Alfred workflow** | Medium | No (invoke to use) | Yes (fuzzy) | Bash | Alfred |
+| **Hammerspoon menubar** | Low-Med | Yes | No | Lua | Hammerspoon |
+| **rumps (Python)** | Low-Med | Yes | No | Python | None |
+| **SwiftUI MenuBarExtra** | High | Yes | Possible | Swift | None |
+
+### Recommended: SwiftBar Plugin (lowest friction)
+
+Install SwiftBar:
+
+```bash
+brew install --cask swiftbar
+```
+
+Create `~/.config/swiftbar/workspaces.5s.sh` (refreshes every 5 seconds):
+
+```bash
+#!/bin/bash
+
+# true
+
+PROJECTS_DIR="${HOME}/.config/workspaces/projects"
+CURRENT_WS=$(aerospace list-workspaces --focused 2>/dev/null || echo "?")
+
+# Find which project maps to the current workspace
+CURRENT_PROJECT=""
+for f in "${PROJECTS_DIR}"/*.toml; do
+ [[ -f "$f" ]] || continue
+ name=$(basename "$f" .toml)
+ ws=$(grep "^workspace" "$f" | sed 's/.*= *//;s/"//g' | tr -d ' ')
+ if [[ "$ws" == "$CURRENT_WS" ]]; then
+ CURRENT_PROJECT="$name"
+ break
+ fi
+done
+
+# Menu bar title: show current project name
+if [[ -n "$CURRENT_PROJECT" ]]; then
+ echo ":desktopcomputer: ${CURRENT_PROJECT} | symbolize=true sfsize=13"
+else
+ echo ":desktopcomputer: ws:${CURRENT_WS} | symbolize=true sfsize=13"
+fi
+
+echo "---"
+
+# List all projects as menu items
+for f in "${PROJECTS_DIR}"/*.toml; do
+ [[ -f "$f" ]] || continue
+ name=$(basename "$f" .toml)
+ ws=$(grep "^workspace" "$f" | sed 's/.*= *//;s/"//g' | tr -d ' ')
+
+ if [[ "$name" == "$CURRENT_PROJECT" ]]; then
+ echo ":checkmark.circle.fill: ${name} (workspace ${ws}) | symbolize=true color=systemGreen sfsize=13"
+ else
+ echo ":circle: ${name} (workspace ${ws}) | symbolize=true sfsize=13 bash=${HOME}/bin/workspace param1=open param2=${name} terminal=false refresh=true"
+ fi
+done
+
+echo "---"
+echo "Refresh | refresh=true"
+```
+
+```bash
+chmod +x ~/.config/swiftbar/workspaces.5s.sh
+```
+
+This gives you:
+- A menu bar item showing the current project name
+- A dropdown listing all configured projects with a checkmark on the active one
+- Click any project to switch (runs `workspace open ` silently)
+- Auto-refreshes every 5 seconds to reflect keyboard-driven switches
+
+### Alternative: Raycast Extension (for search-driven switching)
+
+If you have many projects (6+), a searchable list is better than a dropdown. A Raycast extension provides:
+- Fuzzy search across all workspace names
+- Status indicators (green dot = active)
+- Secondary actions (Cmd+Enter to just switch terminal, Shift+Enter to open browser only)
+
+```typescript
+// src/switch-workspace.tsx — Raycast extension entry point
+import { List, ActionPanel, Action, Icon, Color, showToast, Toast } from "@raycast/api";
+import { execSync } from "child_process";
+import { readdirSync } from "fs";
+import { join, basename } from "path";
+
+export default function Command() {
+ const home = process.env.HOME!;
+ const projectsDir = join(home, ".config/workspaces/projects");
+ let currentWs = "";
+ try {
+ currentWs = execSync("aerospace list-workspaces --focused", { encoding: "utf-8" }).trim();
+ } catch {}
+
+ const projects = readdirSync(projectsDir)
+ .filter((f) => f.endsWith(".toml"))
+ .map((f) => {
+ const name = basename(f, ".toml");
+ const content = require("fs").readFileSync(join(projectsDir, f), "utf-8");
+ const wsMatch = content.match(/^workspace\s*=\s*(\S+)/m);
+ const ws = wsMatch ? wsMatch[1].replace(/"/g, "") : "?";
+ return { name, ws, isActive: ws === currentWs };
+ });
+
+ return (
+
+ {projects.map((p) => (
+
+ {
+ execSync(`${home}/bin/workspace open ${p.name}`);
+ showToast({ style: Toast.Style.Success, title: `Switched to ${p.name}` });
+ }}
+ />
+
+ }
+ />
+ ))}
+
+ );
+}
+```
+
+Scaffold with `npx create-raycast-extension`, paste this in, and `npm run dev`.
+
+### Both Together (recommended for power users)
+
+SwiftBar for at-a-glance status + click switching, Raycast for keyboard-driven fuzzy search when you have many projects. They complement each other — SwiftBar is always visible, Raycast is always one hotkey away.
+
+---
+
+## Installation Checklist
+
+1. **Install AeroSpace**: `brew install --cask nikitabobko/tap/aerospace`
+2. **Create AeroSpace config**: `~/.config/aerospace/aerospace.toml` (see Phase 1)
+3. **Set up iTerm2 profiles**: One profile per project role (claude, server, tests)
+4. **Save iTerm2 arrangements**: One arrangement per project
+5. **Create workspace configs**: `~/.config/workspaces/projects/.toml`
+6. **Install the launcher script**: Copy `workspace.sh` to `~/bin/workspace` and `chmod +x`
+7. **Bind shortcuts**: Wire AeroSpace keybindings to workspace launcher (Phase 4)
+8. **(Optional) Install SwiftBar**: `brew install --cask swiftbar` for menu bar UI
+9. **(Optional) Install Raycast**: For command-palette access to workspace switching
+
+---
+
+## Per-Workspace Notes
+
+Each project config supports a `[notes]` section with editable text that displays when you switch to a workspace.
+
+### Setting notes via CLI
+
+```bash
+# Set a note
+workspace note frontend "TODO: fix auth bug before deploy"
+
+# View a note
+workspace note frontend
+
+# Clear a note
+workspace note frontend ""
+
+# View all project notes
+workspace notes
+```
+
+### How it works
+
+Notes are stored directly in the project config TOML file under `[notes]`:
+
+```toml
+[notes]
+text = "TODO: fix auth bug before deploy"
+```
+
+When you run `workspace open frontend`, the note is printed:
+
+```
+-> Switching to workspace 1 (frontend)
+ [ok] iTerm2 arrangement 'frontend'
+ [ok] Browser: http://localhost:3000
+=> Workspace 'frontend' active on workspace 1
+
+ Note: TODO: fix auth bug before deploy
+```
+
+The SwiftBar menu bar plugin also shows the current workspace's note, so you always have context visible.
+
+---
+
+## Extensions and Future Ideas
+
+- **Auto-detect running projects**: Check which `localhost` ports are active and show status
+- **Teardown command**: `workspace close project-a` — kill dev servers, close browser tabs, free the workspace
+- **Monitor-aware layouts**: Different tiling configurations for laptop-only vs. external monitor
+- **Session persistence**: Save/restore Claude Code conversation context per workspace
+- **MCP integration**: Use an MCP server to let Claude Code itself trigger workspace switches
+- **Dotfiles integration**: Check all configs into a dotfiles repo for portability across machines