From b870738f9a8adad3ccb32c5d9a982a64acfb894c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Sat, 6 Jun 2026 00:10:32 +0200 Subject: [PATCH] refactor: strip release cycle, monorepo, exec-lib, and source from arty.sh - Remove whip/release-cycle commands: release, version, bump, changelog, tag, hooks (and all associated functions) - Remove monorepo commands: mono/monorepo (and all associated functions) - Remove exec command (exec_lib / .arty/bin runner) - Remove source command (source_lib) - Remove test/build scripts from init_project template - Restore exec_script wildcard so arty.yml scripts (npm/install, etc.) still work - Add auto-install for mikefarah/yq v4 with multi-method fallback - Fix silent crash: command -v matched the yq shell wrapper function; switched to type -P and guarded version-check assignment with || true Co-Authored-By: Claude Sonnet 4.6 --- scripts/arty.sh | 1174 +++++++---------------------------------------- 1 file changed, 170 insertions(+), 1004 deletions(-) diff --git a/scripts/arty.sh b/scripts/arty.sh index 233f8d51..ec3f6a09 100755 --- a/scripts/arty.sh +++ b/scripts/arty.sh @@ -1,7 +1,6 @@ #!/bin/bash # arty.sh - A bash library repository and release management system -# Combined functionality from arty.sh (repository management) and whip.sh (release cycle) # Version: 1.0.0 set -euo pipefail @@ -11,11 +10,6 @@ ARTY_ENV="${ARTY_ENV:-default}" ARTY_DRY_RUN="${ARTY_DRY_RUN:-0}" ARTY_VERBOSE="${ARTY_VERBOSE:-0}" -# Whip configuration -WHIP_CONFIG="${WHIP_CONFIG:-arty.yml}" -WHIP_HOOKS_DIR=".whip/hooks" -WHIP_CHANGELOG="${WHIP_CHANGELOG:-CHANGELOG.md}" - # Enable colors only when stdout is a real terminal (or FORCE_COLOR=1 overrides) if [[ -t 1 && "${TERM:-}" != "dumb" ]]; then export FORCE_COLOR=${FORCE_COLOR:-"1"} @@ -169,34 +163,126 @@ yq() { GOMAXPROCS=1 GOGC=off command yq "$@" || return 0 } -# Check if yq is installed -check_yq() { - if ! command -v yq &>/dev/null; then - log_error "yq is not installed. Please install yq to use arty." - log_info "Visit https://github.com/mikefarah/yq for installation instructions" - log_info "Quick install: brew install yq (macOS) or see README.md" - exit 1 +# Auto-install mikefarah/yq v4 using whatever package manager is available. +_install_yq() { + if command -v brew &>/dev/null; then + log_info "Installing yq via Homebrew..." + brew install yq && return 0 fi + + if command -v snap &>/dev/null; then + log_info "Installing yq via snap..." + snap install yq && return 0 + fi + + if command -v apk &>/dev/null; then + log_info "Installing yq via apk..." + apk add --no-cache yq-go && return 0 + fi + + if command -v pacman &>/dev/null; then + log_info "Installing yq via pacman..." + pacman -S --noconfirm go-yq && return 0 + fi + + if command -v go &>/dev/null; then + log_info "Installing yq via go install..." + local go_bin="${HOME}/.local/bin" + mkdir -p "$go_bin" + GOBIN="$go_bin" go install github.com/mikefarah/yq/v4@latest && { + export PATH="${go_bin}:${PATH}" + return 0 + } + fi + + # Binary download fallback (Linux / macOS) + local os arch install_dir use_sudo=0 + os=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$(uname -m)" in + x86_64) arch="amd64" ;; + aarch64|arm64) arch="arm64" ;; + armv7*) arch="arm" ;; + *) arch="amd64" ;; + esac + + local yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_${os}_${arch}" + + if [[ -w /usr/local/bin ]]; then + install_dir="/usr/local/bin" + elif command -v sudo &>/dev/null && sudo -n true 2>/dev/null; then + install_dir="/usr/local/bin" + use_sudo=1 + else + install_dir="${HOME}/.local/bin" + mkdir -p "$install_dir" + export PATH="${install_dir}:${PATH}" + fi + + log_info "Downloading yq binary (${os}/${arch}) to ${install_dir}..." + + if command -v wget &>/dev/null; then + if [[ "$use_sudo" == "1" ]]; then + sudo wget -q "$yq_url" -O "${install_dir}/yq" || return 1 + sudo chmod +x "${install_dir}/yq" + else + wget -q "$yq_url" -O "${install_dir}/yq" || return 1 + chmod +x "${install_dir}/yq" + fi + return 0 + elif command -v curl &>/dev/null; then + if [[ "$use_sudo" == "1" ]]; then + sudo curl -fsSL "$yq_url" -o "${install_dir}/yq" || return 1 + sudo chmod +x "${install_dir}/yq" + else + curl -fsSL "$yq_url" -o "${install_dir}/yq" || return 1 + chmod +x "${install_dir}/yq" + fi + return 0 + fi + + return 1 } -# Check if git is installed -check_dependencies() { - local missing=0 +# Check if mikefarah/yq v4 is installed; auto-install if not. +check_yq() { + local need_install=0 - if ! command -v yq &>/dev/null; then - log_error "yq is not installed" - missing=1 + # Use type -P to find the actual binary; command -v also matches the yq shell wrapper + # function defined in this script and would give a false positive. + if ! type -P yq &>/dev/null; then + log_warn "yq is not installed — attempting automatic installation..." + need_install=1 + else + local ver_out + ver_out=$(command yq --version 2>&1) || true # guard: command not found → exit 127 + set -e + if [[ ! "$ver_out" =~ mikefarah ]]; then + log_warn "Found yq but it is not mikefarah/yq v4 (got: ${ver_out})" + log_warn "arty requires mikefarah/yq v4 — attempting automatic installation..." + need_install=1 + fi fi - if ! command -v git &>/dev/null; then - log_error "git is not installed" - missing=1 + if [[ "$need_install" == "0" ]]; then + return 0 fi - if [[ $missing -eq 1 ]]; then - log_error "Missing required dependencies" - exit 1 + if _install_yq && command -v yq &>/dev/null; then + local ver_out + ver_out=$(command yq --version 2>&1) + log_success "yq installed: ${ver_out}" + return 0 fi + + log_error "Could not install yq automatically. Please install mikefarah/yq v4:" + log_error " brew install yq (macOS / Linuxbrew)" + log_error " snap install yq (Linux with snap)" + log_error " apk add yq-go (Alpine Linux)" + log_error " pacman -S go-yq (Arch Linux)" + log_error " go install github.com/mikefarah/yq/v4@latest (Go toolchain)" + log_error " wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 \\" + log_error " -O /usr/local/bin/yq && chmod +x /usr/local/bin/yq" + log_error "See: https://github.com/mikefarah/yq#install" + exit 1 } # Initialize arty environment @@ -269,6 +355,34 @@ list_yaml_scripts() { yq eval '.scripts | keys | .[]' "$file" 2>/dev/null } +# Execute a script defined in arty.yml +exec_script() { + local script_name="$1" + shift + + if [[ ! -f "$ARTY_CONFIG_FILE" ]]; then + log_error "Config file not found: $ARTY_CONFIG_FILE" + log_info "Run this command in a directory with arty.yml" + return 1 + fi + + local cmd + cmd=$(get_yaml_script "$ARTY_CONFIG_FILE" "$script_name") + + if [[ -z "$cmd" ]] || [[ "$cmd" == "null" ]]; then + log_error "Unknown command: $script_name" + log_info "Available scripts:" + while IFS= read -r name; do + [[ -n "$name" ]] && echo " - $name" + done < <(list_yaml_scripts "$ARTY_CONFIG_FILE") + return 1 + fi + + log_info "Running: $script_name" + [[ ! "$cmd" =~ \$@ ]] && cmd="$cmd"' "$@"' + bash -c "$cmd" -- "$@" +} + # Get library name from repository URL get_lib_name() { local repo_url="$1" @@ -1012,753 +1126,17 @@ description: "A bash library project" author: "" license: "MIT" -# Dependencies from other arty.sh repositories references: - # - https://github.com/user/some-bash-lib.git - # - https://github.com/user/another-lib.git - -# Entry point script -main: "index.sh" - -# Scripts that can be executed -scripts: - test: "bash test.sh" - build: "bash build.sh" + # - https://github.com/user/some-lib.git + # - url: git@github.com:user/repo.git + # into: some/path + # ref: v1.0.0 EOF log_success "Created $ARTY_CONFIG_FILE" log_success "Created .arty/ folder structure" } -# Source/load a library -source_lib() { - local lib_name="$1" - local lib_file="${2:-index.sh}" - local libs_dir="${ARTY_LIBS_DIR:-${ARTY_HOME:-$PWD/.arty}/libs}" - local lib_path="$libs_dir/$lib_name/$lib_file" - - if [[ ! -f "$lib_path" ]]; then - log_error "Library file not found: $lib_path" - return 1 - fi - - source "$lib_path" -} - -# Execute a library's main script -exec_lib() { - local lib_name="$1" - shift # Remove lib_name from arguments, rest are passed to the script - - local lib_name_stripped="$(basename $lib_name .sh)" - local bin_dir="${ARTY_BIN_DIR:-${ARTY_HOME:-$PWD/.arty}/bin}" - local bin_path="$bin_dir/$lib_name_stripped" - - if [[ ! -f "$bin_path" ]]; then - log_error "Library executable not found: $lib_name_stripped" - log_info "Make sure the library is installed with 'arty deps' or 'arty install'" - log_info "Available executables:" - if [[ -d "$bin_dir" ]]; then - for exec_file in $bin_dir/*; do - if [[ -f "$exec_file" ]]; then - echo " -- $(basename "$exec_file")" - fi - done - else - echo " (none found - run 'arty deps' first)" - fi - return 1 - fi - - if [[ ! -x "$bin_path" ]]; then - log_error "Library executable is not executable: $bin_path" - return 1 - fi - - # Execute the library's main script with all passed arguments - "$bin_path" "$@" -} - -# Execute a script from arty.yml -exec_script() { - local script_name="$1" - shift # Remove script name from arguments - local config_file="${ARTY_CONFIG_FILE}" - - if [[ ! -f "$config_file" ]]; then - log_error "Config file not found: $config_file" - log_info "Run this command in a directory with arty.yml" - return 1 - fi - - # Get script command using yq - local cmd=$(get_yaml_script "$config_file" "$script_name") - - if [[ -z "$cmd" ]] || [[ "$cmd" == "null" ]]; then - log_error "Script not found in arty.yml: $script_name" - log_info "Available scripts:" - while IFS= read -r name; do - if [[ -n "$name" ]]; then - echo " - $name" - fi - done < <(list_yaml_scripts "$config_file") - return 1 - fi - - log_info "Executing script: $script_name" - # Pass all remaining arguments to the script - # If the command doesn't already reference $@, append it - if [[ ! "$cmd" =~ \$@ ]]; then - cmd="$cmd"' "$@"' - fi - # The $@ in the command string will expand to the arguments passed after -- - bash -c "$cmd" -- "$@" - return $? -} - -# ============================================================================ -# WHIP.SH FUNCTIONS - Release Cycle Management -# ============================================================================ - -# Get current version from arty.yml -get_current_version() { - local config="${1:-$WHIP_CONFIG}" - - if [[ ! -f "$config" ]]; then - echo "0.0.0" - return - fi - - local version=$(yq eval '.version' "$config" 2>/dev/null) - if [[ -z "$version" ]] || [[ "$version" == "null" ]]; then - echo "0.0.0" - else - echo "$version" - fi -} - -# Update version in arty.yml -update_version() { - local new_version="$1" - local config="${2:-$WHIP_CONFIG}" - - if [[ ! -f "$config" ]]; then - log_error "Config file not found: $config" - return 1 - fi - - yq eval ".version = \"$new_version\"" -i "$config" - log_success "Updated version to $new_version in $config" -} - -# Parse semver components -parse_version() { - local version="$1" - local -n major_ref=$2 - local -n minor_ref=$3 - local -n patch_ref=$4 - - # Remove 'v' prefix if present - version="${version#v}" - - if [[ "$version" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then - major_ref="${BASH_REMATCH[1]}" - minor_ref="${BASH_REMATCH[2]}" - patch_ref="${BASH_REMATCH[3]}" - return 0 - else - return 1 - fi -} - -# Bump version (major, minor, or patch) -bump_version() { - local bump_type="$1" - local config="${2:-$WHIP_CONFIG}" - - local current_version=$(get_current_version "$config") - local major minor patch - - if ! parse_version "$current_version" major minor patch; then - log_error "Invalid version format: $current_version" - return 1 - fi - - case "$bump_type" in - major) - major=$((major + 1)) - minor=0 - patch=0 - ;; - minor) - minor=$((minor + 1)) - patch=0 - ;; - patch) - patch=$((patch + 1)) - ;; - *) - log_error "Invalid bump type: $bump_type (use major, minor, or patch)" - return 1 - ;; -esac - -local new_version="${major}.${minor}.${patch}" -echo "$new_version" -} - -# Generate changelog from git commits -generate_changelog() { - local from_tag="${1:-}" - local to_ref="${2:-HEAD}" - local title="${3:-Changelog}" - - local range - if [[ -z "$from_tag" ]]; then - # Get all commits if no from_tag - range="$to_ref" - else - range="${from_tag}..${to_ref}" - fi - - echo "# $title" - echo "" - echo "## Changes" - echo "" - - git log "$range" --pretty=format:"- %s (%h)" --reverse 2>/dev/null || { - echo "- Initial release" - } - echo "" -} - -# Update CHANGELOG.md file -update_changelog_file() { - local new_version="$1" - local changelog_file="${2:-$WHIP_CHANGELOG}" - - local previous_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - local date=$(date +%Y-%m-%d) - - local temp_file=$(mktemp) - - # Generate new version section - { - echo "# Changelog" - echo "" - echo "## [$new_version] - $date" - echo "" - - if [[ -n "$previous_tag" ]]; then - git log "${previous_tag}..HEAD" --pretty=format:"- %s" --reverse - else - echo "- Initial release" - fi - echo "" - echo "" - - # Append existing changelog if it exists - if [[ -f "$changelog_file" ]]; then - # Skip the first "# Changelog" line and empty lines - tail -n +2 "$changelog_file" | sed '/^$/d; 1s/^//' - fi - } >"$temp_file" - - mv "$temp_file" "$changelog_file" - log_success "Updated $changelog_file" -} - -# Create and push git tag -create_release_tag() { - local version="$1" - local message="${2:-Release version $version}" - local push="${3:-true}" - - local tag="v${version}" - - # Check if tag already exists - if git rev-parse "$tag" >/dev/null 2>&1; then - log_warn "Tag $tag already exists" - return 1 - fi - - # Create annotated tag - git tag -a "$tag" -m "$message" - log_success "Created tag: $tag" - - # Push tag if requested - if [[ "$push" == "true" ]]; then - git push origin "$tag" - log_success "Pushed tag: $tag" - fi -} - -# Full release workflow -release() { - local bump_type="${1:-patch}" - local config="${2:-$WHIP_CONFIG}" - local push="${3:-true}" - - check_dependencies - - # Check if we're in a git repository - if ! git rev-parse --git-dir >/dev/null 2>&1; then - log_error "Not a git repository" - return 1 - fi - - # Check for uncommitted changes - if [[ -n $(git status --porcelain) ]]; then - log_warn "You have uncommitted changes" - read -p "Continue anyway? [y/N] " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - log_info "Release cancelled" - return 1 - fi - fi - - log_step "Starting release process" - - # Bump version - local new_version=$(bump_version "$bump_type" "$config") - log_info "New version: $new_version" - - # Update version in config - update_version "$new_version" "$config" - - # Update changelog - update_changelog_file "$new_version" - - # Commit changes - git add "$config" "$WHIP_CHANGELOG" - git commit -m "chore: release version $new_version" - log_success "Committed version changes" - - # Create and push tag - create_release_tag "$new_version" "Release version $new_version" "$push" - - # Push commits if requested - if [[ "$push" == "true" ]]; then - git push - log_success "Pushed commits" - fi - - log_success "Release $new_version completed successfully!" -} - -# Install git hooks -install_hooks() { - local hooks_source_dir="${1:-$WHIP_HOOKS_DIR}" - local git_hooks_dir=".git/hooks" - - if [[ ! -d "$git_hooks_dir" ]]; then - log_error "Not a git repository" - return 1 - fi - - if [[ ! -d "$hooks_source_dir" ]]; then - log_warn "Hooks directory not found: $hooks_source_dir" - log_info "Creating default hooks directory..." - mkdir -p "$hooks_source_dir" - create_default_hooks "$hooks_source_dir" - fi - - log_step "Installing git hooks from $hooks_source_dir" - - local installed=0 - for hook_file in "$hooks_source_dir"/*; do - if [[ -f "$hook_file" ]]; then - local hook_name=$(basename "$hook_file") - local target="$git_hooks_dir/$hook_name" - - cp "$hook_file" "$target" - chmod +x "$target" - log_success "Installed: $hook_name" - installed=$((installed + 1)) - fi - done - - if [[ $installed -eq 0 ]]; then - log_warn "No hooks found in $hooks_source_dir" - else - log_success "Installed $installed hook(s)" - fi -} - -# Create default hooks with validation -create_default_hooks() { - local hooks_dir="$1" - - mkdir -p "$hooks_dir" - - # Pre-commit hook with shellcheck and bash -n validation - cat >"$hooks_dir/pre-commit" <<'EOF' -#!/usr/bin/env bash -# Pre-commit hook: validates bash scripts - -set -e - -echo "Running pre-commit checks..." - -# Find all staged .sh files -staged_files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.sh$' || true) - -if [[ -z "$staged_files" ]]; then - echo "No shell scripts to check" - exit 0 -fi - -errors=0 - -# Check each file -for file in $staged_files; do - if [[ ! -f "$file" ]]; then - continue - fi - - echo "Checking: $file" - - # Bash syntax check - if ! bash -n "$file" 2>&1; then - echo "ERROR: Syntax error in $file" - errors=$((errors + 1)) - fi - - # ShellCheck if available - if command -v shellcheck &> /dev/null; then - if ! shellcheck "$file" 2>&1; then - echo "WARNING: ShellCheck found issues in $file" - # Don't fail on shellcheck warnings, just inform - fi - fi -done - -if [[ $errors -gt 0 ]]; then - echo "ERROR: $errors file(s) with syntax errors" - echo "Please fix the errors before committing" - exit 1 -fi - -echo "Pre-commit checks passed!" -exit 0 -EOF - - chmod +x "$hooks_dir/pre-commit" - log_success "Created default pre-commit hook" -} - -# Uninstall git hooks -uninstall_hooks() { - local git_hooks_dir=".git/hooks" - - if [[ ! -d "$git_hooks_dir" ]]; then - log_error "Not a git repository" - return 1 - fi - - log_step "Removing git hooks" - - # Remove hooks that were installed by whip - local hooks=("pre-commit" "pre-push" "commit-msg") - local removed=0 - - for hook in "${hooks[@]}"; do - local hook_file="$git_hooks_dir/$hook" - if [[ -f "$hook_file" ]]; then - rm "$hook_file" - log_success "Removed: $hook" - removed=$((removed + 1)) - fi - done - - if [[ $removed -eq 0 ]]; then - log_info "No hooks to remove" - else - log_success "Removed $removed hook(s)" - fi -} - -# Find arty.yml projects in subdirectories -find_arty_projects() { - local root_dir="${1:-.}" - local pattern="${2:-*}" - -find "$root_dir" -maxdepth 2 -type f -name "arty.yml" | while read -r config; do - local project_dir=$(dirname "$config") - local project_name=$(basename "$project_dir") - - # Apply glob pattern filter - if [[ "$project_name" == $pattern ]]; then - echo "$project_dir" - fi -done -} - -# Execute bash command on monorepo projects -monorepo_exec() { - local bash_cmd="$1" - local root_dir="${2:-.}" - local pattern="${3:-*}" - - if [[ -z "$bash_cmd" ]]; then - log_error "Command required" - return 1 - fi - - log_step "Scanning for arty.yml projects in $root_dir" - - local projects=() - while IFS= read -r project_dir; do - projects+=("$project_dir") - done < <(find_arty_projects "$root_dir" "$pattern") - - if [[ ${#projects[@]} -eq 0 ]]; then - log_warn "No arty.yml projects found matching pattern: $pattern" - return 1 - fi - - log_info "Found ${#projects[@]} project(s)" - log_info "Executing: $bash_cmd" - echo - - local failed=0 - for project_dir in "${projects[@]}"; do - local project_name=$(basename "$project_dir") - echo -e "${CYAN} $project_name ${NC}" - - ( - # Export variables for use in command - export WHIP_PROJECT_DIR="$project_dir" - export WHIP_PROJECT_NAME="$project_name" - - cd "$project_dir" || exit 1 - - # Execute the bash command - eval "$bash_cmd" - ) || { - log_error "Failed for $project_name" - failed=$((failed + 1)) - } - - echo - done - - if [[ $failed -gt 0 ]]; then - log_warn "$failed project(s) failed" - return 1 - else - log_success "All projects processed successfully" - fi -} - -# Batch operation on monorepo projects -monorepo_batch() { - local command="$1" - local root_dir="${2:-.}" - local pattern="${3:-*}" - - log_step "Scanning for arty.yml projects in $root_dir" - - local projects=() - while IFS= read -r project_dir; do - projects+=("$project_dir") - done < <(find_arty_projects "$root_dir" "$pattern") - - if [[ ${#projects[@]} -eq 0 ]]; then - log_warn "No arty.yml projects found matching pattern: $pattern" - return 1 - fi - - log_info "Found ${#projects[@]} project(s)" - echo - - local failed=0 - for project_dir in "${projects[@]}"; do - local project_name=$(basename "$project_dir") - echo -e "${CYAN} Processing: $project_name ${NC}" - - ( - cd "$project_dir" - - case "$command" in - version) - local version=$(get_current_version) - echo "Version: $version" - ;; - bump) - local bump_type="${4:-patch}" - local new_version=$(bump_version "$bump_type") - update_version "$new_version" - echo "Bumped to: $new_version" - ;; - status) - if git rev-parse --git-dir >/dev/null 2>&1; then - git status --short - else - echo "Not a git repository" - fi - ;; - *) - log_error "Unknown command: $command" - return 1 - ;; - esac - ) || { - log_error "Failed for $project_name" - failed=$((failed + 1)) - } - - echo -done - -if [[ $failed -gt 0 ]]; then - log_warn "$failed project(s) failed" - return 1 - else - log_success "All projects processed successfully" -fi -} - -# Show comprehensive mono help -show_mono_help() { - cat <<'EOF' -arty mono - Monorepo management commands - -USAGE: - arty mono [options] [pattern] - -SUBCOMMANDS: - list [root] [pattern] List all arty.yml projects - version [root] [pattern] Show version of all projects - bump [root] [pattern] Bump version (major|minor|patch) - status [root] [pattern] Show git status for all projects - exec [root] [pattern] Execute bash command on all projects - help Show this help message - -ARGUMENTS: - root Root directory to search (default: current directory) - pattern Glob pattern to filter projects (default: *) - type Version bump type: major, minor, or patch - command Bash command or script to execute - -AVAILABLE VARIABLES (in exec): - $WHIP_PROJECT_DIR Full path to project directory - $WHIP_PROJECT_NAME Project name (basename) - $PWD Current directory (already cd'd into project) - -EXAMPLES: - - Basic Operations: - arty mono list # List all projects in current dir - arty mono list ../monorepo # List projects in specific dir - arty mono list . "lib-*" # List only lib-* projects - arty mono version # Show all project versions - arty mono status # Git status for all projects - - Version Management: - arty mono bump patch # Bump patch version for all - arty mono bump minor "lib-*" # Bump minor for lib-* projects - arty mono bump major . "*-core" # Bump major for *-core projects - - Executing Commands: - # Simple commands - arty mono exec "pwd" - arty mono exec "echo \$WHIP_PROJECT_NAME" - arty mono exec "git status" - - # Using project variables - arty mono exec 'echo "Project: $WHIP_PROJECT_NAME at $WHIP_PROJECT_DIR"' - - # Multi-line commands (use quotes) - arty mono exec 'git add . && git commit -m "chore: update" && git push' - - # Conditional execution - arty mono exec 'if [[ -f package.json ]]; then npm install; fi' - - # Complex operations - arty mono exec ' - echo "Cleaning $WHIP_PROJECT_NAME..." - rm -rf node_modules dist - echo "Building..." - npm run build - ' - - # With pattern filtering - arty mono exec "npm test" . "lib-*" - arty mono exec "make clean && make" . "*-service" - - Real-World Scenarios: - # Commit and push all projects - arty mono exec 'git add . && git commit -m "chore: streamline" && git push origin main' - - # Update dependencies - arty mono exec 'arty deps' - - # Run tests - arty mono exec 'bash test.sh' - - # Create git tags - arty mono exec 'git tag -a v1.0.0 -m "Release 1.0.0" && git push --tags' - - # Check for uncommitted changes - arty mono exec '[[ -n $(git status --porcelain) ]] && echo "Has changes" || echo "Clean"' - - # Generate documentation - arty mono exec 'leaf.sh . && echo "Docs generated"' - - # Sync with remote - arty mono exec 'git fetch && git pull origin main' - -PATTERN MATCHING: - Glob patterns filter which projects to process: - - * All projects (default) - lib-* Projects starting with "lib-" - *-core Projects ending with "-core" - app-* Projects starting with "app-" - *-service Projects ending with "-service" - test-* Projects starting with "test-" - -PROJECT DISCOVERY: - arty searches for arty.yml files up to 2 levels deep: - - monorepo/ -  lib-core/ -   arty.yml  Found -  services/ -   api-service/ -    arty.yml  Found -   web-service/ -   arty.yml  Found -  tools/ -  deep/ -  nested/ - arty.yml Too deep (>2 levels) - -ERROR HANDLING: - - Individual project failures don't stop the batch - - Failed projects are reported at the end - - Exit code reflects overall success/failure - - Use -e in commands for strict error handling - -TIPS: - - Quote commands with special characters - - Use single quotes to prevent variable expansion - - Test commands on one project first - - Use pattern matching to limit scope - - Check for uncommitted changes before operations - - Combine with other arty commands for workflows - -SEE ALSO: - arty --help Main help - arty release --help Release workflow help - arty hooks --help Git hooks help - -EOF -} - -# ============================================================================ # USAGE AND HELP # ============================================================================ @@ -1770,55 +1148,21 @@ arty.sh - A bash library repository and release management system USAGE: arty [arguments] [--dry-run] [-v|--verbose] -REPOSITORY COMMANDS: +COMMANDS: install [name] Install a library from git repository deps [--dry-run] Install all dependencies from arty.yml list List installed libraries with dependency tree notes Display project notes from arty.yml (supports markdown) remove Remove an installed library init [name] Initialize a new arty.yml project - source [file] Source a library (for use in scripts) - exec [args] Execute a library's main script with arguments - Execute a script defined in arty.yml - -RELEASE COMMANDS: - release [major|minor|patch] Full release workflow (default: patch) - - Bumps version in arty.yml - - Updates CHANGELOG.md from git history - - Creates git commit - - Creates and pushes git tag - - version Show current version from arty.yml - bump Bump version in arty.yml (no commit/tag) - changelog Generate changelog from git history - tag Create and push git tag - -HOOK COMMANDS: - hooks install Install git commit hooks - - Includes shellcheck validation - - Includes bash -n syntax check - hooks uninstall Remove git commit hooks - hooks create Create default hook templates - -MONOREPO COMMANDS: - mono list [root] [pattern] List arty.yml projects - mono version [root] [pattern] Show versions of all projects - mono bump [root] [pattern] Bump version for all projects - mono status [root] [pattern] Show git status for all projects - mono exec [root] [pattern] Execute bash command on all projects - mono help Show detailed mono help + [args] Run a script defined in arty.yml scripts section FLAGS: - --dry-run Simulate installation without making changes - --no-push Don't push commits/tags (for release) - --config Use custom config file (default: arty.yml) - --changelog Use custom changelog file (default: CHANGELOG.md) - -v, --verbose Enable verbose/debug output - -h, --help Show this help message + --dry-run Simulate installation without making changes + -v, --verbose Enable verbose/debug output + -h, --help Show this help message EXAMPLES: - - Repository Management: arty install https://github.com/user/bash-utils.git arty install https://github.com/user/lib.git my-lib arty deps @@ -1826,69 +1170,35 @@ EXAMPLES: arty list arty notes arty init my-project - arty test - arty build - arty exec leaf --help - arty exec mylib process file.txt - - Release Cycle: - arty release # Patch release - arty release major # Major version release - arty release minor --no-push # Minor release without pushing - arty bump minor # Just bump version - arty hooks install # Install commit hooks - arty version # Show current version - arty changelog # Generate changelog - - Monorepo Management: - arty mono list # List all projects - arty mono version # Show all project versions - arty mono bump patch . "lib-*" # Bump patch for lib-* projects - arty mono exec "git status" # Run command on all projects - arty mono exec 'git add . && git commit -m "update" && git push' . "lib-*" - arty mono help # Detailed monorepo help + arty npm/install + arty rust/install REFERENCE FORMATS: - References in arty.yml can be specified in two formats: - - 1. Simple URL format: + Simple URL: references: - https://github.com/user/repo.git - 2. Extended object format: + Extended object: references: - url: git@github.com:user/repo.git - into: custom/path # Custom installation directory (relative to arty.yml) - ref: v1.0.0 # Git ref (branch, tag, or commit hash; default: main) - env: production # Only install in this environment + into: custom/path # installation directory (relative to arty.yml) + ref: v1.0.0 # branch, tag, or commit hash + env: production # only install in this environment - 3. Extended format with multiple environments: + Multiple environments: references: - url: git@github.com:user/dev-tools.git - env: [dev, ci] # Install in dev OR ci environment + env: [dev, ci] PROJECT STRUCTURE: - When running 'arty init' or 'arty deps', the following structure is created: - project/ -  .arty/ -   bin/ # Linked executables (from 'main' field) -    index # Project's main script -    leaf # Dependency's main script -    mylib # Another dependency's main script -   libs/ # Dependencies (from 'references' field) -   dep1/ -   dep2/ -  .whip/ -   hooks/ # Git hooks -   pre-commit -  arty.yml # Project configuration -  CHANGELOG.md # Release changelog + .arty/ + libs/ # cloned dependencies + arty.yml ENVIRONMENT: - ARTY_HOME Home directory for arty (default: ~/.arty) - ARTY_CONFIG Config file name (default: arty.yml) - ARTY_ENV Environment to load from arty.yml envs section (default: default) + ARTY_HOME Home directory for arty (default: $PWD/.arty) + ARTY_ENV Environment name to load from arty.yml (default: default) EOF } @@ -1913,79 +1223,36 @@ main() { exit 0 fi - # Check for flags across all arguments FIRST + # Scan flags first local dry_run_flag=0 local verbose_flag=0 - local push=true - local config="$WHIP_CONFIG" - local changelog="$WHIP_CHANGELOG" for arg in "$@"; do case "$arg" in - --dry-run) - dry_run_flag=1 - ;; - --verbose|-v) - verbose_flag=1 - ;; - --no-push) - push=false - ;; - --config) - # Will be handled in the next loop - ;; - --changelog) - # Will be handled in the next loop - ;; + --dry-run) dry_run_flag=1 ;; + --verbose|-v) verbose_flag=1 ;; esac done - # Set verbose mode first (before loading env vars) if [[ "$verbose_flag" == "1" ]]; then export ARTY_VERBOSE=1 fi - # Load environment variables after verbose flag is set load_env_vars - # Set dry run mode if [[ "$dry_run_flag" == "1" ]]; then export ARTY_DRY_RUN=1 log_info "${YELLOW}[DRY RUN MODE]${NC} Simulating actions, no changes will be made" echo fi - # Remove flags from all arguments and extract command + # Extract command and positional args, stripping flags local args=() local command="" - local skip_next=0 for arg in "$@"; do - if [[ $skip_next -eq 1 ]]; then - skip_next=0 - continue - fi - case "$arg" in - --dry-run|--verbose|-v|--no-push|-h|--help) - continue - ;; - --config) - skip_next=1 - if [[ -n "${2:-}" ]]; then - config="$2" - WHIP_CONFIG="$config" - fi - continue - ;; - --changelog) - skip_next=1 - if [[ -n "${2:-}" ]]; then - changelog="$2" - WHIP_CHANGELOG="$changelog" - fi - continue - ;; + --dry-run|--verbose|-v|-h|--help) continue ;; *) if [[ -z "$command" ]]; then command="$arg" @@ -1996,7 +1263,6 @@ main() { esac done - # Handle help if [[ "$command" == "help" ]] || [[ "$command" == "--help" ]] || [[ "$command" == "-h" ]]; then show_usage exit 0 @@ -2011,7 +1277,6 @@ main() { install_lib "${args[@]}" local install_result=$? fi - # Show tree after installation if successful and arty.yml exists and has references if [[ $install_result -eq 0 ]] && [[ -f "$ARTY_CONFIG_FILE" ]]; then local ref_count=$(yq eval '.references | length' "$ARTY_CONFIG_FILE" 2>/dev/null) if [[ -n "$ref_count" ]] && [[ "$ref_count" != "null" ]] && [[ "$ref_count" != "0" ]]; then @@ -2024,7 +1289,6 @@ main() { deps) install_references local install_result=$? - # Show tree after installation if successful and arty.yml exists and has references if [[ $install_result -eq 0 ]] && [[ -f "$ARTY_CONFIG_FILE" ]]; then local ref_count=$(yq eval '.references | length' "$ARTY_CONFIG_FILE" 2>/dev/null) if [[ -n "$ref_count" ]] && [[ "$ref_count" != "null" ]] && [[ "$ref_count" != "0" ]]; then @@ -2038,7 +1302,7 @@ main() { list_libs ;; notes) - show_notes "$config" + show_notes ;; remove | rm) if [[ ${#args[@]} -eq 0 ]]; then @@ -2050,108 +1314,10 @@ main() { init) init_project "${args[@]}" ;; - exec) - if [[ ${#args[@]} -eq 0 ]]; then - log_error "Library name required" - log_info "Usage: arty exec [arguments]" - exit 1 - fi - exec_lib "${args[@]}" - ;; - source) - if [[ ${#args[@]} -eq 0 ]]; then - log_error "Library name required" - exit 1 - fi - source_lib "${args[@]}" - ;; - release) - local bump_type="${args[0]:-patch}" - release "$bump_type" "$config" "$push" - ;; - version) - get_current_version "$config" - ;; - bump) - if [[ ${#args[@]} -eq 0 ]]; then - log_error "Bump type required (major, minor, or patch)" - exit 1 - fi - local new_version=$(bump_version "${args[0]}" "$config") - update_version "$new_version" "$config" - echo "$new_version" - ;; - changelog) - generate_changelog "${args[0]:-}" "${args[1]:-HEAD}" - ;; - tag) - if [[ ${#args[@]} -eq 0 ]]; then - log_error "Version required" - exit 1 - fi - create_release_tag "${args[0]}" "${args[1]:-Release version ${args[0]}}" "$push" - ;; - hooks) - if [[ ${#args[@]} -eq 0 ]]; then - log_error "Hooks subcommand required (install, uninstall, create)" - exit 1 - fi - local subcommand="${args[0]}" - case "$subcommand" in - install) - install_hooks "${args[1]:-$WHIP_HOOKS_DIR}" - ;; - uninstall) - uninstall_hooks - ;; - create) - create_default_hooks "${args[1]:-$WHIP_HOOKS_DIR}" - ;; *) - log_error "Unknown hooks subcommand: $subcommand" - exit 1 + exec_script "$command" "${args[@]}" ;; -esac -;; -mono | monorepo) -if [[ ${#args[@]} -eq 0 ]]; then - log_error "Monorepo subcommand required" - show_mono_help - exit 1 -fi -local subcommand="${args[0]}" -case "$subcommand" in -help | --help | -h) -show_mono_help -;; -list) -find_arty_projects "${args[1]:-.}" "${args[2]:-*}" -;; -version | bump | status) -monorepo_batch "$subcommand" "${args[1]:-.}" "${args[2]:-*}" "${args[3]:-}" -;; -exec) -if [[ ${#args[@]} -lt 2 ]]; then - log_error "Command required for exec" - echo "Usage: arty mono exec [root] [pattern]" - echo "Example: arty mono exec 'git status' . 'lib-*'" - exit 1 -fi -local cmd="${args[1]}" -monorepo_exec "$cmd" "${args[2]:-.}" "${args[3]:-*}" -;; -*) -log_error "Unknown monorepo subcommand: $subcommand" -echo "Run 'arty mono help' for detailed usage" -exit 1 -;; -esac -;; -*) - # Try to execute as a script from arty.yml -exec_script "$command" "${args[@]}" -;; -esac + esac } # Run main if script is executed directly