From 9fa50b0b19143b2f939f7d06bbbd94974292bb1a Mon Sep 17 00:00:00 2001 From: Guodong Zhu Date: Thu, 29 Jan 2026 12:04:13 -0500 Subject: [PATCH 01/11] Redesign install.sh onboarding to be repo-agnostic Replace hardcoded java-monorepo defaults with a user-centric flow that: - Asks which repository to manage first - Auto-detects default branch from origin/HEAD or main/master - Derives all paths from repo location (e.g., myrepo -> myrepo-master, myrepo-worktrees) - Shows derived config and allows customization --- install.sh | 190 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 175 insertions(+), 15 deletions(-) diff --git a/install.sh b/install.sh index aff88cd..ae1cb35 100755 --- a/install.sh +++ b/install.sh @@ -58,6 +58,72 @@ prompt_with_default() { echo "${result:-$default}" } +# Expand ~ to $HOME in a path +expand_path() { + local path="$1" + # Expand ~ at the beginning of the path (e.g., ~/foo -> /home/user/foo) + case "$path" in + "~") + echo "$HOME" + ;; + "~/"*) + echo "${HOME}${path#\~}" + ;; + *) + echo "$path" + ;; + esac +} + +# Detect the default branch for a repository +# Tries: origin/HEAD, then common branch names +detect_default_branch() { + local repo="$1" + + # Try to get from origin/HEAD + local default_branch + default_branch=$(git -C "$repo" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||') + + if [[ -n "$default_branch" ]]; then + echo "$default_branch" + return 0 + fi + + # Check for common default branch names + for branch in main master; do + if git -C "$repo" show-ref --verify --quiet "refs/heads/$branch" 2>/dev/null || \ + git -C "$repo" show-ref --verify --quiet "refs/remotes/origin/$branch" 2>/dev/null; then + echo "$branch" + return 0 + fi + done + + # Fallback to main + echo "main" +} + +# Derive all paths from the repository location +# Sets: WT_MAIN_REPO_ROOT, WT_WORKTREES_BASE, WT_ACTIVE_WORKTREE, WT_IDEA_FILES_BASE +derive_paths_from_repo() { + local repo="$1" + local repo_name repo_parent + + repo_name="$(basename "$repo")" + repo_parent="$(dirname "$repo")" + + # The current repo location becomes the symlink location + WT_ACTIVE_WORKTREE="$repo" + + # Main repo gets "-master" suffix + WT_MAIN_REPO_ROOT="${repo_parent}/${repo_name}-master" + + # Worktrees base gets "-worktrees" suffix + WT_WORKTREES_BASE="${repo_parent}/${repo_name}-worktrees" + + # IntelliJ metadata goes to a central location in ~/.config/wt/ + WT_IDEA_FILES_BASE="$HOME/.config/wt/idea-files/${repo_name}" +} + # Copy toolkit to installation directory install_toolkit() { echo "Installing worktree-toolkit to $INSTALL_DIR ..." @@ -96,7 +162,7 @@ configure_shell_rc() { } # Prompt user for configuration and write to wt-common -# Note: WT_* variables are already set with defaults from wt-common +# Uses a user-centric flow: start with which repo to manage, then derive paths configure_wt_common() { local wt_common="$INSTALL_DIR/lib/wt-common" @@ -105,26 +171,120 @@ configure_wt_common() { return 1 fi - echo "Configure your worktree environment. Press Enter to accept defaults." + # ───────────────────────────────────────────────────────────────────────────── + # Step 1: Ask which repository to manage + # ───────────────────────────────────────────────────────────────────────────── + echo "Which repository do you want to manage with worktrees?" + echo + echo "Enter the path to your existing git repository." + echo "Example: ~/Development/myrepo" echo - WT_MAIN_REPO_ROOT=$(prompt_with_default "Main repository root" "$WT_MAIN_REPO_ROOT") - WT_WORKTREES_BASE=$(prompt_with_default "Worktrees base directory" "$WT_WORKTREES_BASE") - WT_IDEA_FILES_BASE=$(prompt_with_default "IntelliJ metadata directory" "$WT_IDEA_FILES_BASE") - WT_ACTIVE_WORKTREE=$(prompt_with_default "Active worktree symlink" "$WT_ACTIVE_WORKTREE") - WT_BASE_BRANCH=$(prompt_with_default "Default base branch" "$WT_BASE_BRANCH") + local repo_path + while true; do + if ! read -rp "Repository path: " repo_path; then + echo + exit 1 + fi + + # Handle empty input + if [[ -z "$repo_path" ]]; then + echo "Please enter a repository path." + continue + fi + + # Expand ~ to $HOME + repo_path=$(expand_path "$repo_path") + + # Validate it exists + if [[ ! -d "$repo_path" ]]; then + error "Directory not found: $repo_path" + continue + fi + # Validate it's a git repository + if ! git -C "$repo_path" rev-parse --git-dir &>/dev/null; then + error "Not a git repository: $repo_path" + continue + fi + + break + done + + # Normalize to absolute path + repo_path="$(cd "$repo_path" && pwd)" + + # ───────────────────────────────────────────────────────────────────────────── + # Step 2: Auto-detect git info and show confirmation + # ───────────────────────────────────────────────────────────────────────────── + echo + echo "Detected repository:" + echo " Path: $repo_path" + + # Get remote origin URL if available + local remote_url + remote_url=$(git -C "$repo_path" remote get-url origin 2>/dev/null || echo "(no remote)") + echo " Remote: $remote_url" + + # Detect default branch + WT_BASE_BRANCH=$(detect_default_branch "$repo_path") + echo " Branch: $WT_BASE_BRANCH (default branch)" + + echo + + # ───────────────────────────────────────────────────────────────────────────── + # Step 3: Derive paths automatically + # ───────────────────────────────────────────────────────────────────────────── + derive_paths_from_repo "$repo_path" + + # ───────────────────────────────────────────────────────────────────────────── + # Step 4: Show derived configuration and allow edits + # ───────────────────────────────────────────────────────────────────────────── + echo "Derived configuration:" + echo + echo " The worktree toolkit will set up the following structure:" + echo + echo " ${BOLD}Active symlink:${NC} $WT_ACTIVE_WORKTREE" + echo " Your IDE opens this path. It's a symlink that can point to any worktree." echo - echo "Configuration:" - echo " WT_MAIN_REPO_ROOT: $WT_MAIN_REPO_ROOT" - echo " WT_WORKTREES_BASE: $WT_WORKTREES_BASE" - echo " WT_IDEA_FILES_BASE: $WT_IDEA_FILES_BASE" - echo " WT_ACTIVE_WORKTREE: $WT_ACTIVE_WORKTREE" - echo " WT_BASE_BRANCH: $WT_BASE_BRANCH" + echo " ${BOLD}Main repository:${NC} $WT_MAIN_REPO_ROOT" + echo " Your current repo will be moved here (the \"master\" worktree)." echo + echo " ${BOLD}Worktrees directory:${NC} $WT_WORKTREES_BASE" + echo " New worktrees will be created here." + echo + echo " ${BOLD}IntelliJ metadata:${NC} $WT_IDEA_FILES_BASE" + echo " Shared .ijwb files for instant project switching." + echo + echo " ${BOLD}Default branch:${NC} $WT_BASE_BRANCH" + echo " Used when creating new worktrees." + echo + + if prompt_confirm "Use this configuration? [Y/n]" "y"; then + : # Continue with derived values + else + echo + echo "You can customize each value. Press Enter to keep the default." + echo + + WT_ACTIVE_WORKTREE=$(prompt_with_default "Active symlink path" "$WT_ACTIVE_WORKTREE") + WT_MAIN_REPO_ROOT=$(prompt_with_default "Main repository path" "$WT_MAIN_REPO_ROOT") + WT_WORKTREES_BASE=$(prompt_with_default "Worktrees directory" "$WT_WORKTREES_BASE") + WT_IDEA_FILES_BASE=$(prompt_with_default "IntelliJ metadata directory" "$WT_IDEA_FILES_BASE") + WT_BASE_BRANCH=$(prompt_with_default "Default base branch" "$WT_BASE_BRANCH") + + echo + echo "Final configuration:" + echo " WT_ACTIVE_WORKTREE: $WT_ACTIVE_WORKTREE" + echo " WT_MAIN_REPO_ROOT: $WT_MAIN_REPO_ROOT" + echo " WT_WORKTREES_BASE: $WT_WORKTREES_BASE" + echo " WT_IDEA_FILES_BASE: $WT_IDEA_FILES_BASE" + echo " WT_BASE_BRANCH: $WT_BASE_BRANCH" + echo + fi # Write to wt-common using sed - echo "Saving to wt-common..." + echo "Saving configuration..." sed -i.bak \ -e "s|: \"\${WT_MAIN_REPO_ROOT:=.*}\"|: \"\${WT_MAIN_REPO_ROOT:=\"$WT_MAIN_REPO_ROOT\"}\"|" \ -e "s|: \"\${WT_WORKTREES_BASE:=.*}\"|: \"\${WT_WORKTREES_BASE:=\"$WT_WORKTREES_BASE\"}\"|" \ @@ -348,7 +508,7 @@ main() { echo echo "════════════════════════════════════════════════════════════════════════════════" - echo " Workspace Configuration" + echo " Repository Setup" echo "════════════════════════════════════════════════════════════════════════════════" echo From 71c267a8f275954dd60e29e212493d2f5de27c8f Mon Sep 17 00:00:00 2001 From: Guodong Zhu Date: Thu, 29 Jan 2026 13:52:16 -0500 Subject: [PATCH 02/11] Add generic project metadata detection and sync Expand beyond .ijwb to support multiple IDE/editor metadata patterns: - Add WT_METADATA_PATTERNS config variable (space-separated list) - Add WT_KNOWN_METADATA array with 16 known patterns: - JetBrains: .idea, .run, .fleet - Bazel: .ijwb, .aswb, .clwb, .bazelbsp, .bsp - Xcode/iOS: .swiftpm, xcuserdata - VS Code: .vscode - Scala: .metals, .bloop - Eclipse: .settings, .project, .classpath New scripts: - bin/wt-metadata-export: Export all configured patterns to vault - bin/wt-metadata-import: Import all configured patterns to worktree Install flow changes: - Scan repo for existing metadata directories - Interactive checkbox selection (all detected selected by default) - Save selection to WT_METADATA_PATTERNS Updated: - wt-add uses wt-metadata-import - wt.sh adds metadata-export/import commands (keeps ijwb-* as aliases) - All find commands use -L to follow symlinks Co-Authored-By: Claude Opus 4.5 --- bin/wt-add | 42 +++---- bin/wt-metadata-export | 213 +++++++++++++++++++++++++++++++++++ bin/wt-metadata-import | 249 +++++++++++++++++++++++++++++++++++++++++ install.sh | 225 +++++++++++++++++++++++++++++++++---- lib/wt-common | 33 ++++++ lib/wt-ijwb-refresh | 30 ++--- wt.sh | 17 +-- 7 files changed, 743 insertions(+), 66 deletions(-) create mode 100755 bin/wt-metadata-export create mode 100755 bin/wt-metadata-import diff --git a/bin/wt-add b/bin/wt-add index 7dac3f5..6443065 100755 --- a/bin/wt-add +++ b/bin/wt-add @@ -13,13 +13,13 @@ # 2. If NOT creating a new branch (i.e., no -b/--branch flag), supports: # a) wt-add [extra args...] # → Direct passthrough to `git worktree add ...`, -# then installs IntelliJ `.ijwb` metadata into the new worktree. +# then installs project metadata into the new worktree. # # b) wt-add # → Convenience mode: # path = $WT_WORKTREES_BASE/ # git worktree add "$path" "" -# install IntelliJ `.ijwb` metadata into "$path". +# install project metadata into "$path". # # 3. If creating a new branch (using -b/--branch): # a. If there are uncommitted changes in the base repo: @@ -28,7 +28,7 @@ # - Switch to that base branch. # c. Ensure the base branch is up to date by running `git pull`. # d. Run `git worktree add ` to create the new worktree. -# e. Run wt-ijwb-import to import IntelliJ metadata into the new worktree. +# e. Run wt-metadata-import to import project metadata into the new worktree. # f. Create symlinks for Bazel outputs (bazel-out, bazel-bin, etc.) pointing # to the same targets as in the main repo, to speed up IntelliJ sync. # g. After completion: @@ -48,18 +48,18 @@ # - `git pull` only happens when the script is truly on the base branch. # - Detached HEAD states are restored correctly. # - Worktree creation behaves like `git worktree add`, but with safety -# guardrails and automatic IntelliJ metadata installation. +# guardrails and automatic project metadata installation. # # Usage examples: # --------------- # wt-add -b feature/foo -# → Creates a worktree at $WT_WORKTREES_BASE/feature/foo and installs .ijwb +# → Creates a worktree at $WT_WORKTREES_BASE/feature/foo and installs metadata # # wt-add -b feature/foo /custom/path origin/master -# → Uses the explicit path and extra arguments as-is, then installs .ijwb +# → Uses the explicit path and extra arguments as-is, then installs metadata # # wt-add ../path/to/worktree existing-branch -# → No branch creation; simply runs git worktree add and installs .ijwb. +# → No branch creation; simply runs git worktree add and installs metadata. # set -euo pipefail @@ -136,28 +136,28 @@ while [[ $i -lt ${#ARGS[@]} ]]; do i=$((i + 1)) done -# Helper: install IntelliJ metadata for a created worktree -install_ijwb_for_worktree() { +# Helper: install project metadata for a created worktree +install_metadata_for_worktree() { local worktree_path="$1" # Normalize to absolute (worktree now exists) local worktree_path_abs worktree_path_abs="$(cd "$worktree_path" && pwd)" - echo "Installing .ijwb metadata into worktree: $worktree_path_abs" + echo "Installing project metadata into worktree: $worktree_path_abs" - # Find wt-ijwb-import: try script directory first, then PATH - local ijwb_import="" - if [[ -f "$SCRIPT_DIR/wt-ijwb-import" ]]; then - ijwb_import="$SCRIPT_DIR/wt-ijwb-import" - elif command -v wt-ijwb-import >/dev/null 2>&1; then - ijwb_import="wt-ijwb-import" + # Find wt-metadata-import: try script directory first, then PATH + local metadata_import="" + if [[ -f "$SCRIPT_DIR/wt-metadata-import" ]]; then + metadata_import="$SCRIPT_DIR/wt-metadata-import" + elif command -v wt-metadata-import >/dev/null 2>&1; then + metadata_import="wt-metadata-import" else - error "Cannot find wt-ijwb-import" + error "Cannot find wt-metadata-import" return 1 fi - "$ijwb_import" -y "$WT_IDEA_FILES_BASE" "$worktree_path_abs" + "$metadata_import" -y "$WT_IDEA_FILES_BASE" "$worktree_path_abs" } # Helper: install Bazel output symlinks (bazel-out, bazel-bin, etc.) for a created worktree @@ -257,7 +257,7 @@ if [[ $creating_new_branch -eq 0 ]]; then echo "Creating worktree for existing branch '$local_branch' at: $worktree_path" git worktree add -- "$worktree_path" "$local_branch" - install_ijwb_for_worktree "$worktree_path" + install_metadata_for_worktree "$worktree_path" install_bazel_symlinks_for_worktree "$worktree_path" success "git worktree add completed successfully." exit 0 @@ -279,7 +279,7 @@ if [[ $creating_new_branch -eq 0 ]]; then info "Not creating a new branch; running: git worktree add ${ARGS[*]}" git worktree add "${ARGS[@]}" - install_ijwb_for_worktree "$worktree_path" + install_metadata_for_worktree "$worktree_path" install_bazel_symlinks_for_worktree "$worktree_path" success "git worktree add completed successfully." exit 0 @@ -466,7 +466,7 @@ else git worktree add "${FINAL_ARGS[@]}" fi -install_ijwb_for_worktree "$worktree_path" +install_metadata_for_worktree "$worktree_path" install_bazel_symlinks_for_worktree "$worktree_path" success "git worktree add completed successfully." diff --git a/bin/wt-metadata-export b/bin/wt-metadata-export new file mode 100755 index 0000000..a72b79b --- /dev/null +++ b/bin/wt-metadata-export @@ -0,0 +1,213 @@ +#!/usr/bin/env bash +# +# wt-metadata-export — Export Project Metadata Directories to Vault +# =================================================================== +# +# This script scans a source directory for project metadata directories +# (as configured in WT_METADATA_PATTERNS) and recreates the same directory +# structure under a target directory using symbolic links. +# +# Supported metadata includes: +# - .ijwb, .aswb, .clwb (Bazel IDE plugins) +# - .idea (JetBrains IDEs) +# - .vscode (VS Code) +# - .swiftpm (Swift Package Manager) +# - And more (see WT_KNOWN_METADATA in wt-common) +# +# Behavior: +# --------- +# 1. Accepts a and . +# 2. For each pattern in WT_METADATA_PATTERNS: +# - Recursively finds all matching directories under the source +# - Computes the relative path from the source root +# - Recreates that directory structure under the target +# - Creates symbolic links to the source directories +# +# Usage: +# wt-metadata-export # Use defaults from env vars +# wt-metadata-export +# +# Defaults (when no arguments): +# source = $WT_MAIN_REPO_ROOT +# target = $WT_IDEA_FILES_BASE +# + +set -euo pipefail + +# Resolve script and lib directories +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LIB_DIR="$SCRIPT_DIR/../lib" + +# Bootstrap: source wt-common from lib/ +if [[ -f "$LIB_DIR/wt-common" ]]; then + . "$LIB_DIR/wt-common" +elif [[ -f "$HOME/.config/wt/lib/wt-common" ]]; then + . "$HOME/.config/wt/lib/wt-common" +else + echo "Error: Cannot find wt-common" >&2 + exit 1 +fi + +usage() { + cat >&2 </dev/null | while IFS= read -r -d '' OLD_LINK; do + rm -f "$OLD_LINK" + done + + # Find all matching directories under SOURCE_DIR + # Use -maxdepth 5 for better performance + # Use -L to follow symlinks (source path might be a symlink) + find -L "$SOURCE_DIR" -maxdepth 5 -type d -name "$pattern" -print0 2>/dev/null | + while IFS= read -r -d '' META_DIR; do + # Compute the path of the parent directory relative to SOURCE_DIR + PARENT_DIR="$(dirname "$META_DIR")" + + # Handle pattern at root (PARENT_DIR == SOURCE_DIR) + if [[ "$PARENT_DIR" == "$SOURCE_DIR" ]]; then + REL_PARENT="" + else + REL_PARENT="${PARENT_DIR#"$SOURCE_DIR"/}" + fi + + # Validate that we actually stripped the prefix (path should be relative now) + if [[ "$REL_PARENT" == /* ]]; then + warn "Skipping $pattern outside source directory: $META_DIR" + continue + fi + + # Destination parent and pattern path in the target directory + if [[ -z "$REL_PARENT" ]]; then + DEST_PARENT="$TARGET_DIR" + else + DEST_PARENT="$TARGET_DIR/$REL_PARENT" + fi + DEST_META="$DEST_PARENT/$pattern" + + echo " -> Linking $DEST_META" + + # Create the parent directories first + mkdir -p "$DEST_PARENT" + + # Create or update the symlink + ln -sfn "$META_DIR" "$DEST_META" + + count=$((count + 1)) + done + + if [[ $count -eq 0 ]]; then + echo " (no '$pattern' directories found)" + fi +} + +# Export each configured pattern +for pattern in $WT_METADATA_PATTERNS; do + export_pattern "$pattern" + echo +done + +success "Done exporting metadata directories." diff --git a/bin/wt-metadata-import b/bin/wt-metadata-import new file mode 100755 index 0000000..3489840 --- /dev/null +++ b/bin/wt-metadata-import @@ -0,0 +1,249 @@ +#!/usr/bin/env bash +# +# wt-metadata-import — Import Project Metadata Into a Target Worktree +# ===================================================================== +# +# This script locates all project metadata directories within a source +# directory (vault) and copies them into a target worktree. It resolves +# any symlinks and copies the actual underlying directories. +# +# Supported metadata includes: +# - .ijwb, .aswb, .clwb (Bazel IDE plugins) +# - .idea (JetBrains IDEs) +# - .vscode (VS Code) +# - .swiftpm (Swift Package Manager) +# - And more (see WT_KNOWN_METADATA in wt-common) +# +# Behavior: +# --------- +# 1. Supports three invocation modes: +# - `wt-metadata-import ` → explicit paths +# - `wt-metadata-import ` → $WT_IDEA_FILES_BASE is source +# - `wt-metadata-import` → interactive worktree selection +# +# 2. For every metadata directory found under the source: +# - Computes its relative path +# - Ensures the corresponding directory exists under the target +# - If the source is a symlink, resolves and copies the real directory +# - If the source is a real directory, copies it directly +# +# Usage Examples: +# --------------- +# wt-metadata-import # Interactive: pick target worktree +# wt-metadata-import ~/dev/worktrees/feature-x # Import to specific worktree +# wt-metadata-import ~/dev/vault ~/dev/worktree # Explicit source and target +# + +set -euo pipefail + +# Resolve script and lib directories +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LIB_DIR="$SCRIPT_DIR/../lib" + +# Bootstrap: source wt-common from lib/ +if [[ -f "$LIB_DIR/wt-common" ]]; then + . "$LIB_DIR/wt-common" +elif [[ -f "$HOME/.config/wt/lib/wt-common" ]]; then + . "$HOME/.config/wt/lib/wt-common" +else + echo "Error: Cannot find wt-common" >&2 + exit 1 +fi + +# Source wt-choose using the helper +wt_source wt-choose + +usage() { + cat >&2 < + +Options: + -y, --yes Skip confirmation prompt + +Arguments: + source-directory Metadata vault location (default: \$WT_IDEA_FILES_BASE) + target-directory Target worktree to import into (REQUIRED, or interactive if omitted) + +Patterns to import: ${WT_METADATA_PATTERNS:-"(none configured)"} + +Modes: + $(basename "$0") # Interactive: pick target worktree + $(basename "$0") # Import to specific worktree + $(basename "$0") # Explicit source and target + +Default source: $WT_IDEA_FILES_BASE +EOF +} + +SOURCE_DIR="" +TARGET_DIR="" +SKIP_CONFIRM=false + +# Parse options +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + -y|--yes) + SKIP_CONFIRM=true + shift + ;; + -*) + error "Unknown option: $1" + usage + exit 1 + ;; + *) + break + ;; + esac +done + +# Parse positional arguments +case $# in + 0) + # No arguments: default source, interactive target selection + SOURCE_DIR="$WT_IDEA_FILES_BASE" + echo "Source: $SOURCE_DIR (from \$WT_IDEA_FILES_BASE)" + echo "No target directory specified, selecting a git worktree..." + TARGET_DIR="$(select_git_worktree)" || exit 1 + ;; + 1) + # One argument: default source, specified target + SOURCE_DIR="$WT_IDEA_FILES_BASE" + TARGET_DIR="$1" + ;; + 2) + # Two arguments: explicit source and target + SOURCE_DIR="$1" + TARGET_DIR="$2" + ;; + *) + error "Too many arguments" + usage + exit 1 + ;; +esac + +# Resolve absolute paths +SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)" +TARGET_DIR="$(cd "$TARGET_DIR" && pwd)" + +# Check if any patterns are configured +if [[ -z "${WT_METADATA_PATTERNS:-}" ]]; then + warn "No metadata patterns configured (WT_METADATA_PATTERNS is empty)" + info "Configure patterns during 'wt install' or edit wt-common directly" + exit 0 +fi + +echo "Source directory: $SOURCE_DIR" +echo "Target directory: $TARGET_DIR" +echo "Patterns: $WT_METADATA_PATTERNS" +echo + +# Confirmation prompt (skip if -y/--yes was provided) +if [[ "$SKIP_CONFIRM" != "true" ]]; then + if ! prompt_confirm "Import metadata from source to target? [y/N]" "n"; then + echo "Aborted." + exit 0 + fi + echo +fi + +# Copy a metadata directory, resolving symlinks to real paths when needed. +# Args: +# $1 = source path (may be symlink or real dir) +# $2 = destination path (directory to create) +# Returns: +# 0 on success, 1 if source doesn't exist (broken symlink) +copy_real_metadata() { + local src="$1" + local dst="$2" + local real_path + + if [[ -L "$src" ]]; then + # It's a symlink: resolve it + real_path="$(readlink "$src")" + + # If the symlink target is relative, make it absolute + if [[ "$real_path" != /* ]]; then + real_path="$(cd "$(dirname "$src")" && cd "$(dirname "$real_path")" && pwd)/$(basename "$real_path")" + fi + + # Check if the resolved path exists + if [[ ! -e "$real_path" ]]; then + warn "Broken symlink: $src -> $real_path (target does not exist)" + return 1 + fi + else + # Not a symlink; use the directory itself + real_path="$src" + fi + + # Copy the real directory contents + # Use cp -a to preserve attributes and copy recursively + cp -a "$real_path" "$dst" +} + +# Import a single pattern +# Args: $1 = pattern name (e.g., ".ijwb") +import_pattern() { + local pattern="$1" + local count=0 + + echo "Importing '$pattern' directories..." + + # Find all matching directories (symlinks or real directories) under the source directory + # Use -L to follow symlinks (source path might be a symlink) + find -L "$SOURCE_DIR" \( -type l -o -type d \) -name "$pattern" -print0 2>/dev/null | + while IFS= read -r -d '' META_SRC; do + # Parent dir relative to SOURCE_DIR + PARENT_SRC="$(dirname "$META_SRC")" + + # Handle pattern at root (PARENT_SRC == SOURCE_DIR) + if [[ "$PARENT_SRC" == "$SOURCE_DIR" ]]; then + REL_PATH="" + else + REL_PATH="${PARENT_SRC#"$SOURCE_DIR"/}" + fi + + # Corresponding target parent + if [[ -z "$REL_PATH" ]]; then + TARGET_PARENT="$TARGET_DIR" + else + TARGET_PARENT="$TARGET_DIR/$REL_PATH" + fi + TARGET_META="$TARGET_PARENT/$pattern" + + echo " Found source: $META_SRC" + echo " -> Installing to: $TARGET_META" + + mkdir -p "$TARGET_PARENT" + + if [[ -e "$TARGET_META" || -L "$TARGET_META" ]]; then + rm -rf "$TARGET_META" + fi + + if ! copy_real_metadata "$META_SRC" "$TARGET_META"; then + echo " Skipping due to broken symlink" + echo " Removing broken symlink from vault: $META_SRC" + rm -f "$META_SRC" + fi + + count=$((count + 1)) + done + + if [[ $count -eq 0 ]]; then + echo " (no '$pattern' directories found in vault)" + fi +} + +# Import each configured pattern +for pattern in $WT_METADATA_PATTERNS; do + import_pattern "$pattern" + echo +done + +success "Done importing metadata." diff --git a/install.sh b/install.sh index ae1cb35..b22f4b4 100755 --- a/install.sh +++ b/install.sh @@ -124,6 +124,164 @@ derive_paths_from_repo() { WT_IDEA_FILES_BASE="$HOME/.config/wt/idea-files/${repo_name}" } +# Detect which known metadata patterns exist in a repository +# Args: $1 = repo path +# Outputs: space-separated list of detected patterns +detect_metadata_patterns() { + local repo="$1" + local detected=() + + for entry in "${WT_KNOWN_METADATA[@]}"; do + local pattern="${entry%%:*}" + # Search for the pattern in the repo (up to depth 3 for nested projects) + # Use -L to follow symlinks (repo path might be a symlink) + if find -L "$repo" -maxdepth 3 -type d -name "$pattern" 2>/dev/null | grep -q .; then + detected+=("$pattern") + fi + done + + echo "${detected[*]}" +} + +# Get description for a metadata pattern +# Args: $1 = pattern +get_pattern_description() { + local pattern="$1" + for entry in "${WT_KNOWN_METADATA[@]}"; do + if [[ "${entry%%:*}" == "$pattern" ]]; then + echo "${entry#*:}" + return + fi + done + echo "$pattern" +} + +# Interactive selection of metadata patterns to preserve +# Args: $1 = repo path +# Sets: WT_METADATA_PATTERNS +select_metadata_patterns() { + local repo="$1" + + echo "════════════════════════════════════════════════════════════════════════════════" + echo " Project Metadata Detection" + echo "════════════════════════════════════════════════════════════════════════════════" + echo + echo "Scanning repository for IDE/editor project metadata..." + echo + + local detected + detected=$(detect_metadata_patterns "$repo") + + if [[ -z "$detected" ]]; then + echo "No known project metadata found in repository." + echo + echo "Known patterns that can be preserved:" + for entry in "${WT_KNOWN_METADATA[@]}"; do + local pattern="${entry%%:*}" + local desc="${entry#*:}" + echo " $pattern - $desc" + done + echo + echo "You can manually add patterns to WT_METADATA_PATTERNS in wt-common later." + WT_METADATA_PATTERNS="" + return 0 + fi + + echo "Detected project metadata:" + echo + + # Convert to array for selection + local -a detected_arr + read -ra detected_arr <<< "$detected" + local -a selected=() + + # Display each detected pattern with checkbox + local i=1 + for pattern in "${detected_arr[@]}"; do + local desc + desc=$(get_pattern_description "$pattern") + echo " $i) [x] $pattern - $desc" + selected+=("$pattern") + ((i++)) + done + + echo + echo "All detected patterns are selected by default." + echo "Enter numbers to toggle (e.g., '1 3'), 'a' for all, 'n' for none, or Enter to confirm:" + echo + + while true; do + local input + if ! read -rp "> " input; then + echo + exit 1 + fi + + # Empty input = confirm current selection + if [[ -z "$input" ]]; then + break + fi + + case "$input" in + a|A|all) + selected=("${detected_arr[@]}") + ;; + n|N|none) + selected=() + ;; + *) + # Toggle specified numbers + for num in $input; do + if [[ "$num" =~ ^[0-9]+$ ]] && ((num >= 1 && num <= ${#detected_arr[@]})); then + local idx=$((num - 1)) + local pattern="${detected_arr[$idx]}" + # Check if already selected + local found=0 + local new_selected=() + for s in "${selected[@]}"; do + if [[ "$s" == "$pattern" ]]; then + found=1 + else + new_selected+=("$s") + fi + done + if ((found)); then + selected=("${new_selected[@]}") + else + selected+=("$pattern") + fi + fi + done + ;; + esac + + # Redisplay with current selection + echo + i=1 + for pattern in "${detected_arr[@]}"; do + local desc + desc=$(get_pattern_description "$pattern") + local mark=" " + for s in "${selected[@]}"; do + [[ "$s" == "$pattern" ]] && mark="x" + done + echo " $i) [$mark] $pattern - $desc" + ((i++)) + done + echo + echo "Enter numbers to toggle, 'a' for all, 'n' for none, or Enter to confirm:" + done + + WT_METADATA_PATTERNS="${selected[*]}" + + echo + if [[ -n "$WT_METADATA_PATTERNS" ]]; then + echo "Selected patterns: $WT_METADATA_PATTERNS" + else + echo "No patterns selected." + fi +} + # Copy toolkit to installation directory install_toolkit() { echo "Installing worktree-toolkit to $INSTALL_DIR ..." @@ -283,7 +441,14 @@ configure_wt_common() { echo fi + # ───────────────────────────────────────────────────────────────────────────── + # Step 5: Detect and select project metadata to preserve + # ───────────────────────────────────────────────────────────────────────────── + echo + select_metadata_patterns "$repo_path" + # Write to wt-common using sed + echo echo "Saving configuration..." sed -i.bak \ -e "s|: \"\${WT_MAIN_REPO_ROOT:=.*}\"|: \"\${WT_MAIN_REPO_ROOT:=\"$WT_MAIN_REPO_ROOT\"}\"|" \ @@ -291,6 +456,7 @@ configure_wt_common() { -e "s|: \"\${WT_IDEA_FILES_BASE:=.*}\"|: \"\${WT_IDEA_FILES_BASE:=\"$WT_IDEA_FILES_BASE\"}\"|" \ -e "s|: \"\${WT_ACTIVE_WORKTREE:=.*}\"|: \"\${WT_ACTIVE_WORKTREE:=\"$WT_ACTIVE_WORKTREE\"}\"|" \ -e "s|: \"\${WT_BASE_BRANCH:=.*}\"|: \"\${WT_BASE_BRANCH:=\"$WT_BASE_BRANCH\"}\"|" \ + -e "s|: \"\${WT_METADATA_PATTERNS:=.*}\"|: \"\${WT_METADATA_PATTERNS:=\"$WT_METADATA_PATTERNS\"}\"|" \ "$wt_common" rm -f "$wt_common.bak" echo " ✓ Configuration saved" @@ -413,51 +579,64 @@ setup_cron_job() { echo " crontab -e Edit cron jobs (to modify or remove)" } -# Sync .ijwb metadata from main repo to shared location -sync_ijwb() { +# Sync project metadata from main repo to shared location +sync_metadata() { if [[ ! -d "$WT_MAIN_REPO_ROOT" ]]; then - echo "Skipping .ijwb sync: Main repository not found at $WT_MAIN_REPO_ROOT" + echo "Skipping metadata sync: Main repository not found at $WT_MAIN_REPO_ROOT" + return 0 + fi + + # Check if any patterns are configured + if [[ -z "${WT_METADATA_PATTERNS:-}" ]]; then + echo "Skipping metadata sync: No patterns configured" return 0 fi echo "════════════════════════════════════════════════════════════════════════════════" - echo " IntelliJ Metadata Export (.ijwb)" + echo " Project Metadata Export" echo "════════════════════════════════════════════════════════════════════════════════" echo - echo "Scanning for existing IntelliJ Bazel projects..." + echo "Scanning for existing project metadata..." + echo "Patterns: $WT_METADATA_PATTERNS" + echo - local ijwb_count - # Use -maxdepth 3 since .ijwb dirs are at service level (e.g., orders/.ijwb) - ijwb_count=$(find "$WT_MAIN_REPO_ROOT" -maxdepth 3 -type d -name '.ijwb' 2>/dev/null | wc -l | tr -d ' ') + # Count total metadata directories found + local total_count=0 + for pattern in $WT_METADATA_PATTERNS; do + local count + count=$(find -L "$WT_MAIN_REPO_ROOT" -maxdepth 5 -type d -name "$pattern" 2>/dev/null | wc -l | tr -d ' ') + if [[ $count -gt 0 ]]; then + echo " Found $count '$pattern' directories" + total_count=$((total_count + count)) + fi + done - if [[ "$ijwb_count" -eq 0 ]]; then - echo "No .ijwb directories found in $WT_MAIN_REPO_ROOT" + if [[ $total_count -eq 0 ]]; then + echo "No project metadata directories found in $WT_MAIN_REPO_ROOT" echo - echo "This is expected if you haven't imported any projects in IntelliJ yet." - echo "After importing projects, run 'wt ijwb-export' to export metadata." + echo "This is expected if you haven't set up any IDE projects yet." + echo "After setting up projects, run 'wt metadata-export' to export metadata." return 0 fi - echo "Found $ijwb_count .ijwb directories in main repository." echo echo "This step will:" - echo " 1. Copy .ijwb directories from: $WT_MAIN_REPO_ROOT" - echo " 2. Store them in the vault: $WT_IDEA_FILES_BASE" + echo " 1. Link metadata directories from: $WT_MAIN_REPO_ROOT" + echo " 2. Store links in the vault: $WT_IDEA_FILES_BASE" echo - echo "The vault is a shared location where .ijwb metadata is stored." - echo "When you create new worktrees, this metadata is automatically installed," - echo "avoiding expensive IntelliJ re-imports and re-indexing." + echo "The vault is a shared location where metadata is stored." + echo "When you create new worktrees, this metadata is automatically installed." echo - if ! prompt_confirm "Export .ijwb metadata to vault? [Y/n]" "y"; then - echo "Skipping. You can run 'wt ijwb-export' manually later." + if ! prompt_confirm "Export metadata to vault? [Y/n]" "y"; then + echo "Skipping. You can run 'wt metadata-export' manually later." return 0 fi echo - echo "Exporting .ijwb metadata..." + echo "Exporting metadata..." # Use -y to skip internal confirmation (installer already prompted) - "$INSTALL_DIR/bin/wt-ijwb-export" -y "$WT_MAIN_REPO_ROOT" "$WT_IDEA_FILES_BASE" + "$INSTALL_DIR/bin/wt-metadata-export" -y "$WT_MAIN_REPO_ROOT" "$WT_IDEA_FILES_BASE" } # Print completion message @@ -521,7 +700,7 @@ main() { migrate_repo echo - sync_ijwb + sync_metadata echo setup_cron_job diff --git a/lib/wt-common b/lib/wt-common index 4c68f48..b5df7dd 100644 --- a/lib/wt-common +++ b/lib/wt-common @@ -27,6 +27,39 @@ # Default primary branch to sync from when creating new worktrees : "${WT_BASE_BRANCH:="master"}" +# Project metadata patterns to preserve across worktrees (space-separated) +# Detected during install, can be customized later +: "${WT_METADATA_PATTERNS:=""}" + +# ───────────────────────────────────────────────────────────────────────────── +# Known project metadata patterns (for detection) +# Format: pattern:description +# ───────────────────────────────────────────────────────────────────────────── +WT_KNOWN_METADATA=( + # JetBrains IDEs + ".idea:JetBrains IDEs (IntelliJ, WebStorm, PyCharm, etc.)" + ".run:JetBrains run/debug configurations" + ".fleet:JetBrains Fleet" + # Bazel + ".ijwb:IntelliJ + Bazel plugin (legacy)" + ".aswb:Android Studio + Bazel plugin (legacy)" + ".clwb:CLion + Bazel plugin (legacy)" + ".bazelbsp:JetBrains Bazel plugin (new, BSP-based)" + ".bsp:Build Server Protocol (Scala, Java, Bazel)" + # Xcode / iOS + ".swiftpm:Swift Package Manager metadata" + "xcuserdata:Xcode user data (breakpoints, UI state)" + # VS Code + ".vscode:Visual Studio Code" + # Scala + ".metals:Metals LSP (Scala)" + ".bloop:Bloop build server (Scala)" + # Eclipse + ".settings:Eclipse settings" + ".project:Eclipse project" + ".classpath:Eclipse classpath" +) + # ───────────────────────────────────────────────────────────────────────────── # Color support (only if output is a TTY) # ───────────────────────────────────────────────────────────────────────────── diff --git a/lib/wt-ijwb-refresh b/lib/wt-ijwb-refresh index 5d52339..e462182 100755 --- a/lib/wt-ijwb-refresh +++ b/lib/wt-ijwb-refresh @@ -299,33 +299,33 @@ refresh_all_ijwb() { log "Refresh complete: $REFRESH_COUNT succeeded, $ERROR_COUNT failed" } -# Re-export .ijwb directories to vault +# Re-export metadata directories to vault do_export() { if [[ "$NO_EXPORT" == "true" ]]; then log "Skipping export (--no-export specified)" return 0 fi - + log "" - log "Re-exporting .ijwb directories to vault..." - - # Find wt-ijwb-export: try bin directory first, then PATH - local ijwb_export="" - if [[ -x "$BIN_DIR/wt-ijwb-export" ]]; then - ijwb_export="$BIN_DIR/wt-ijwb-export" - elif command -v wt-ijwb-export >/dev/null 2>&1; then - ijwb_export="wt-ijwb-export" + log "Re-exporting metadata directories to vault..." + + # Find wt-metadata-export: try bin directory first, then PATH + local metadata_export="" + if [[ -x "$BIN_DIR/wt-metadata-export" ]]; then + metadata_export="$BIN_DIR/wt-metadata-export" + elif command -v wt-metadata-export >/dev/null 2>&1; then + metadata_export="wt-metadata-export" else - error "Cannot find wt-ijwb-export" + error "Cannot find wt-metadata-export" return 1 fi - + if [[ "$DRY_RUN" == "true" ]]; then - log "[dry-run] Would run: $ijwb_export -y" + log "[dry-run] Would run: $metadata_export -y" return 0 fi - - "$ijwb_export" -y + + "$metadata_export" -y } # Ensure log directory exists diff --git a/wt.sh b/wt.sh index abc0dd2..bb50dd0 100755 --- a/wt.sh +++ b/wt.sh @@ -120,13 +120,16 @@ wt() { shift 2>/dev/null || true case "$cmd" in - add) _wt_run wt-add "$@" ;; - switch) _wt_run wt-switch "$@" ;; - remove) _wt_run wt-remove "$@" ;; - list) _wt_run wt-list "$@" ;; - ijwb-export) _wt_run wt-ijwb-export "$@" ;; - ijwb-import) _wt_run wt-ijwb-import "$@" ;; - cd) __wt_do_cd "$@" ;; + add) _wt_run wt-add "$@" ;; + switch) _wt_run wt-switch "$@" ;; + remove) _wt_run wt-remove "$@" ;; + list) _wt_run wt-list "$@" ;; + metadata-export) _wt_run wt-metadata-export "$@" ;; + metadata-import) _wt_run wt-metadata-import "$@" ;; + # Legacy aliases (kept for backward compatibility) + ijwb-export) _wt_run wt-metadata-export "$@" ;; + ijwb-import) _wt_run wt-metadata-import "$@" ;; + cd) __wt_do_cd "$@" ;; help|--help|-h|"") wt_show_help # helper for showing help, defined in wt-help library ;; From a3ceca6a52a09b5bca21f6d80b5a21db0d1f6f9b Mon Sep 17 00:00:00 2001 From: Guodong Zhu Date: Thu, 29 Jan 2026 14:33:04 -0500 Subject: [PATCH 03/11] Fix nested metadata detection and export deduplication - Detection now finds top-level metadata dirs only (e.g., .idea inside .ijwb is not listed separately) - Export deduplicates by sorting paths and skipping nested ones - Add tilde expansion for paths containing ~/ Algorithm: sort all found paths, keep only those not inside another kept path Co-Authored-By: Claude Opus 4.5 --- bin/wt-metadata-export | 135 ++++++++++++++++++++++++++--------------- bin/wt-metadata-import | 6 ++ install.sh | 53 ++++++++++++++-- 3 files changed, 139 insertions(+), 55 deletions(-) diff --git a/bin/wt-metadata-export b/bin/wt-metadata-export index a72b79b..b7ce176 100755 --- a/bin/wt-metadata-export +++ b/bin/wt-metadata-export @@ -110,6 +110,12 @@ case $# in ;; esac +# Expand ~ to $HOME (bash doesn't expand ~ in variable assignments) +[[ "$SOURCE_DIR" == "~/"* ]] && SOURCE_DIR="${HOME}/${SOURCE_DIR:2}" +[[ "$SOURCE_DIR" == "~" ]] && SOURCE_DIR="$HOME" +[[ "$TARGET_DIR" == "~/"* ]] && TARGET_DIR="${HOME}/${TARGET_DIR:2}" +[[ "$TARGET_DIR" == "~" ]] && TARGET_DIR="$HOME" + # Validate directories exist before proceeding if [[ ! -d "$SOURCE_DIR" ]]; then error "Source directory does not exist: $SOURCE_DIR" @@ -146,68 +152,99 @@ if [[ "$SKIP_CONFIRM" != "true" ]]; then echo fi -# Export a single pattern -# Args: $1 = pattern name (e.g., ".ijwb") -export_pattern() { - local pattern="$1" - local count=0 +# Find all metadata directories and deduplicate +# Outputs: one path per line, with nested paths removed +# (e.g., if .ijwb contains .idea, only .ijwb is listed) +find_all_metadata_dirs() { + local all_paths=() + + # Collect all metadata directories for all patterns + for pattern in $WT_METADATA_PATTERNS; do + while IFS= read -r path; do + [[ -n "$path" ]] && all_paths+=("$path") + done < <(find -L "$SOURCE_DIR" -maxdepth 5 -type d -name "$pattern" 2>/dev/null) + done + + # Sort paths (shorter paths come first) + local sorted_paths + sorted_paths=$(printf '%s\n' "${all_paths[@]}" | sort) + + # Deduplicate: skip paths that are inside another metadata path + local kept_paths=() + while IFS= read -r path; do + [[ -z "$path" ]] && continue + local dominated=false + + for kept in "${kept_paths[@]}"; do + # Check if $path is inside $kept (kept is a prefix of path) + if [[ "$path" == "$kept/"* ]]; then + dominated=true + break + fi + done + + if [[ "$dominated" == "false" ]]; then + kept_paths+=("$path") + echo "$path" + fi + done <<< "$sorted_paths" +} - echo "Exporting '$pattern' directories..." +echo "Finding and deduplicating metadata directories..." +echo - # First, clean up any existing symlinks for this pattern in the vault +# Clean up existing symlinks in vault for all patterns +for pattern in $WT_METADATA_PATTERNS; do find -L "$TARGET_DIR" -type l -name "$pattern" -print0 2>/dev/null | while IFS= read -r -d '' OLD_LINK; do rm -f "$OLD_LINK" done +done - # Find all matching directories under SOURCE_DIR - # Use -maxdepth 5 for better performance - # Use -L to follow symlinks (source path might be a symlink) - find -L "$SOURCE_DIR" -maxdepth 5 -type d -name "$pattern" -print0 2>/dev/null | - while IFS= read -r -d '' META_DIR; do - # Compute the path of the parent directory relative to SOURCE_DIR - PARENT_DIR="$(dirname "$META_DIR")" - - # Handle pattern at root (PARENT_DIR == SOURCE_DIR) - if [[ "$PARENT_DIR" == "$SOURCE_DIR" ]]; then - REL_PARENT="" - else - REL_PARENT="${PARENT_DIR#"$SOURCE_DIR"/}" - fi +# Find all metadata dirs (deduplicated) and export them +count=0 +while IFS= read -r META_DIR; do + [[ -z "$META_DIR" ]] && continue - # Validate that we actually stripped the prefix (path should be relative now) - if [[ "$REL_PARENT" == /* ]]; then - warn "Skipping $pattern outside source directory: $META_DIR" - continue - fi + pattern="$(basename "$META_DIR")" + PARENT_DIR="$(dirname "$META_DIR")" - # Destination parent and pattern path in the target directory - if [[ -z "$REL_PARENT" ]]; then - DEST_PARENT="$TARGET_DIR" - else - DEST_PARENT="$TARGET_DIR/$REL_PARENT" - fi - DEST_META="$DEST_PARENT/$pattern" + # Handle pattern at root (PARENT_DIR == SOURCE_DIR) + if [[ "$PARENT_DIR" == "$SOURCE_DIR" ]]; then + REL_PARENT="" + else + REL_PARENT="${PARENT_DIR#"$SOURCE_DIR"/}" + fi - echo " -> Linking $DEST_META" + # Validate that we actually stripped the prefix (path should be relative now) + if [[ "$REL_PARENT" == /* ]]; then + warn "Skipping $pattern outside source directory: $META_DIR" + continue + fi - # Create the parent directories first - mkdir -p "$DEST_PARENT" + # Destination parent and pattern path in the target directory + if [[ -z "$REL_PARENT" ]]; then + DEST_PARENT="$TARGET_DIR" + else + DEST_PARENT="$TARGET_DIR/$REL_PARENT" + fi + DEST_META="$DEST_PARENT/$pattern" - # Create or update the symlink - ln -sfn "$META_DIR" "$DEST_META" + echo " -> Linking $DEST_META" - count=$((count + 1)) - done + # Create the parent directories first + mkdir -p "$DEST_PARENT" - if [[ $count -eq 0 ]]; then - echo " (no '$pattern' directories found)" - fi -} + # Create or update the symlink + ln -sfn "$META_DIR" "$DEST_META" -# Export each configured pattern -for pattern in $WT_METADATA_PATTERNS; do - export_pattern "$pattern" + count=$((count + 1)) +done < <(find_all_metadata_dirs) + +if [[ $count -eq 0 ]]; then + echo " (no metadata directories found)" +else echo -done + echo "Exported $count metadata directories." +fi -success "Done exporting metadata directories." +success "Done." diff --git a/bin/wt-metadata-import b/bin/wt-metadata-import index 3489840..f97fc32 100755 --- a/bin/wt-metadata-import +++ b/bin/wt-metadata-import @@ -127,6 +127,12 @@ case $# in ;; esac +# Expand ~ to $HOME (bash doesn't expand ~ in variable assignments) +[[ "$SOURCE_DIR" == "~/"* ]] && SOURCE_DIR="${HOME}/${SOURCE_DIR:2}" +[[ "$SOURCE_DIR" == "~" ]] && SOURCE_DIR="$HOME" +[[ "$TARGET_DIR" == "~/"* ]] && TARGET_DIR="${HOME}/${TARGET_DIR:2}" +[[ "$TARGET_DIR" == "~" ]] && TARGET_DIR="$HOME" + # Resolve absolute paths SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)" TARGET_DIR="$(cd "$TARGET_DIR" && pwd)" diff --git a/install.sh b/install.sh index b22f4b4..9d58ce4 100755 --- a/install.sh +++ b/install.sh @@ -127,20 +127,61 @@ derive_paths_from_repo() { # Detect which known metadata patterns exist in a repository # Args: $1 = repo path # Outputs: space-separated list of detected patterns +# Note: Deduplicates by finding top-level metadata dirs only +# (e.g., if .ijwb contains .idea, only .ijwb is reported) detect_metadata_patterns() { local repo="$1" - local detected=() + local all_paths=() + # Find all metadata directories for all known patterns for entry in "${WT_KNOWN_METADATA[@]}"; do local pattern="${entry%%:*}" - # Search for the pattern in the repo (up to depth 3 for nested projects) - # Use -L to follow symlinks (repo path might be a symlink) - if find -L "$repo" -maxdepth 3 -type d -name "$pattern" 2>/dev/null | grep -q .; then - detected+=("$pattern") + while IFS= read -r path; do + [[ -n "$path" ]] && all_paths+=("$path") + done < <(find -L "$repo" -maxdepth 5 -type d -name "$pattern" 2>/dev/null) + done + + # No metadata found + if [[ ${#all_paths[@]} -eq 0 ]]; then + return + fi + + # Sort paths (shorter paths come first) + local sorted_paths + sorted_paths=$(printf '%s\n' "${all_paths[@]}" | sort) + + # Deduplicate: keep only top-level metadata dirs + local kept_paths=() + while IFS= read -r path; do + [[ -z "$path" ]] && continue + local dominated=false + + for kept in "${kept_paths[@]}"; do + if [[ "$path" == "$kept/"* ]]; then + dominated=true + break + fi + done + + if [[ "$dominated" == "false" ]]; then + kept_paths+=("$path") fi + done <<< "$sorted_paths" + + # Extract unique pattern names from kept paths + local patterns=() + for path in "${kept_paths[@]}"; do + local pattern + pattern="$(basename "$path")" + # Add to patterns if not already present + local found=false + for p in "${patterns[@]}"; do + [[ "$p" == "$pattern" ]] && found=true && break + done + [[ "$found" == "false" ]] && patterns+=("$pattern") done - echo "${detected[*]}" + echo "${patterns[*]}" } # Get description for a metadata pattern From 285e095e1ca7649ab9c3cbea3026a752d469cf3c Mon Sep 17 00:00:00 2001 From: Guodong Zhu Date: Thu, 29 Jan 2026 15:24:11 -0500 Subject: [PATCH 04/11] Remove obsolete wt-ijwb-export and wt-ijwb-import scripts These are replaced by wt-metadata-export and wt-metadata-import. The 'wt ijwb-export' and 'wt ijwb-import' commands still work as aliases pointing to the new metadata scripts. Co-Authored-By: Claude Opus 4.5 --- bin/wt-ijwb-export | 207 -------------------------------------- bin/wt-ijwb-import | 246 --------------------------------------------- 2 files changed, 453 deletions(-) delete mode 100755 bin/wt-ijwb-export delete mode 100755 bin/wt-ijwb-import diff --git a/bin/wt-ijwb-export b/bin/wt-ijwb-export deleted file mode 100755 index 1137806..0000000 --- a/bin/wt-ijwb-export +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env bash -# -# wt-ijwb-export — Export IntelliJ `.ijwb` Directories to Metadata Vault -# ========================================================================= -# -# This script scans a source directory for IntelliJ Bazel project metadata -# directories named `.ijwb` and recreates the same directory structure under a -# target directory. Instead of copying actual files, it creates **symbolic links** -# pointing back to the real `.ijwb` directories in the source. -# -# This is useful when: -# - You maintain a *canonical* set of `.ijwb` directories (e.g., from a main -# worktree), and -# - You want additional worktrees or environments to *reuse* those metadata -# directories without duplicating them. -# -# Behavior: -# --------- -# 1. Accepts a and . -# 2. Recursively finds all `.ijwb` directories under the source. -# 3. Computes the relative path from the source root for each `.ijwb`. -# 4. Recreates that directory structure under the target. -# 5. Creates symbolic links: -# //.ijwb → //.ijwb -# -# Guarantees: -# ----------- -# - Absolute paths are used to avoid inconsistencies when resolving symlinks. -# - Only `.ijwb` directories are linked; all other files are ignored. -# - The script preserves directory structure so the target mirrors the source layout. -# -# Usage: -# wt-ijwb-export # Use defaults from env vars -# wt-ijwb-export -# -# Defaults (when no arguments): -# source = $WT_MAIN_REPO_ROOT -# target = $WT_IDEA_FILES_BASE -# -# Example: -# wt-ijwb-export # Use env var defaults -# wt-ijwb-export ~/dev/main-worktree ~/dev/shared-idea -# -# Notes: -# ------ -# - This script creates symlinks, not copies, so changes in the source `.ijwb` -# directories propagate automatically to all linked targets. -# - Useful for IntelliJ + Bazel workflows where `.ijwb` metadata is large, -# expensive to regenerate, or should be shared across worktrees. -# -# IMPORTANT: After creating a new IntelliJ Bazel project in the source directory, -# re-run this script to sync the new .ijwb metadata to the vault. This ensures -# that wt-ijwb-import will include the new metadata when setting up worktrees. -# - -set -euo pipefail - -# Resolve script and lib directories -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -LIB_DIR="$SCRIPT_DIR/../lib" - -# Bootstrap: source wt-common from lib/ -if [[ -f "$LIB_DIR/wt-common" ]]; then - . "$LIB_DIR/wt-common" -elif [[ -f "$HOME/.config/wt/lib/wt-common" ]]; then - . "$HOME/.config/wt/lib/wt-common" -else - echo "Error: Cannot find wt-common" >&2 - exit 1 -fi - -usage() { - cat >&2 < Linking $DEST_IJWB" - - # Create the parent directories first - mkdir -p "$DEST_PARENT" - - # Create or update the symlink - ln -sfn "$IJWB_DIR" "$DEST_IJWB" - - done - -success "Done linking .ijwb directories." diff --git a/bin/wt-ijwb-import b/bin/wt-ijwb-import deleted file mode 100755 index 13cd525..0000000 --- a/bin/wt-ijwb-import +++ /dev/null @@ -1,246 +0,0 @@ -#!/usr/bin/env bash -# -# wt-ijwb-import — Import IntelliJ `.ijwb` Metadata Into a Target Worktree -# ========================================================================== -# -# This script locates all `.ijwb` directories within a source directory and -# reproduces them inside a target directory. Instead of linking the `.ijwb` -# directory itself, the script **resolves any symlinks** in the source and -# **copies the actual underlying directory** into the target. -# -# This allows a worktree to receive the “real” IntelliJ Bazel project metadata -# even when the source `.ijwb` directories are symlinked (e.g., from a shared -# metadata workspace). It is especially useful in large monorepos where Bazel -# import time is expensive, and multiple worktrees must share identical IDE -# metadata. -# -# Behavior: -# --------- -# 1. Supports three invocation modes: -# - `wt-ijwb-import ` → explicit paths -# - `wt-ijwb-import ` → $WT_IDEA_FILES_BASE is source -# - `wt-ijwb-import` → $WT_IDEA_FILES_BASE is source, -# interactively pick the target worktree -# -# 2. If no target directory is provided, the script invokes `select_git_worktree` -# (from `wt-choose`) to interactively select one. -# -# 3. For every `.ijwb` directory found under the source: -# - Computes its relative path -# - Ensures the corresponding directory exists under the target -# - If the source `.ijwb` is a symlink: -# • Resolves the real path (absolute) -# • Copies the *real* directory using `cp -a` -# - If the source `.ijwb` is a real directory: -# • Copies it directly -# -# 4. The result is a target worktree whose `.ijwb` directories exactly mirror the -# underlying canonical IntelliJ metadata, avoiding duplication while remaining -# resilient to changes in the source workspace. -# -# Guarantees: -# ----------- -# - Works with absolute paths to avoid symlink resolution issues. -# - Only `.ijwb` directories are processed; other files and directories are ignored. -# - Safe to rerun; existing `.ijwb` directories in the target are replaced. -# - All UI output is printed normally; scripting callers can capture behavior reliably. -# -# Usage Examples: -# --------------- -# # Import real .ijwb content from current directory into a selected worktree -# wt-ijwb-import -# -# # Import from one explicit directory to another -# wt-ijwb-import ~/dev/shared-idea ~/dev/worktrees/feature-x -# -# # Use current directory as source, specific directory as target -# wt-ijwb-import ~/Development/java-worktrees/feature-x -# -# Note: If you create a new IntelliJ Bazel project in the source directory, -# you need to run wt-ijwb-export again to sync it to the metadata vault. -# This ensures newly created worktrees will receive the new .ijwb metadata. -# - -set -euo pipefail - -# Resolve script and lib directories -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -LIB_DIR="$SCRIPT_DIR/../lib" - -# Bootstrap: source wt-common from lib/ -if [[ -f "$LIB_DIR/wt-common" ]]; then - . "$LIB_DIR/wt-common" -elif [[ -f "$HOME/.config/wt/lib/wt-common" ]]; then - . "$HOME/.config/wt/lib/wt-common" -else - echo "Error: Cannot find wt-common" >&2 - exit 1 -fi - -# Source wt-choose using the helper -wt_source wt-choose - -usage() { - cat >&2 < - -Options: - -y, --yes Skip confirmation prompt - -Arguments: - source-directory Metadata vault location (default: \$WT_IDEA_FILES_BASE) - target-directory Target worktree to import into (REQUIRED, or interactive if omitted) - -Modes: - $(basename "$0") # Interactive: pick target worktree - $(basename "$0") # Import to specific worktree - $(basename "$0") # Explicit source and target - -Default source: $WT_IDEA_FILES_BASE -EOF -} - -SOURCE_DIR="" -TARGET_DIR="" -SKIP_CONFIRM=false - -# Parse options -while [[ $# -gt 0 ]]; do - case "$1" in - -h|--help) - usage - exit 0 - ;; - -y|--yes) - SKIP_CONFIRM=true - shift - ;; - -*) - error "Unknown option: $1" - usage - exit 1 - ;; - *) - break - ;; - esac -done - -# Parse positional arguments -case $# in - 0) - # No arguments: default source, interactive target selection - SOURCE_DIR="$WT_IDEA_FILES_BASE" - echo "Source: $SOURCE_DIR (from \$WT_IDEA_FILES_BASE)" - echo "No target directory specified, selecting a git worktree..." - TARGET_DIR="$(select_git_worktree)" || exit 1 - ;; - 1) - # One argument: default source, specified target - SOURCE_DIR="$WT_IDEA_FILES_BASE" - TARGET_DIR="$1" - ;; - 2) - # Two arguments: explicit source and target - SOURCE_DIR="$1" - TARGET_DIR="$2" - ;; - *) - error "Too many arguments" - usage - exit 1 - ;; -esac - -# Resolve absolute paths -SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)" -TARGET_DIR="$(cd "$TARGET_DIR" && pwd)" - -echo "Source directory: $SOURCE_DIR" -echo "Target directory: $TARGET_DIR" -echo - -# Confirmation prompt (skip if -y/--yes was provided) -if [[ "$SKIP_CONFIRM" != "true" ]]; then - if ! prompt_confirm "Import .ijwb metadata from source to target? [y/N]" "n"; then - echo "Aborted." - exit 0 - fi - echo -fi - -# Copy a .ijwb directory, resolving symlinks to real paths when needed. -# Args: -# $1 = source .ijwb path (may be symlink or real dir) -# $2 = destination .ijwb path (directory to create) -# Returns: -# 0 on success, 1 if source doesn't exist (broken symlink) -copy_real_ijwb() { - local src="$1" - local dst="$2" - local real_path - - if [[ -L "$src" ]]; then - # It's a symlink: resolve it - real_path="$(readlink "$src")" - - # If the symlink target is relative, make it absolute - if [[ "$real_path" != /* ]]; then - real_path="$(cd "$(dirname "$src")" && cd "$(dirname "$real_path")" && pwd)/$(basename "$real_path")" - fi - - # Check if the resolved path exists - if [[ ! -e "$real_path" ]]; then - warn "Broken symlink: $src -> $real_path (target does not exist)" - return 1 - fi - else - # Not a symlink; use the directory itself - real_path="$src" - fi - - # Copy the real directory contents - # Use cp -a to preserve attributes and copy recursively - cp -a "$real_path" "$dst" -} - -# Find all .ijwb directories (symlinks or real directories) under the source directory -find "$SOURCE_DIR" \( -type l -o -type d \) -name '.ijwb' -print0 | - while IFS= read -r -d '' IJWB_SRC; do - # Parent dir relative to SOURCE_DIR - PARENT_SRC="$(dirname "$IJWB_SRC")" - - # Handle .ijwb at root (PARENT_SRC == SOURCE_DIR) - if [[ "$PARENT_SRC" == "$SOURCE_DIR" ]]; then - REL_PATH="" - else - REL_PATH="${PARENT_SRC#"$SOURCE_DIR"/}" - fi - - # Corresponding target parent - if [[ -z "$REL_PATH" ]]; then - TARGET_PARENT="$TARGET_DIR" - else - TARGET_PARENT="$TARGET_DIR/$REL_PATH" - fi - TARGET_IJWB="$TARGET_PARENT/.ijwb" - - echo "Found source: $IJWB_SRC" - echo " -> Installing to: $TARGET_IJWB" - - mkdir -p "$TARGET_PARENT" - - if [[ -e "$TARGET_IJWB" || -L "$TARGET_IJWB" ]]; then - rm -rf "$TARGET_IJWB" - fi - - if ! copy_real_ijwb "$IJWB_SRC" "$TARGET_IJWB"; then - echo " Skipping due to broken symlink" - echo " Removing broken symlink from vault: $IJWB_SRC" - rm -f "$IJWB_SRC" - fi - - echo - done - -success "Done installing .ijwb metadata." From bfddee62053b261a6ab9f9bc9655367c0f55528d Mon Sep 17 00:00:00 2001 From: Guodong Zhu Date: Sun, 8 Feb 2026 21:22:16 -0500 Subject: [PATCH 05/11] Fix wt-common fallback path in metadata scripts Update fallback path from ~/.config/wt/ to ~/.wt/ to match PR #4's path scheme update. Co-Authored-By: Claude Opus 4.5 --- bin/wt-metadata-export | 4 ++-- bin/wt-metadata-import | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/wt-metadata-export b/bin/wt-metadata-export index b7ce176..9072954 100755 --- a/bin/wt-metadata-export +++ b/bin/wt-metadata-export @@ -41,8 +41,8 @@ LIB_DIR="$SCRIPT_DIR/../lib" # Bootstrap: source wt-common from lib/ if [[ -f "$LIB_DIR/wt-common" ]]; then . "$LIB_DIR/wt-common" -elif [[ -f "$HOME/.config/wt/lib/wt-common" ]]; then - . "$HOME/.config/wt/lib/wt-common" +elif [[ -f "$HOME/.wt/lib/wt-common" ]]; then + . "$HOME/.wt/lib/wt-common" else echo "Error: Cannot find wt-common" >&2 exit 1 diff --git a/bin/wt-metadata-import b/bin/wt-metadata-import index f97fc32..3c60e9a 100755 --- a/bin/wt-metadata-import +++ b/bin/wt-metadata-import @@ -43,8 +43,8 @@ LIB_DIR="$SCRIPT_DIR/../lib" # Bootstrap: source wt-common from lib/ if [[ -f "$LIB_DIR/wt-common" ]]; then . "$LIB_DIR/wt-common" -elif [[ -f "$HOME/.config/wt/lib/wt-common" ]]; then - . "$HOME/.config/wt/lib/wt-common" +elif [[ -f "$HOME/.wt/lib/wt-common" ]]; then + . "$HOME/.wt/lib/wt-common" else echo "Error: Cannot find wt-common" >&2 exit 1 From 0b6a6bba1687b58621d1e9d5dabb33958bddbbc3 Mon Sep 17 00:00:00 2001 From: Guodong Zhu Date: Sun, 8 Feb 2026 21:34:23 -0500 Subject: [PATCH 06/11] Fix code review issues and generalize metadata refresh - Fix subshell variable scope bug in wt-metadata-import (pipe to while loop caused count variable to be lost; now uses process substitution) - Rename wt-ijwb-refresh to wt-metadata-refresh - Now refreshes all Bazel IDE patterns (.ijwb, .aswb, .clwb) that are configured in WT_METADATA_PATTERNS, not just .ijwb - Updated all references in README.md and install.sh - Update documentation to reflect generic metadata support Co-Authored-By: Claude Opus 4.5 --- README.md | 37 +-- bin/wt-metadata-import | 74 +++--- install.sh | 20 +- lib/{wt-ijwb-refresh => wt-metadata-refresh} | 244 +++++++++++-------- 4 files changed, 215 insertions(+), 160 deletions(-) rename lib/{wt-ijwb-refresh => wt-metadata-refresh} (70%) diff --git a/README.md b/README.md index 5699b83..c0b421a 100644 --- a/README.md +++ b/README.md @@ -182,11 +182,11 @@ wt ijwb-export -y wt ijwb-import -y ~/Development/java-worktrees/feature/foo ``` -### Refreshing Stale .ijwb Metadata (Cron Job) +### Refreshing Stale Bazel IDE Metadata (Cron Job) -When most development work is done in worktrees, the `.ijwb` directories in the main repository can become stale (targets files don't reflect new Bazel targets). +When most development work is done in worktrees, the Bazel IDE directories (`.ijwb`, `.aswb`, `.clwb`) in the main repository can become stale (targets files don't reflect new Bazel targets). -The `lib/wt-ijwb-refresh` script is designed to run as a cron job to keep metadata current. +The `lib/wt-metadata-refresh` script is designed to run as a cron job to keep metadata current. **Note:** When IntelliJ has `derive_targets_from_directories: true` in `.bazelproject` (the default), it queries Bazel fresh on every sync. The `targets-*` file serves as a cache for initial project imports and may improve import speed. @@ -202,27 +202,28 @@ mkdir -p ~/.wt/logs crontab -e # Add this line to run nightly at 2am (uses login shell for full PATH): -0 2 * * * /bin/zsh -lc '~/.wt/lib/wt-ijwb-refresh' >> ~/.wt/logs/ijwb-refresh.log 2>&1 +0 2 * * * /bin/zsh -lc '~/.wt/lib/wt-metadata-refresh' >> ~/.wt/logs/metadata-refresh.log 2>&1 ``` You can also run the script manually: ```bash -# Refresh all .ijwb directories and re-export to vault -~/.wt/lib/wt-ijwb-refresh +# Refresh all Bazel IDE directories and re-export to vault +~/.wt/lib/wt-metadata-refresh # Preview what would be refreshed (dry run) -~/.wt/lib/wt-ijwb-refresh --dry-run +~/.wt/lib/wt-metadata-refresh --dry-run # Refresh targets files only (skip re-export step) -~/.wt/lib/wt-ijwb-refresh --no-export +~/.wt/lib/wt-metadata-refresh --no-export ``` The refresh script: -- Uses `bazel query` to regenerate `targets/targets-*` files in each `.ijwb` directory +- Uses `bazel query` to regenerate `targets/targets-*` files in each Bazel IDE directory +- Supports all Bazel patterns configured in WT_METADATA_PATTERNS (`.ijwb`, `.aswb`, `.clwb`) - Parses `.bazelproject` to determine which directories to include in the query - Preserves existing targets file hashes (IntelliJ may reference them) -- Re-exports refreshed metadata to the vault +- Re-exports all metadata to the vault (including non-Bazel patterns) - Logs timestamped output for monitoring - Returns exit codes: 0=success, 1=error, 2=partial success @@ -278,9 +279,9 @@ export WT_IDEA_FILES_BASE="$HOME/Development/idea-project-files" ``` Used by: -- wt-ijwb-import -- wt-ijwb-export -- wt-ijwb-refresh +- wt-metadata-import +- wt-metadata-export +- wt-metadata-refresh - wt-add (when installing metadata) @@ -332,14 +333,14 @@ wt/ │ ├── wt-list │ ├── wt-remove │ ├── wt-switch -│ ├── wt-ijwb-import -│ └── wt-ijwb-export +│ ├── wt-metadata-import +│ └── wt-metadata-export ├── lib/ # Shared libraries │ ├── wt-common # Configuration and helpers │ ├── wt-choose # Interactive worktree selection │ ├── wt-help # Help text for wt command │ ├── wt-completion # Shell completion for wt command -│ └── wt-ijwb-refresh # Cron script to refresh .ijwb metadata +│ └── wt-metadata-refresh # Cron script to refresh Bazel IDE metadata ├── completion/ # Shell completions for wt-* scripts │ ├── wt.zsh │ └── wt.bash @@ -352,12 +353,12 @@ wt/ You can also run the underlying scripts directly: ```bash -wt-add, wt-switch, wt-remove, wt-list, wt-cd, wt-ijwb-export, wt-ijwb-import +wt-add, wt-switch, wt-remove, wt-list, wt-cd, wt-metadata-export, wt-metadata-import ``` These are located in `bin/` and work identically to the `wt` subcommands. -The `lib/wt-ijwb-refresh` script is designed for cron jobs and can be run directly from its location. +The `lib/wt-metadata-refresh` script is designed for cron jobs and can be run directly from its location. ## Project Resources diff --git a/bin/wt-metadata-import b/bin/wt-metadata-import index 3c60e9a..27df155 100755 --- a/bin/wt-metadata-import +++ b/bin/wt-metadata-import @@ -203,43 +203,43 @@ import_pattern() { # Find all matching directories (symlinks or real directories) under the source directory # Use -L to follow symlinks (source path might be a symlink) - find -L "$SOURCE_DIR" \( -type l -o -type d \) -name "$pattern" -print0 2>/dev/null | - while IFS= read -r -d '' META_SRC; do - # Parent dir relative to SOURCE_DIR - PARENT_SRC="$(dirname "$META_SRC")" - - # Handle pattern at root (PARENT_SRC == SOURCE_DIR) - if [[ "$PARENT_SRC" == "$SOURCE_DIR" ]]; then - REL_PATH="" - else - REL_PATH="${PARENT_SRC#"$SOURCE_DIR"/}" - fi - - # Corresponding target parent - if [[ -z "$REL_PATH" ]]; then - TARGET_PARENT="$TARGET_DIR" - else - TARGET_PARENT="$TARGET_DIR/$REL_PATH" - fi - TARGET_META="$TARGET_PARENT/$pattern" - - echo " Found source: $META_SRC" - echo " -> Installing to: $TARGET_META" - - mkdir -p "$TARGET_PARENT" - - if [[ -e "$TARGET_META" || -L "$TARGET_META" ]]; then - rm -rf "$TARGET_META" - fi - - if ! copy_real_metadata "$META_SRC" "$TARGET_META"; then - echo " Skipping due to broken symlink" - echo " Removing broken symlink from vault: $META_SRC" - rm -f "$META_SRC" - fi - - count=$((count + 1)) - done + # Note: Use process substitution (< <(...)) instead of pipe to avoid subshell variable scope issues + while IFS= read -r -d '' META_SRC; do + # Parent dir relative to SOURCE_DIR + PARENT_SRC="$(dirname "$META_SRC")" + + # Handle pattern at root (PARENT_SRC == SOURCE_DIR) + if [[ "$PARENT_SRC" == "$SOURCE_DIR" ]]; then + REL_PATH="" + else + REL_PATH="${PARENT_SRC#"$SOURCE_DIR"/}" + fi + + # Corresponding target parent + if [[ -z "$REL_PATH" ]]; then + TARGET_PARENT="$TARGET_DIR" + else + TARGET_PARENT="$TARGET_DIR/$REL_PATH" + fi + TARGET_META="$TARGET_PARENT/$pattern" + + echo " Found source: $META_SRC" + echo " -> Installing to: $TARGET_META" + + mkdir -p "$TARGET_PARENT" + + if [[ -e "$TARGET_META" || -L "$TARGET_META" ]]; then + rm -rf "$TARGET_META" + fi + + if ! copy_real_metadata "$META_SRC" "$TARGET_META"; then + echo " Skipping due to broken symlink" + echo " Removing broken symlink from vault: $META_SRC" + rm -f "$META_SRC" + fi + + count=$((count + 1)) + done < <(find -L "$SOURCE_DIR" \( -type l -o -type d \) -name "$pattern" -print0 2>/dev/null) if [[ $count -eq 0 ]]; then echo " (no '$pattern' directories found in vault)" diff --git a/install.sh b/install.sh index e5e2d5c..45a1b7d 100755 --- a/install.sh +++ b/install.sh @@ -338,8 +338,8 @@ install_toolkit() { # Make bin scripts executable chmod +x "$INSTALL_DIR"/bin/wt-* - # Make lib/wt-ijwb-refresh executable (for cron job) - chmod +x "$INSTALL_DIR"/lib/wt-ijwb-refresh + # Make lib/wt-metadata-refresh executable (for cron job) + chmod +x "$INSTALL_DIR"/lib/wt-metadata-refresh echo " ✓ Installed to $INSTALL_DIR" } @@ -579,24 +579,24 @@ migrate_repo() { fi } -# Set up cron job for .ijwb refresh +# Set up cron job for metadata refresh setup_cron_job() { - local refresh_script="$INSTALL_DIR/lib/wt-ijwb-refresh" + local refresh_script="$INSTALL_DIR/lib/wt-metadata-refresh" local log_dir="$HOME/.wt/logs" - local log_file="$log_dir/ijwb-refresh.log" + local log_file="$log_dir/metadata-refresh.log" local cron_entry="0 2 * * * /bin/zsh -lc '$refresh_script' >> $log_file 2>&1" echo "════════════════════════════════════════════════════════════════════════════════" - echo " Nightly .ijwb Refresh Cron Job" + echo " Nightly Metadata Refresh Cron Job" echo "════════════════════════════════════════════════════════════════════════════════" echo - echo "When most development happens in worktrees, the .ijwb metadata in the main" + echo "When most development happens in worktrees, the Bazel IDE metadata in the main" echo "repository can become stale (missing new Bazel targets)." echo echo "This cron job will:" echo " 1. Run nightly at 2am" - echo " 2. Use 'bazel query' to regenerate targets files in each .ijwb directory" - echo " 3. Re-export refreshed metadata to the vault" + echo " 2. Use 'bazel query' to regenerate targets files in Bazel IDE directories" + echo " 3. Re-export all metadata to the vault" echo echo "Cron entry:" echo " $cron_entry" @@ -614,7 +614,7 @@ setup_cron_job() { echo " ✓ Created log directory: $log_dir" # Check if cron job already exists - if crontab -l 2>/dev/null | grep -qF "wt-ijwb-refresh"; then + if crontab -l 2>/dev/null | grep -qF "wt-metadata-refresh"; then echo " Cron job already exists. Skipping." else # Add cron job diff --git a/lib/wt-ijwb-refresh b/lib/wt-metadata-refresh similarity index 70% rename from lib/wt-ijwb-refresh rename to lib/wt-metadata-refresh index e1c0dbc..b8482ff 100755 --- a/lib/wt-ijwb-refresh +++ b/lib/wt-metadata-refresh @@ -1,34 +1,35 @@ #!/usr/bin/env bash # -# wt-ijwb-refresh — Refresh .ijwb directories and re-export to vault -# =================================================================== +# wt-metadata-refresh — Refresh Bazel IDE metadata and re-export to vault +# ======================================================================== # -# This script refreshes stale `.ijwb` directories in the main repository by -# regenerating their `targets/targets-*` files using bazel query, then -# re-exports the refreshed metadata to the vault. +# This script refreshes stale Bazel IDE directories (.ijwb, .aswb, .clwb) in +# the main repository by regenerating their `targets/targets-*` files using +# bazel query, then re-exports all metadata to the vault. # # This is useful for: -# - Keeping .ijwb metadata current when most work is done in worktrees +# - Keeping Bazel IDE metadata current when most work is done in worktrees # - Running as a nightly cron job to prevent metadata staleness -# - Ensuring new Bazel targets are included in IntelliJ project metadata +# - Ensuring new Bazel targets are included in IDE project metadata # # Behavior: # --------- -# 1. Finds all `.ijwb` directories in $WT_MAIN_REPO_ROOT -# 2. For each `.ijwb` directory: -# - Determines the project root (parent directory of .ijwb) +# 1. Finds all Bazel IDE directories (.ijwb, .aswb, .clwb) in $WT_MAIN_REPO_ROOT +# that match patterns in WT_METADATA_PATTERNS +# 2. For each Bazel IDE directory: +# - Determines the project root (parent directory) # - Runs `bazel query` to regenerate the targets file # - Preserves the existing targets file hash if present -# 3. Re-exports all .ijwb directories to the vault using wt-ijwb-export +# 3. Re-exports all metadata directories to the vault using wt-metadata-export # # Usage: -# wt-ijwb-refresh # Refresh all .ijwb directories -# wt-ijwb-refresh --dry-run # Show what would be done without making changes -# wt-ijwb-refresh --no-export # Refresh targets files but skip re-export step +# wt-metadata-refresh # Refresh all Bazel IDE directories +# wt-metadata-refresh --dry-run # Show what would be done without making changes +# wt-metadata-refresh --no-export # Refresh targets files but skip re-export step # # Cron Setup: # # Run nightly at 2am with full shell environment, log output -# 0 2 * * * /bin/zsh -lc '~/.wt/lib/wt-ijwb-refresh' >> ~/.wt/logs/ijwb-refresh.log 2>&1 +# 0 2 * * * /bin/zsh -lc '~/.wt/lib/wt-metadata-refresh' >> ~/.wt/logs/metadata-refresh.log 2>&1 # # Note: # The bazel query approach may include more targets than IntelliJ's native sync @@ -58,6 +59,9 @@ fi # Configuration # ═══════════════════════════════════════════════════════════════════════════════ +# Bazel IDE patterns that can be refreshed (have targets directory structure) +BAZEL_IDE_PATTERNS=".ijwb .aswb .clwb" + # Log directory for cron runs LOG_DIR="${WT_LOG_DIR:-$HOME/.wt/logs}" @@ -83,21 +87,24 @@ usage() { cat <> $LOG_DIR/ijwb-refresh.log 2>&1 + 0 2 * * * $SCRIPT_DIR/wt-metadata-refresh >> $LOG_DIR/metadata-refresh.log 2>&1 EOF } @@ -107,24 +114,55 @@ log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" } -# Find all .ijwb directories in the main repo -find_all_ijwb_dirs() { - # Use -maxdepth 5 for better performance since most .ijwb dirs are at service level - find "$WT_MAIN_REPO_ROOT" -maxdepth 5 -type d -name '.ijwb' 2>/dev/null +# Check if a pattern is a Bazel IDE pattern (refreshable) +is_bazel_pattern() { + local pattern="$1" + for bazel_pattern in $BAZEL_IDE_PATTERNS; do + if [[ "$pattern" == "$bazel_pattern" ]]; then + return 0 + fi + done + return 1 +} + +# Get Bazel patterns that are configured in WT_METADATA_PATTERNS +get_configured_bazel_patterns() { + local configured_bazel="" + for pattern in ${WT_METADATA_PATTERNS:-}; do + if is_bazel_pattern "$pattern"; then + configured_bazel="$configured_bazel $pattern" + fi + done + echo "$configured_bazel" | xargs # trim whitespace +} + +# Find all Bazel IDE directories in the main repo for configured patterns +find_all_bazel_dirs() { + local patterns + patterns="$(get_configured_bazel_patterns)" + + if [[ -z "$patterns" ]]; then + return + fi + + for pattern in $patterns; do + # Use -maxdepth 5 for better performance since most dirs are at service level + find "$WT_MAIN_REPO_ROOT" -maxdepth 5 -type d -name "$pattern" 2>/dev/null + done } -# Get the project root for an .ijwb directory (parent directory) +# Get the project root for a Bazel IDE directory (parent directory) get_project_root() { - local ijwb_dir="$1" - dirname "$ijwb_dir" + local dir="$1" + dirname "$dir" } -# Get the existing targets file in an .ijwb directory +# Get the existing targets file in a Bazel IDE directory # Returns the path to the targets file, or empty if none exists get_existing_targets_file() { - local ijwb_dir="$1" - local targets_dir="$ijwb_dir/targets" - + local dir="$1" + local targets_dir="$dir/targets" + if [[ -d "$targets_dir" ]]; then # Find the first targets-* file find "$targets_dir" -maxdepth 1 -name 'targets-*' -type f 2>/dev/null | head -n1 @@ -141,11 +179,11 @@ generate_targets_hash() { # Returns: space-separated list of directories, or empty string if none found parse_bazelproject_directories() { local bazelproject_file="$1" - + if [[ ! -f "$bazelproject_file" ]]; then return fi - + # Extract directories section (lines after "directories:" until next section or EOF) # A section starts with a non-indented line ending with ":" awk ' @@ -161,11 +199,11 @@ parse_bazelproject_directories() { build_query_expression() { local first=true local query="" - + while IFS= read -r dir; do # Skip empty lines [[ -z "$dir" ]] && continue - + if [[ "$first" == "true" ]]; then query="//${dir}/..." first=false @@ -173,53 +211,56 @@ build_query_expression() { query="$query + //${dir}/..." fi done - + echo "$query" } -# Refresh the targets file for a single .ijwb directory using bazel query -# Args: $1 = .ijwb directory path +# Refresh the targets file for a single Bazel IDE directory using bazel query +# Args: $1 = Bazel IDE directory path (e.g., .ijwb, .aswb, .clwb) # Returns: 0 on success, 1 on failure refresh_targets_file() { - local ijwb_dir="$1" + local bazel_dir="$1" local project_root - project_root="$(get_project_root "$ijwb_dir")" - + project_root="$(get_project_root "$bazel_dir")" + local project_name project_name="$(basename "$project_root")" - - log "Refreshing: $project_name" - + + local pattern_name + pattern_name="$(basename "$bazel_dir")" + + log "Refreshing: $project_name ($pattern_name)" + # Check for .bazelproject file to determine query scope - local bazelproject_file="$ijwb_dir/.bazelproject" + local bazelproject_file="$bazel_dir/.bazelproject" local query_expr="" - + if [[ -f "$bazelproject_file" ]]; then local directories directories="$(parse_bazelproject_directories "$bazelproject_file")" - + if [[ -n "$directories" ]]; then query_expr="$(echo "$directories" | build_query_expression)" log " Using directories from .bazelproject: $(echo "$directories" | tr '\n' ' ')" fi fi - + # Fallback to //... if no .bazelproject or no directories found if [[ -z "$query_expr" ]]; then query_expr="//..." log " No .bazelproject found, using //..." fi - + # Ensure targets directory exists - local targets_dir="$ijwb_dir/targets" + local targets_dir="$bazel_dir/targets" if [[ "$DRY_RUN" == "false" ]]; then mkdir -p "$targets_dir" fi - + # Determine targets file path local existing_targets_file - existing_targets_file="$(get_existing_targets_file "$ijwb_dir")" - + existing_targets_file="$(get_existing_targets_file "$bazel_dir")" + local targets_file if [[ -n "$existing_targets_file" ]]; then # Preserve existing filename (hash) @@ -228,73 +269,85 @@ refresh_targets_file() { # Generate new filename targets_file="$targets_dir/$(generate_targets_hash)" fi - + # Full bazel query command local full_query="kind('.*', $query_expr)" - + if [[ "$DRY_RUN" == "true" ]]; then log " [dry-run] Would run: bazel query \"$full_query\" from $WT_MAIN_REPO_ROOT" log " [dry-run] Would write to: $targets_file" return 0 fi - + # Run bazel query from the MAIN REPO ROOT (not project root) # This is important because .bazelproject directories are relative to repo root local query_output local query_exit_code=0 - + query_output=$( cd "$WT_MAIN_REPO_ROOT" 2>/dev/null && \ bazel query "$full_query" --output=label 2>/dev/null | sort ) || query_exit_code=$? - + if [[ $query_exit_code -ne 0 ]]; then warn " Bazel query failed for $project_name (exit code: $query_exit_code)" return 1 fi - + if [[ -z "$query_output" ]]; then warn " Bazel query returned empty results for $project_name" return 1 fi - + # Write output to targets file echo "$query_output" > "$targets_file" - + local target_count target_count=$(echo "$query_output" | wc -l | tr -d ' ') - log " ✓ Updated $targets_file ($target_count targets)" - + log " Updated $targets_file ($target_count targets)" + return 0 } -# Refresh all .ijwb directories -refresh_all_ijwb() { - log "Starting .ijwb refresh" +# Refresh all Bazel IDE directories +refresh_all_bazel_metadata() { + log "Starting metadata refresh" log "Main repo: $WT_MAIN_REPO_ROOT" + + local configured_bazel + configured_bazel="$(get_configured_bazel_patterns)" + + if [[ -z "$configured_bazel" ]]; then + log "No Bazel IDE patterns configured in WT_METADATA_PATTERNS" + log "Bazel patterns are: $BAZEL_IDE_PATTERNS" + log "Configured patterns: ${WT_METADATA_PATTERNS:-"(none)"}" + return 0 + fi + + log "Refreshable patterns: $configured_bazel" log "" - - local ijwb_dirs - ijwb_dirs=$(find_all_ijwb_dirs) - - if [[ -z "$ijwb_dirs" ]]; then - log "No .ijwb directories found in $WT_MAIN_REPO_ROOT" + + local bazel_dirs + bazel_dirs=$(find_all_bazel_dirs) + + if [[ -z "$bazel_dirs" ]]; then + log "No Bazel IDE directories found in $WT_MAIN_REPO_ROOT" return 0 fi - + local total_count - total_count=$(echo "$ijwb_dirs" | wc -l | tr -d ' ') - log "Found $total_count .ijwb directories" + total_count=$(echo "$bazel_dirs" | wc -l | tr -d ' ') + log "Found $total_count Bazel IDE directories" log "" - - while IFS= read -r ijwb_dir; do - if refresh_targets_file "$ijwb_dir"; then + + while IFS= read -r bazel_dir; do + if refresh_targets_file "$bazel_dir"; then REFRESH_COUNT=$((REFRESH_COUNT + 1)) else ERROR_COUNT=$((ERROR_COUNT + 1)) fi - done <<< "$ijwb_dirs" - + done <<< "$bazel_dirs" + log "" log "Refresh complete: $REFRESH_COUNT succeeded, $ERROR_COUNT failed" } @@ -307,7 +360,8 @@ do_export() { fi log "" - log "Re-exporting metadata directories to vault..." + log "Re-exporting all metadata directories to vault..." + log "Patterns: ${WT_METADATA_PATTERNS:-"(none)"}" # Find wt-metadata-export: try bin directory first, then PATH local metadata_export="" @@ -362,13 +416,13 @@ main() { ;; esac done - + # Validate main repo exists if [[ ! -d "$WT_MAIN_REPO_ROOT" ]]; then error "WT_MAIN_REPO_ROOT does not exist: $WT_MAIN_REPO_ROOT" exit 1 fi - + # Verify bazel is available (critical for cron environments) if ! command -v bazel >/dev/null 2>&1; then error "bazel not found in PATH" @@ -376,39 +430,39 @@ main() { error "If running from cron, ensure bazel is in a standard location or update this script" exit 1 fi - + # Validate vault exists (for export step) if [[ "$NO_EXPORT" == "false" && ! -d "$WT_IDEA_FILES_BASE" ]]; then error "WT_IDEA_FILES_BASE does not exist: $WT_IDEA_FILES_BASE" exit 1 fi - + # Ensure log directory exists ensure_log_dir - + log "════════════════════════════════════════════════════════════════════" - log " wt-ijwb-refresh" + log " wt-metadata-refresh" log "════════════════════════════════════════════════════════════════════" - + if [[ "$DRY_RUN" == "true" ]]; then log "[DRY RUN MODE - no changes will be made]" fi - + log "" - - # Refresh all .ijwb directories - refresh_all_ijwb - - # Re-export to vault + + # Refresh all Bazel IDE directories + refresh_all_bazel_metadata + + # Re-export to vault (exports ALL metadata patterns, not just Bazel ones) if [[ $REFRESH_COUNT -gt 0 || "$DRY_RUN" == "true" ]]; then do_export else - log "No .ijwb directories refreshed; skipping export" + log "No Bazel IDE directories refreshed; skipping export" fi - + log "" log "════════════════════════════════════════════════════════════════════" - + # Determine exit code if [[ $ERROR_COUNT -eq 0 ]]; then log "All done!" From 11ecb8016c3fa1e7cc3149b9f8372e72c15b2686 Mon Sep 17 00:00:00 2001 From: Guodong Zhu Date: Sun, 8 Feb 2026 21:48:59 -0500 Subject: [PATCH 07/11] cleanup --- bin/wt-switch | 2 +- install.sh | 20 ++++++++++++++++---- lib/wt-choose | 4 ++-- lib/wt-common | 4 ++-- lib/wt-completion | 19 ++++++++++--------- lib/wt-help | 34 ++++++++++++++++++++-------------- lib/wt-metadata-refresh | 4 ++-- 7 files changed, 53 insertions(+), 34 deletions(-) diff --git a/bin/wt-switch b/bin/wt-switch index 2f530f1..7ad155f 100755 --- a/bin/wt-switch +++ b/bin/wt-switch @@ -30,7 +30,7 @@ # ------ # - The symlink location is configured via WT_ACTIVE_WORKTREE in wt-common. # - This script is intended to be used together with the `wt-add`, `wt-choose`, -# `wt-ijwb-import`, and `wt-ijwb-export` tools as part of the workflow suite. +# `wt-metadata-import`, and `wt-metadata-export` tools as part of the workflow suite. # set -euo pipefail diff --git a/install.sh b/install.sh index 45a1b7d..8f3eac2 100755 --- a/install.sh +++ b/install.sh @@ -278,7 +278,8 @@ select_metadata_patterns() { # Check if already selected local found=0 local new_selected=() - for s in "${selected[@]}"; do + # Use ${arr[@]+"${arr[@]}"} pattern for empty array safety in older bash + for s in ${selected[@]+"${selected[@]}"}; do if [[ "$s" == "$pattern" ]]; then found=1 else @@ -286,7 +287,12 @@ select_metadata_patterns() { fi done if ((found)); then - selected=("${new_selected[@]}") + # Assign empty or populated array safely + if [[ ${#new_selected[@]} -gt 0 ]]; then + selected=("${new_selected[@]}") + else + selected=() + fi else selected+=("$pattern") fi @@ -302,7 +308,8 @@ select_metadata_patterns() { local desc desc=$(get_pattern_description "$pattern") local mark=" " - for s in "${selected[@]}"; do + # Use ${arr[@]+"${arr[@]}"} pattern for empty array safety in older bash + for s in ${selected[@]+"${selected[@]}"}; do [[ "$s" == "$pattern" ]] && mark="x" done echo " $i) [$mark] $pattern - $desc" @@ -312,7 +319,12 @@ select_metadata_patterns() { echo "Enter numbers to toggle, 'a' for all, 'n' for none, or Enter to confirm:" done - WT_METADATA_PATTERNS="${selected[*]}" + # Safely handle empty array (${arr[*]} on empty array is fine, but be explicit) + if [[ ${#selected[@]} -gt 0 ]]; then + WT_METADATA_PATTERNS="${selected[*]}" + else + WT_METADATA_PATTERNS="" + fi echo if [[ -n "$WT_METADATA_PATTERNS" ]]; then diff --git a/lib/wt-choose b/lib/wt-choose index 44fffe3..2f9df32 100644 --- a/lib/wt-choose +++ b/lib/wt-choose @@ -25,8 +25,8 @@ # Intended Use: # ------------- # These helper functions are used by: -# - wt-ijwb-import (imports IntelliJ project metadata into a worktree) -# - wt-switch (updates a symlink to point to a chosen worktree) +# - wt-metadata-import (imports project metadata into a worktree) +# - wt-switch (updates a symlink to point to a chosen worktree) # # Guarantees: # ----------- diff --git a/lib/wt-common b/lib/wt-common index 7ff0914..7fa14df 100644 --- a/lib/wt-common +++ b/lib/wt-common @@ -7,8 +7,8 @@ # - wt-add # - wt-switch # - wt-choose -# - wt-ijwb-import -# - wt-ijwb-export +# - wt-metadata-import +# - wt-metadata-export # - wt-remove # diff --git a/lib/wt-completion b/lib/wt-completion index 92c8a9b..57d0b53 100644 --- a/lib/wt-completion +++ b/lib/wt-completion @@ -21,11 +21,13 @@ if [[ -n "$ZSH_VERSION" ]]; then 'remove:Remove a worktree' 'list:List all worktrees with status' 'cd:Change directory to a worktree' - 'ijwb-export:Export .ijwb metadata to vault' - 'ijwb-import:Import .ijwb metadata into worktree' + 'metadata-export:Export project metadata to vault' + 'metadata-import:Import project metadata into worktree' + 'ijwb-export:Alias for metadata-export' + 'ijwb-import:Alias for metadata-import' 'help:Show help message' ) - + if (( CURRENT == 2 )); then _describe 'command' commands else @@ -44,7 +46,7 @@ if [[ -n "$ZSH_VERSION" ]]; then _describe 'worktree' worktrees fi ;; - ijwb-import) + metadata-import|ijwb-import) # First arg: source directory or target worktree # Second arg: target worktree if (( CURRENT == 3 )); then @@ -64,7 +66,7 @@ if [[ -n "$ZSH_VERSION" ]]; then fi fi ;; - ijwb-export) + metadata-export|ijwb-export) # Complete directories _files -/ ;; @@ -80,7 +82,7 @@ if [[ -n "$BASH_VERSION" ]]; then local cur commands COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" - commands="add switch remove list cd ijwb-export ijwb-import help" + commands="add switch remove list cd metadata-export metadata-import ijwb-export ijwb-import help" if [[ ${COMP_CWORD} -eq 1 ]]; then COMPREPLY=($(compgen -W "$commands" -- "$cur")) @@ -98,7 +100,7 @@ if [[ -n "$BASH_VERSION" ]]; then COMPREPLY=($(compgen -W "$worktrees" -- "$cur")) fi ;; - ijwb-import) + metadata-import|ijwb-import) # First arg: source directory or target worktree # Second arg: target worktree if [[ ${COMP_CWORD} -eq 2 ]]; then @@ -118,7 +120,7 @@ if [[ -n "$BASH_VERSION" ]]; then fi fi ;; - ijwb-export) + metadata-export|ijwb-export) # Complete directories COMPREPLY=($(compgen -d -- "$cur")) ;; @@ -127,4 +129,3 @@ if [[ -n "$BASH_VERSION" ]]; then } complete -F _wt_completion_bash wt fi - diff --git a/lib/wt-help b/lib/wt-help index 689e3e4..ae07566 100644 --- a/lib/wt-help +++ b/lib/wt-help @@ -21,18 +21,22 @@ Worktree Commands: remove [worktree] Remove a worktree - interactive worktree selection if omitted - use --merged to remove all merged-branch worktrees - list List all worktrees with status + list [-v] List all worktrees with status + - use -v for dirty/ahead/behind indicators cd [worktree] Change directory to a worktree - interactive worktree selection if omitted -IntelliJ Metadata Commands: - ijwb-export [src] [dst] Export .ijwb directories from source to metadata vault - - src: defaults to $WT_MAIN_REPO_ROOT - - dst: defaults to $WT_IDEA_FILES_BASE +Metadata Commands: + metadata-export [src] [dst] Export project metadata to vault + - src: defaults to $WT_MAIN_REPO_ROOT + - dst: defaults to $WT_IDEA_FILES_BASE - ijwb-import [src] [dst] Import .ijwb metadata into a target worktree - - src: defaults to $WT_IDEA_FILES_BASE - - dst: interactive worktree selection if omitted + metadata-import [src] [dst] Import project metadata into a worktree + - src: defaults to $WT_IDEA_FILES_BASE + - dst: interactive worktree selection if omitted + + ijwb-export Alias for metadata-export (backward compat) + ijwb-import Alias for metadata-import (backward compat) Other: help Show this help message @@ -42,14 +46,14 @@ Examples: wt switch # Interactive: pick worktree to link wt cd # Interactive: cd to selected worktree wt list # Show all worktrees (fast) + wt list -v # Show worktrees with dirty/ahead/behind wt remove # Interactive: remove a worktree wt remove --merged # Remove all worktrees with merged branches - wt ijwb-export # Export using default paths - wt ijwb-export ~/repo ~/vault # Export from specific source to specific vault - wt ijwb-import # Interactive: pick worktree to import into - wt ijwb-import ~/worktree # Import into specific worktree - wt ijwb-import ~/vault ~/wt # Import from specific vault into worktree + wt metadata-export # Export using default paths + wt metadata-export ~/repo ~/vault + wt metadata-import # Interactive: pick worktree to import into + wt metadata-import ~/worktree # Import into specific worktree EOF cat </dev/null + # Use -L to follow symlinks, -maxdepth 5 for better performance + find -L "$WT_MAIN_REPO_ROOT" -maxdepth 5 -type d -name "$pattern" 2>/dev/null done } From 47cf290dcbcd787e76e0e023691fd6fe15fc95a6 Mon Sep 17 00:00:00 2001 From: Guodong Zhu Date: Sun, 8 Feb 2026 22:01:19 -0500 Subject: [PATCH 08/11] cleanup --- README.md | 42 +++++++++++++++++++++++------------------- bin/wt-metadata-export | 7 ++++++- install.sh | 11 +++++++++-- lib/wt-help | 12 ++++++------ 4 files changed, 44 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index c0b421a..30626ef 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Git worktrees let you work on multiple branches in parallel, but IntelliJ treats This toolkit makes IntelliJ context switching **instant** by: - **Symlink trick**: IntelliJ always opens the same path; switching worktrees looks like a branch checkout → incremental refresh in seconds, not minutes -- **Metadata vault**: `.ijwb` directories are stored externally and automatically installed into every new worktree—no manual Bazel import needed +- **Metadata vault**: IDE project metadata (`.ijwb`, `.idea`, `.vscode`, etc.) is stored externally and automatically installed into every new worktree—no manual IDE setup needed - **Safe worktree management**: Automatic stash/restore, branch creation, and cleanup of merged branches - **Parallel development at scale**: Works for humans and AI agents alike @@ -36,8 +36,8 @@ The installer will: 3. Prompt for workspace paths (main repo, worktrees, metadata vault) 4. Create required directories 5. Optionally migrate existing repo to worktree structure -6. Optionally export `.ijwb` metadata to the vault -7. Optionally set up a nightly cron job to refresh `.ijwb` metadata +6. Optionally export project metadata to the vault +7. Optionally set up a nightly cron job to refresh Bazel IDE metadata ## Workflow @@ -50,26 +50,26 @@ The directory structure expected (controlled by environment variables, can be ov ├── java -> java-master # Symlink (IntelliJ opens this) ├── java-master/ # Main repository ├── java-worktrees/ # Worktrees go here -└── idea-project-files/ # .ijwb metadata vault +└── idea-project-files/ # Project metadata vault ``` ### Full Workflow Diagram ``` ┌─────────────────────────────────────────────┐ - │ External IntelliJ Metadata Vault │ + │ External Project Metadata Vault │ │ ~/Development/idea-project-files │ - │ (canonical .ijwb directories) │ + │ (IDE configs: .ijwb, .idea, etc.) │ └──────────▲───────────────┬──────────────────┘ - │ │ │ │ - ┌────wt ijwb-export───┘ └───wt ijwb-import──┐ - │ │ -┌──────────┴───────────────────────┐ ┌───────────▼────────────────────────┐ + │ │ + ┌──wt metadata-export─┘ └──wt metadata-import─┐ + │ │ +┌──────────┴───────────────────────┐ ┌─────────────▼──────────────────────┐ │ Main Repository │ │ Worktrees │ │ ~/Development/java-master │ wt add │ ~/Development/java-worktrees/... │ │ • master branch │ ──────────────────► │ • feature/foo │ -│ • safe stash/pull/restore │ (calls ijwb-import) │ • bugfix/bar │ +│ • safe stash/pull/restore │(calls metadata-imp) │ • bugfix/bar │ │ • never removed │ │ • agent-task-123 │ └───────────────┬──────────────────┘ └─────────┬──────────────────────────┘ │ │ @@ -167,19 +167,23 @@ Safety features: - Always prompts for confirmation if uncommitted changes exist, even with `-y` - `--merged` mode: automatically finds and removes all worktrees whose branches are merged -### Managing IntelliJ Metadata +### Managing Project Metadata ```bash -# Export .ijwb from main repo to vault (run after importing new Bazel projects) -wt ijwb-export +# Export metadata from main repo to vault (run after setting up new IDE projects) +wt metadata-export -# Import .ijwb into a worktree (interactive selection if target omitted) -wt ijwb-import -wt ijwb-import ~/Development/java-worktrees/feature/foo +# Import metadata into a worktree (interactive selection if target omitted) +wt metadata-import +wt metadata-import ~/Development/java-worktrees/feature/foo # Skip confirmation prompts (useful in scripts) -wt ijwb-export -y -wt ijwb-import -y ~/Development/java-worktrees/feature/foo +wt metadata-export -y +wt metadata-import -y ~/Development/java-worktrees/feature/foo + +# Legacy aliases (backward compatibility) +wt ijwb-export # same as wt metadata-export +wt ijwb-import # same as wt metadata-import ``` ### Refreshing Stale Bazel IDE Metadata (Cron Job) diff --git a/bin/wt-metadata-export b/bin/wt-metadata-export index 9072954..38ed78f 100755 --- a/bin/wt-metadata-export +++ b/bin/wt-metadata-export @@ -165,6 +165,11 @@ find_all_metadata_dirs() { done < <(find -L "$SOURCE_DIR" -maxdepth 5 -type d -name "$pattern" 2>/dev/null) done + # No metadata found + if [[ ${#all_paths[@]} -eq 0 ]]; then + return + fi + # Sort paths (shorter paths come first) local sorted_paths sorted_paths=$(printf '%s\n' "${all_paths[@]}" | sort) @@ -195,7 +200,7 @@ echo # Clean up existing symlinks in vault for all patterns for pattern in $WT_METADATA_PATTERNS; do - find -L "$TARGET_DIR" -type l -name "$pattern" -print0 2>/dev/null | while IFS= read -r -d '' OLD_LINK; do + find "$TARGET_DIR" -type l -name "$pattern" -print0 2>/dev/null | while IFS= read -r -d '' OLD_LINK; do rm -f "$OLD_LINK" done done diff --git a/install.sh b/install.sh index 8f3eac2..f9d718a 100755 --- a/install.sh +++ b/install.sh @@ -625,8 +625,15 @@ setup_cron_job() { mkdir -p "$log_dir" echo " ✓ Created log directory: $log_dir" - # Check if cron job already exists - if crontab -l 2>/dev/null | grep -qF "wt-metadata-refresh"; then + # Check for old cron job (wt-ijwb-refresh) and offer to migrate + if crontab -l 2>/dev/null | grep -qF "wt-ijwb-refresh"; then + echo " Found old cron job (wt-ijwb-refresh)." + echo " Replacing with new cron job (wt-metadata-refresh)..." + # Remove old entry and add new one + (crontab -l 2>/dev/null | grep -vF "wt-ijwb-refresh"; echo "$cron_entry") | crontab - + echo " ✓ Cron job migrated to wt-metadata-refresh" + echo " ✓ Logs will be written to: $log_file" + elif crontab -l 2>/dev/null | grep -qF "wt-metadata-refresh"; then echo " Cron job already exists. Skipping." else # Add cron job diff --git a/lib/wt-help b/lib/wt-help index ae07566..61f9f6c 100644 --- a/lib/wt-help +++ b/lib/wt-help @@ -27,13 +27,13 @@ Worktree Commands: - interactive worktree selection if omitted Metadata Commands: - metadata-export [src] [dst] Export project metadata to vault - - src: defaults to $WT_MAIN_REPO_ROOT - - dst: defaults to $WT_IDEA_FILES_BASE + metadata-export [src] [vault] Export project metadata to vault + - src: source repo (default: $WT_MAIN_REPO_ROOT) + - vault: metadata vault (default: $WT_IDEA_FILES_BASE) - metadata-import [src] [dst] Import project metadata into a worktree - - src: defaults to $WT_IDEA_FILES_BASE - - dst: interactive worktree selection if omitted + metadata-import [vault] [wt] Import project metadata into a worktree + - vault: metadata vault (default: $WT_IDEA_FILES_BASE) + - wt: target worktree (interactive if omitted) ijwb-export Alias for metadata-export (backward compat) ijwb-import Alias for metadata-import (backward compat) From 08463294c83eb6d09c4f274e1a097c0bbeb7ddd6 Mon Sep 17 00:00:00 2001 From: Guodong Zhu Date: Sun, 8 Feb 2026 22:21:42 -0500 Subject: [PATCH 09/11] cleanup --- README.md | 4 ---- install.sh | 4 +--- lib/wt-completion | 12 +++++------- lib/wt-help | 7 ++----- wt.sh | 3 --- 5 files changed, 8 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 30626ef..617f9a2 100644 --- a/README.md +++ b/README.md @@ -180,10 +180,6 @@ wt metadata-import ~/Development/java-worktrees/feature/foo # Skip confirmation prompts (useful in scripts) wt metadata-export -y wt metadata-import -y ~/Development/java-worktrees/feature/foo - -# Legacy aliases (backward compatibility) -wt ijwb-export # same as wt metadata-export -wt ijwb-import # same as wt metadata-import ``` ### Refreshing Stale Bazel IDE Metadata (Cron Job) diff --git a/install.sh b/install.sh index f9d718a..0b033f4 100755 --- a/install.sh +++ b/install.sh @@ -174,7 +174,7 @@ detect_metadata_patterns() { pattern="$(basename "$path")" # Add to patterns if not already present local found=false - for p in "${patterns[@]}"; do + for p in ${patterns[@]+"${patterns[@]}"}; do [[ "$p" == "$pattern" ]] && found=true && break done [[ "$found" == "false" ]] && patterns+=("$pattern") @@ -278,7 +278,6 @@ select_metadata_patterns() { # Check if already selected local found=0 local new_selected=() - # Use ${arr[@]+"${arr[@]}"} pattern for empty array safety in older bash for s in ${selected[@]+"${selected[@]}"}; do if [[ "$s" == "$pattern" ]]; then found=1 @@ -308,7 +307,6 @@ select_metadata_patterns() { local desc desc=$(get_pattern_description "$pattern") local mark=" " - # Use ${arr[@]+"${arr[@]}"} pattern for empty array safety in older bash for s in ${selected[@]+"${selected[@]}"}; do [[ "$s" == "$pattern" ]] && mark="x" done diff --git a/lib/wt-completion b/lib/wt-completion index 57d0b53..505defa 100644 --- a/lib/wt-completion +++ b/lib/wt-completion @@ -23,8 +23,6 @@ if [[ -n "$ZSH_VERSION" ]]; then 'cd:Change directory to a worktree' 'metadata-export:Export project metadata to vault' 'metadata-import:Import project metadata into worktree' - 'ijwb-export:Alias for metadata-export' - 'ijwb-import:Alias for metadata-import' 'help:Show help message' ) @@ -46,7 +44,7 @@ if [[ -n "$ZSH_VERSION" ]]; then _describe 'worktree' worktrees fi ;; - metadata-import|ijwb-import) + metadata-import) # First arg: source directory or target worktree # Second arg: target worktree if (( CURRENT == 3 )); then @@ -66,7 +64,7 @@ if [[ -n "$ZSH_VERSION" ]]; then fi fi ;; - metadata-export|ijwb-export) + metadata-export) # Complete directories _files -/ ;; @@ -82,7 +80,7 @@ if [[ -n "$BASH_VERSION" ]]; then local cur commands COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" - commands="add switch remove list cd metadata-export metadata-import ijwb-export ijwb-import help" + commands="add switch remove list cd metadata-export metadata-import help" if [[ ${COMP_CWORD} -eq 1 ]]; then COMPREPLY=($(compgen -W "$commands" -- "$cur")) @@ -100,7 +98,7 @@ if [[ -n "$BASH_VERSION" ]]; then COMPREPLY=($(compgen -W "$worktrees" -- "$cur")) fi ;; - metadata-import|ijwb-import) + metadata-import) # First arg: source directory or target worktree # Second arg: target worktree if [[ ${COMP_CWORD} -eq 2 ]]; then @@ -120,7 +118,7 @@ if [[ -n "$BASH_VERSION" ]]; then fi fi ;; - metadata-export|ijwb-export) + metadata-export) # Complete directories COMPREPLY=($(compgen -d -- "$cur")) ;; diff --git a/lib/wt-help b/lib/wt-help index 61f9f6c..a62868d 100644 --- a/lib/wt-help +++ b/lib/wt-help @@ -31,12 +31,9 @@ Metadata Commands: - src: source repo (default: $WT_MAIN_REPO_ROOT) - vault: metadata vault (default: $WT_IDEA_FILES_BASE) - metadata-import [vault] [wt] Import project metadata into a worktree + metadata-import [wt] Import project metadata into a worktree + metadata-import [vault] [wt] - wt: target worktree (interactive if omitted) - vault: metadata vault (default: $WT_IDEA_FILES_BASE) - - wt: target worktree (interactive if omitted) - - ijwb-export Alias for metadata-export (backward compat) - ijwb-import Alias for metadata-import (backward compat) Other: help Show this help message diff --git a/wt.sh b/wt.sh index 417d96d..9dd61e8 100755 --- a/wt.sh +++ b/wt.sh @@ -126,9 +126,6 @@ wt() { list) _wt_run wt-list "$@" ;; metadata-export) _wt_run wt-metadata-export "$@" ;; metadata-import) _wt_run wt-metadata-import "$@" ;; - # Legacy aliases (kept for backward compatibility) - ijwb-export) _wt_run wt-metadata-export "$@" ;; - ijwb-import) _wt_run wt-metadata-import "$@" ;; cd) __wt_do_cd "$@" ;; help|--help|-h|"") wt_show_help # helper for showing help, defined in wt-help library From 47682981c23140f4024326047318d7d431b16c5d Mon Sep 17 00:00:00 2001 From: Guodong Zhu Date: Sun, 8 Feb 2026 22:24:27 -0500 Subject: [PATCH 10/11] cleanup --- README.md | 4 ++-- bin/wt-remove | 2 +- install.sh | 2 +- lib/wt-common | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 617f9a2..76a2ef3 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ When creating with `-b`, the script: 1. Stashes uncommitted changes 2. Switches to master, pulls latest 3. Creates branch + worktree -4. Imports .ijwb metadata +4. Imports project metadata from vault 5. Restores original state ### Switching Worktrees @@ -270,7 +270,7 @@ export WT_WORKTREES_BASE="$HOME/Development/java-worktrees" ### WT_IDEA_FILES_BASE -Canonical metadata vault storing `.ijwb` directories. +Canonical metadata vault storing project metadata (IDE configs, etc.). **Default:** `~/Development/idea-project-files` diff --git a/bin/wt-remove b/bin/wt-remove index 58f2716..6ec0afe 100755 --- a/bin/wt-remove +++ b/bin/wt-remove @@ -17,7 +17,7 @@ # 3. Shows confirmation prompt before removing. # # 4. Uses `git worktree remove --force` because worktrees contain -# copied .ijwb metadata that git considers "untracked changes". +# copied project metadata that git considers "untracked changes". # # 5. Checks if the worktree being removed is the currently linked worktree # and warns the user. diff --git a/install.sh b/install.sh index 0b033f4..3d4cda8 100755 --- a/install.sh +++ b/install.sh @@ -10,7 +10,7 @@ # 4. Creates required directories # 5. Optionally migrates existing repo to worktree structure # 6. Optionally syncs project metadata to the vault -# 7. Optionally sets up nightly cron job to refresh .ijwb metadata +# 7. Optionally sets up nightly cron job to refresh Bazel IDE metadata # set -euo pipefail diff --git a/lib/wt-common b/lib/wt-common index 7fa14df..a508e1a 100644 --- a/lib/wt-common +++ b/lib/wt-common @@ -19,7 +19,7 @@ # Base directory where new worktrees will be created by default : "${WT_WORKTREES_BASE:="$HOME/.wt/repos/repo/worktrees"}" -# Base directory for IntelliJ / Bazel project metadata (e.g. canonical .ijwb) +# Base directory for project metadata vault (IDE configs: .ijwb, .idea, .vscode, etc.) : "${WT_IDEA_FILES_BASE:="$HOME/.wt/repos/repo/idea-files"}" # Symlink path that points to the currently active worktree (for IDE integration) From 3f20581cf52977c760d662394f4db2ebcdf586cc Mon Sep 17 00:00:00 2001 From: Guodong Zhu Date: Sun, 8 Feb 2026 22:30:04 -0500 Subject: [PATCH 11/11] cleanup --- completion/wt.bash | 43 +++++++++++++++++++++++ completion/wt.zsh | 86 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/completion/wt.bash b/completion/wt.bash index 2bbcea8..4f4227a 100644 --- a/completion/wt.bash +++ b/completion/wt.bash @@ -260,8 +260,51 @@ _wt_cd_complete() { COMPREPLY+=( $(compgen -d -- "$cur") ) } +# --- Completion for wt-metadata-export: directories --- +_wt_metadata_export_complete() { + local cur + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + + # Complete directories + COMPREPLY=($(compgen -d -- "$cur")) +} + +# --- Completion for wt-metadata-import: worktrees and directories --- +_wt_metadata_import_complete() { + local cur cword + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + cword=$COMP_CWORD + + # First arg: source directory or target worktree + # Second arg: target worktree + if [[ $cword -eq 1 ]]; then + # Offer both worktrees and directories + local worktrees + worktrees="$(_wt_worktree_list)" + if [[ -n "$worktrees" ]]; then + local IFS=$'\n' + compopt -o filenames 2>/dev/null + COMPREPLY+=( $(compgen -W "$worktrees" -- "$cur") ) + fi + COMPREPLY+=($(compgen -d -- "$cur")) + elif [[ $cword -eq 2 ]]; then + # Second argument - target worktree + local worktrees + worktrees="$(_wt_worktree_list)" + if [[ -n "$worktrees" ]]; then + local IFS=$'\n' + compopt -o filenames 2>/dev/null + COMPREPLY+=( $(compgen -W "$worktrees" -- "$cur") ) + fi + fi +} + # --- Wire up completion functions (only if commands exist on PATH) --- type wt-add >/dev/null 2>&1 && complete -F _wt_add_complete wt-add type wt-switch >/dev/null 2>&1 && complete -F _wt_switch_complete wt-switch type wt-remove >/dev/null 2>&1 && complete -F _wt_remove_complete wt-remove type wt-cd >/dev/null 2>&1 && complete -F _wt_cd_complete wt-cd +type wt-metadata-export >/dev/null 2>&1 && complete -F _wt_metadata_export_complete wt-metadata-export +type wt-metadata-import >/dev/null 2>&1 && complete -F _wt_metadata_import_complete wt-metadata-import diff --git a/completion/wt.zsh b/completion/wt.zsh index 77d6469..3cbe6ef 100644 --- a/completion/wt.zsh +++ b/completion/wt.zsh @@ -218,6 +218,49 @@ if (( $+commands[fzf] )); then compdef _wt_remove wt-remove compdef _wt_cd wt-cd + # Completion for wt-metadata-export: directories + _wt_metadata_export() { + _arguments -C \ + '1:source directory:_files -/' \ + '2:target directory:_files -/' + } + + # Completion for wt-metadata-import: worktrees and directories + _wt_metadata_import() { + local context state + typeset -A opt_args + + _arguments -C \ + '1:source or target:->first' \ + '2:target worktree:->worktree' && return 0 + + case "$state" in + first) + local -a worktrees + worktrees=(${(f)$(_wt_worktree_list)}) + + if (( ${#worktrees[@]} > 0 )); then + _describe 'worktrees' worktrees || _files -/ + else + _files -/ + fi + ;; + worktree) + local -a worktrees + worktrees=(${(f)$(_wt_worktree_list)}) + + if (( ${#worktrees[@]} > 0 )); then + _describe 'worktrees' worktrees || _files -/ + else + _files -/ + fi + ;; + esac + } + + compdef _wt_metadata_export wt-metadata-export + compdef _wt_metadata_import wt-metadata-import + # ------------------------------------------------------------------- # PATH 2: FZF not available → pure zsh completion # ------------------------------------------------------------------- @@ -321,4 +364,47 @@ else compdef _wt_switch wt-switch compdef _wt_remove wt-remove compdef _wt_cd wt-cd + + # Completion for wt-metadata-export: directories + _wt_metadata_export() { + _arguments -C \ + '1:source directory:_files -/' \ + '2:target directory:_files -/' + } + + # Completion for wt-metadata-import: worktrees and directories + _wt_metadata_import() { + local context state + typeset -A opt_args + + _arguments -C \ + '1:source or target:->first' \ + '2:target worktree:->worktree' && return 0 + + case "$state" in + first) + local -a worktrees + worktrees=(${(f)$(_wt_worktree_list)}) + + if (( ${#worktrees[@]} > 0 )); then + _describe 'worktrees' worktrees || _files -/ + else + _files -/ + fi + ;; + worktree) + local -a worktrees + worktrees=(${(f)$(_wt_worktree_list)}) + + if (( ${#worktrees[@]} > 0 )); then + _describe 'worktrees' worktrees || _files -/ + else + _files -/ + fi + ;; + esac + } + + compdef _wt_metadata_export wt-metadata-export + compdef _wt_metadata_import wt-metadata-import fi