diff --git a/build.sh b/build.sh index 312c417..e7d10f1 100755 --- a/build.sh +++ b/build.sh @@ -19,6 +19,9 @@ cat > "$OUTPUT_FILE" << 'HEADER' set -euo pipefail +# Exit cleanly on SIGPIPE (e.g., mars clone | grep, mars status | head) +trap 'exit 0' PIPE + MARS_VERSION="0.1.1" HEADER diff --git a/demo.gif b/demo.gif index 54405c2..58626e9 100644 Binary files a/demo.gif and b/demo.gif differ diff --git a/demo.tape b/demo.tape index da3b83b..c70264b 100644 --- a/demo.tape +++ b/demo.tape @@ -43,10 +43,10 @@ Type "mars add https://github.com/dean0x/mars-example-shared.git --tags shared" Enter Sleep 1.5s -# Step 3: Clone all repos +# Step 3: Clone all repos (spinners show per-repo progress) Type "mars clone" Enter -Sleep 4s +Sleep 8s # Step 4: Check status Type "mars status" diff --git a/dist/mars b/dist/mars index da42e95..bb9064f 100755 --- a/dist/mars +++ b/dist/mars @@ -5,6 +5,9 @@ set -euo pipefail +# Exit cleanly on SIGPIPE (e.g., mars clone | grep, mars status | head) +trap 'exit 0' PIPE + MARS_VERSION="0.1.1" @@ -289,6 +292,11 @@ ui_spinner_start() { local message="$1" _SPINNER_MSG="$message" + # Skip spinner animation when stdout is not a terminal (piped/redirected) + if [[ ! -t 1 ]]; then + return + fi + ( local i=0 local frame_count=${#S_SPINNER_FRAMES[@]} @@ -314,8 +322,10 @@ ui_spinner_stop() { fi _SPINNER_PID="" - # Clear spinner line - printf '\r\033[K' + # Clear spinner line only when connected to a terminal + if [[ -t 1 ]]; then + printf '\r\033[K' + fi # Show final message if provided if [[ -n "$final_message" ]]; then @@ -332,7 +342,9 @@ ui_spinner_error() { fi _SPINNER_PID="" - printf '\r\033[K' + if [[ -t 1 ]]; then + printf '\r\033[K' + fi ui_step_error "$message" } @@ -1140,10 +1152,7 @@ EOF # === lib/commands/clone.sh === # Mars CLI - clone command -# Clone configured repositories with parallel execution - -# Maximum concurrent clone jobs -CLONE_PARALLEL_LIMIT=4 +# Clone configured repositories with per-repo progress cmd_clone() { local tag="" @@ -1184,95 +1193,68 @@ cmd_clone() { return 1 fi - # Count repos + # Count total repos local total=0 - local to_clone=() - local already_cloned=() - while IFS= read -r repo; do [[ -z "$repo" ]] && continue total=$((total + 1)) - - local path - path=$(yaml_get_path "$repo") - local full_path="$MARS_REPOS_DIR/$path" - - if [[ -d "$full_path" ]] && [[ $force -eq 0 ]]; then - already_cloned+=("$repo") - else - to_clone+=("$repo") - fi done <<< "$repos" - # Report already cloned - for repo in "${already_cloned[@]}"; do - local path - path=$(yaml_get_path "$repo") - ui_step_done "Already cloned:" "$path" - done - - if [[ ${#to_clone[@]} -eq 0 ]]; then - ui_outro "All repositories already cloned" - return 0 - fi - - ui_bar_line - ui_info "Cloning ${#to_clone[@]} of $total repositories..." - ui_bar_line - - # Clone with parallelism - local pids=() - local repo_for_pid=() + local current=0 local success_count=0 + local skip_count=0 local fail_count=0 - local failed_repos=() - for repo in "${to_clone[@]}"; do + # Clone each repo with spinner feedback + while IFS= read -r repo; do + [[ -z "$repo" ]] && continue + current=$((current + 1)) + local url url=$(yaml_get_url "$repo") local path path=$(yaml_get_path "$repo") local full_path="$MARS_REPOS_DIR/$path" + # Already cloned? + if [[ -d "$full_path" ]] && [[ $force -eq 0 ]]; then + ui_step_done "Already cloned:" "$path" + skip_count=$((skip_count + 1)) + continue + fi + # Remove existing directory if force if [[ -d "$full_path" ]] && [[ $force -eq 1 ]]; then rm -rf "$full_path" fi - # Wait if at parallel limit - while [[ ${#pids[@]} -ge $CLONE_PARALLEL_LIMIT ]]; do - _clone_wait_one - done + # Show spinner while cloning + ui_spinner_start "Cloning $path... ($current/$total)" - # Start clone in background - ( - if git clone --quiet "$url" "$full_path" 2>/dev/null; then - exit 0 - else - exit 1 + local clone_err + if clone_err=$(git clone --quiet "$url" "$full_path" 2>&1); then + ui_spinner_stop + ui_step_done "Cloned:" "$path" + success_count=$((success_count + 1)) + else + ui_spinner_error "Failed to clone: $path" + if [[ -n "$clone_err" ]]; then + ui_info "$(ui_dim "$clone_err")" fi - ) & - - pids+=($!) - repo_for_pid+=("$repo") - done - - # Wait for remaining jobs - while [[ ${#pids[@]} -gt 0 ]]; do - _clone_wait_one - done + fail_count=$((fail_count + 1)) + fi + done <<< "$repos" # Summary ui_bar_line if [[ $fail_count -eq 0 ]]; then - ui_outro "Cloned $success_count repositories successfully" + local msg="Cloned $success_count repositories successfully" + if [[ $skip_count -gt 0 ]]; then + msg="$msg, $skip_count already cloned" + fi + ui_outro "$msg" else - for repo in "${failed_repos[@]}"; do - local path - path=$(yaml_get_path "$repo") - ui_step_error "Failed: $path" - done ui_outro_cancel "Cloned $success_count, failed $fail_count" return 1 fi @@ -1280,46 +1262,6 @@ cmd_clone() { return 0 } -# Helper to wait for one clone job -_clone_wait_one() { - if [[ ${#pids[@]} -eq 0 ]]; then - return - fi - - # Wait for any process to complete - local pid - for i in "${!pids[@]}"; do - pid="${pids[$i]}" - if ! kill -0 "$pid" 2>/dev/null; then - # Process finished - wait "$pid" - local exit_code=$? - local repo="${repo_for_pid[$i]}" - local path - path=$(yaml_get_path "$repo") - - if [[ $exit_code -eq 0 ]]; then - ui_step_done "Cloned:" "$path" - success_count=$((success_count + 1)) - else - ui_step_error "Failed to clone: $path" - fail_count=$((fail_count + 1)) - failed_repos+=("$repo") - fi - - # Remove from arrays - unset 'pids[i]' - unset 'repo_for_pid[i]' - pids=("${pids[@]}") - repo_for_pid=("${repo_for_pid[@]}") - return - fi - done - - # If all still running, sleep briefly - sleep 0.1 -} - # === lib/commands/status.sh === # Mars CLI - status command # Show git status across all repositories @@ -1767,9 +1709,13 @@ cmd_exec() { return 1 ;; *) - # Everything else is the command - command="$*" - break + if [[ -z "$command" ]]; then + command="$1" + else + ui_step_error "Unexpected argument: $1" + return 1 + fi + shift ;; esac done diff --git a/lib/commands/clone.sh b/lib/commands/clone.sh index 911ef17..c712d4b 100644 --- a/lib/commands/clone.sh +++ b/lib/commands/clone.sh @@ -1,9 +1,6 @@ #!/usr/bin/env bash # Mars CLI - clone command -# Clone configured repositories with parallel execution - -# Maximum concurrent clone jobs -CLONE_PARALLEL_LIMIT=4 +# Clone configured repositories with per-repo progress cmd_clone() { local tag="" @@ -44,138 +41,71 @@ cmd_clone() { return 1 fi - # Count repos + # Count total repos local total=0 - local to_clone=() - local already_cloned=() - while IFS= read -r repo; do [[ -z "$repo" ]] && continue total=$((total + 1)) - - local path - path=$(yaml_get_path "$repo") - local full_path="$MARS_REPOS_DIR/$path" - - if [[ -d "$full_path" ]] && [[ $force -eq 0 ]]; then - already_cloned+=("$repo") - else - to_clone+=("$repo") - fi done <<< "$repos" - # Report already cloned - for repo in "${already_cloned[@]}"; do - local path - path=$(yaml_get_path "$repo") - ui_step_done "Already cloned:" "$path" - done - - if [[ ${#to_clone[@]} -eq 0 ]]; then - ui_outro "All repositories already cloned" - return 0 - fi - - ui_bar_line - ui_info "Cloning ${#to_clone[@]} of $total repositories..." - ui_bar_line - - # Clone with parallelism - local pids=() - local repo_for_pid=() + local current=0 local success_count=0 + local skip_count=0 local fail_count=0 - local failed_repos=() - for repo in "${to_clone[@]}"; do + # Clone each repo with spinner feedback + while IFS= read -r repo; do + [[ -z "$repo" ]] && continue + current=$((current + 1)) + local url url=$(yaml_get_url "$repo") local path path=$(yaml_get_path "$repo") local full_path="$MARS_REPOS_DIR/$path" + # Already cloned? + if [[ -d "$full_path" ]] && [[ $force -eq 0 ]]; then + ui_step_done "Already cloned:" "$path" + skip_count=$((skip_count + 1)) + continue + fi + # Remove existing directory if force if [[ -d "$full_path" ]] && [[ $force -eq 1 ]]; then rm -rf "$full_path" fi - # Wait if at parallel limit - while [[ ${#pids[@]} -ge $CLONE_PARALLEL_LIMIT ]]; do - _clone_wait_one - done - - # Start clone in background - ( - if git clone --quiet "$url" "$full_path" 2>/dev/null; then - exit 0 - else - exit 1 - fi - ) & - - pids+=($!) - repo_for_pid+=("$repo") - done + # Show spinner while cloning + ui_spinner_start "Cloning $path... ($current/$total)" - # Wait for remaining jobs - while [[ ${#pids[@]} -gt 0 ]]; do - _clone_wait_one - done + local clone_err + if clone_err=$(git clone --quiet "$url" "$full_path" 2>&1); then + ui_spinner_stop + ui_step_done "Cloned:" "$path" + success_count=$((success_count + 1)) + else + ui_spinner_error "Failed to clone: $path" + if [[ -n "$clone_err" ]]; then + ui_info "$(ui_dim "$clone_err")" + fi + fail_count=$((fail_count + 1)) + fi + done <<< "$repos" # Summary ui_bar_line if [[ $fail_count -eq 0 ]]; then - ui_outro "Cloned $success_count repositories successfully" + local msg="Cloned $success_count repositories successfully" + if [[ $skip_count -gt 0 ]]; then + msg="$msg, $skip_count already cloned" + fi + ui_outro "$msg" else - for repo in "${failed_repos[@]}"; do - local path - path=$(yaml_get_path "$repo") - ui_step_error "Failed: $path" - done ui_outro_cancel "Cloned $success_count, failed $fail_count" return 1 fi return 0 } - -# Helper to wait for one clone job -_clone_wait_one() { - if [[ ${#pids[@]} -eq 0 ]]; then - return - fi - - # Wait for any process to complete - local pid - for i in "${!pids[@]}"; do - pid="${pids[$i]}" - if ! kill -0 "$pid" 2>/dev/null; then - # Process finished - wait "$pid" - local exit_code=$? - local repo="${repo_for_pid[$i]}" - local path - path=$(yaml_get_path "$repo") - - if [[ $exit_code -eq 0 ]]; then - ui_step_done "Cloned:" "$path" - success_count=$((success_count + 1)) - else - ui_step_error "Failed to clone: $path" - fail_count=$((fail_count + 1)) - failed_repos+=("$repo") - fi - - # Remove from arrays - unset 'pids[i]' - unset 'repo_for_pid[i]' - pids=("${pids[@]}") - repo_for_pid=("${repo_for_pid[@]}") - return - fi - done - - # If all still running, sleep briefly - sleep 0.1 -} diff --git a/lib/commands/exec.sh b/lib/commands/exec.sh index 5b50fe6..c770c0c 100644 --- a/lib/commands/exec.sh +++ b/lib/commands/exec.sh @@ -28,9 +28,13 @@ cmd_exec() { return 1 ;; *) - # Everything else is the command - command="$*" - break + if [[ -z "$command" ]]; then + command="$1" + else + ui_step_error "Unexpected argument: $1" + return 1 + fi + shift ;; esac done diff --git a/lib/ui.sh b/lib/ui.sh index f0ee090..b1690f4 100644 --- a/lib/ui.sh +++ b/lib/ui.sh @@ -279,6 +279,11 @@ ui_spinner_start() { local message="$1" _SPINNER_MSG="$message" + # Skip spinner animation when stdout is not a terminal (piped/redirected) + if [[ ! -t 1 ]]; then + return + fi + ( local i=0 local frame_count=${#S_SPINNER_FRAMES[@]} @@ -304,8 +309,10 @@ ui_spinner_stop() { fi _SPINNER_PID="" - # Clear spinner line - printf '\r\033[K' + # Clear spinner line only when connected to a terminal + if [[ -t 1 ]]; then + printf '\r\033[K' + fi # Show final message if provided if [[ -n "$final_message" ]]; then @@ -322,7 +329,9 @@ ui_spinner_error() { fi _SPINNER_PID="" - printf '\r\033[K' + if [[ -t 1 ]]; then + printf '\r\033[K' + fi ui_step_error "$message" } diff --git a/mars b/mars index 3e40f68..5968e35 100755 --- a/mars +++ b/mars @@ -4,6 +4,9 @@ set -euo pipefail +# Exit cleanly on SIGPIPE (e.g., mars clone | grep, mars status | head) +trap 'exit 0' PIPE + MARS_VERSION="0.1.1" # Determine script directory for sourcing libs