#!/bin/zsh # Platform-specific commands case $OSTYPE in darwin*) date_cmd() { gdate "$@"; } ;; linux*) date_cmd() { date "$@"; } ;; esac # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Configuration SCRIPT_DIR="${0:a:h}" REPO_ROOT="${SCRIPT_DIR:h:h}" LOGFILE="$REPO_ROOT/.submodule-update.log" AI_JSON="$REPO_ROOT/ai.json" # Usage function usage() { echo "Usage: $0 [--all] [--module=] [--auto] [--dry-run] [--validate] [--sync] [--add=] [--remove=] [--help]" echo "" echo "Options:" echo " --all Update all submodules" echo " --module= Update specific submodule (os, gpt, card, etc.)" echo " --auto Auto-commit if changes detected" echo " --dry-run Show what would be done without making changes" echo " --validate Validate URL consistency between ai.json and .gitmodules" echo " --sync Sync .gitmodules from ai.json configuration" echo " --add= Add new submodule from ai.json configuration" echo " --remove= Remove submodule (both from .gitmodules and filesystem)" echo " --help Show this help message" echo " --check-consistency Check for project inconsistencies and conflicts" echo "" echo "Available submodules:" git config --file .gitmodules --get-regexp path | awk '{print " " $2}' exit 1 } # Logging function log() { local level="$1" shift local message="$*" local timestamp=$(date_cmd '+%Y-%m-%d %H:%M:%S') echo "[$timestamp] [$level] $message" | tee -a "$LOGFILE" } # Parse arguments UPDATE_ALL=false SPECIFIC_MODULE="" AUTO_COMMIT=false DRY_RUN=false VALIDATE_ONLY=false SYNC_GITMODULES=false ADD_MODULE="" REMOVE_MODULE="" CHECK_CONSISTENCY=false for arg in "$@"; do case $arg in --all) UPDATE_ALL=true ;; --module=*) SPECIFIC_MODULE="${arg#*=}" ;; --auto) AUTO_COMMIT=true ;; --dry-run) DRY_RUN=true ;; --validate) VALIDATE_ONLY=true ;; --sync) SYNC_GITMODULES=true ;; --add=*) ADD_MODULE="${arg#*=}" ;; --remove=*) REMOVE_MODULE="${arg#*=}" ;; --check-consistency) CHECK_CONSISTENCY=true ;; --help|-h) usage ;; *) echo "Unknown argument: $arg" usage ;; esac done # Validate arguments operations_count=0 [[ "$UPDATE_ALL" == true ]] && ((operations_count++)) [[ -n "$SPECIFIC_MODULE" ]] && ((operations_count++)) [[ "$VALIDATE_ONLY" == true ]] && ((operations_count++)) [[ "$SYNC_GITMODULES" == true ]] && ((operations_count++)) [[ -n "$ADD_MODULE" ]] && ((operations_count++)) [[ -n "$REMOVE_MODULE" ]] && ((operations_count++)) [[ "$CHECK_CONSISTENCY" == true ]] && ((operations_count++)) if [[ $operations_count -eq 0 ]]; then echo "Error: At least one operation is required" usage fi if [[ $operations_count -gt 1 ]]; then echo "Error: Only one operation can be specified at a time" usage fi # Change to repository root cd "$REPO_ROOT" || exit 1 echo -e "${BLUE}🚀 Starting submodule update...${NC}" log "INFO" "Starting submodule update (all=$UPDATE_ALL, module=$SPECIFIC_MODULE, auto=$AUTO_COMMIT, dry-run=$DRY_RUN)" if [[ "$DRY_RUN" == true ]]; then echo -e "${YELLOW}🔍 DRY RUN MODE - No changes will be made${NC}" fi # Update function update_submodule() { local module_name="$1" local module_path="$2" echo -e "\n${BLUE}📦 Processing: $module_name${NC}" # Check if submodule exists if [[ ! -d "$module_path" ]]; then echo -e "${RED}❌ Submodule directory not found: $module_path${NC}" return 1 fi # Get current commit local current_commit=$(git submodule status "$module_path" | awk '{print $1}' | sed 's/^[+-]//') # Get current branch of the submodule cd "$module_path" || return 1 local current_branch=$(git branch --show-current) local target_branch="${branches[$module_name]:-main}" if [[ -z "$current_branch" ]]; then # If not on a branch (detached HEAD), use target branch from ai.json current_branch="$target_branch" fi cd "$REPO_ROOT" if [[ "$DRY_RUN" == true ]]; then echo -e "${YELLOW}🔍 [DRY RUN] Would update $module_name (target branch: $target_branch)${NC}" echo " Current: $current_commit" cd "$module_path" || return 1 git fetch origin >/dev/null 2>&1 local latest_commit=$(git rev-parse "origin/$target_branch" 2>/dev/null) echo " Latest: $latest_commit" cd "$REPO_ROOT" if [[ "$current_commit" != "$latest_commit" ]]; then echo -e "${YELLOW} 📝 Changes available${NC}" return 0 else echo -e "${GREEN} ✅ Already up to date${NC}" return 1 fi fi # Update submodule echo "🔄 Fetching latest changes..." cd "$module_path" || return 1 if ! git fetch origin; then echo -e "${RED}❌ Failed to fetch from origin${NC}" cd "$REPO_ROOT" return 1 fi # Use the target branch from ai.json local latest_commit=$(git rev-parse "origin/$target_branch" 2>/dev/null) if [[ "$current_commit" == "$latest_commit" ]]; then echo -e "${GREEN}✅ Already up to date${NC}" cd "$REPO_ROOT" return 1 fi echo "📝 Updating to latest commit on branch $target_branch (configured in ai.json)..." # First ensure we're on the correct branch if ! git checkout "$target_branch"; then echo -e "${RED}❌ Failed to checkout branch $target_branch${NC}" cd "$REPO_ROOT" return 1 fi # Then pull the latest changes if ! git pull origin "$target_branch"; then echo -e "${RED}❌ Failed to pull latest changes from $target_branch${NC}" cd "$REPO_ROOT" return 1 fi cd "$REPO_ROOT" # Get the new commit after update local new_commit=$(cd "$module_path" && git rev-parse HEAD) # Stage the submodule update git add "$module_path" echo -e "${GREEN}✅ Updated $module_name (branch: $target_branch)${NC}" echo " From: $current_commit" echo " To: $new_commit" log "INFO" "Updated $module_name on branch $target_branch: $current_commit -> $new_commit" return 0 } # Get list of submodules declare -A submodules while IFS= read -r line; do if [[ $line =~ '^\[submodule "([^"]+)"\]' ]]; then current_name="${match[1]}" elif [[ $line =~ '^[[:space:]]*path[[:space:]]*=[[:space:]]*(.+)$' ]]; then submodules[$current_name]="${match[1]}" fi done < .gitmodules # Get branch information from ai.json declare -A branches get_branch_for_module() { local module="$1" local branch="main" # default branch if [[ -f "$AI_JSON" ]]; then # Try to extract branch from ai.json using jq if command -v jq >/dev/null 2>&1; then local json_branch=$(jq -r ".ai.${module}.branch // \"main\"" "$AI_JSON" 2>/dev/null) if [[ -n "$json_branch" && "$json_branch" != "null" ]]; then branch="$json_branch" fi fi fi echo "$branch" } # Get URL and branch information from ai.json declare -A urls get_git_config() { local config_key="$1" local default_value="$2" local value="" if [[ -f "$AI_JSON" ]]; then if command -v jq >/dev/null 2>&1; then value=$(jq -r ".metadata.git.${config_key} // \"${default_value}\"" "$AI_JSON" 2>/dev/null) fi fi # Fallback to default if not found if [[ -z "$value" || "$value" == "null" ]]; then value="$default_value" fi echo "$value" } # Extract username and repo from JSON path # For ai.gpt -> username=ai, repo=gpt extract_git_info_from_path() { local module="$1" local username="" local repo="" # Check if module exists in ai.json structure if [[ -f "$AI_JSON" ]]; then if command -v jq >/dev/null 2>&1; then # Get all keys in the ai object local ai_keys=$(jq -r '.ai | keys[]' "$AI_JSON" 2>/dev/null) # Find the first level (username) that contains our module while IFS= read -r key; do if jq -e ".ai.${key}.${module}" "$AI_JSON" >/dev/null 2>&1; then username="$key" repo="$module" break fi # Also check if the key itself is our module (direct under ai) if [[ "$key" == "$module" ]]; then username="ai" # Default namespace repo="$module" break fi done <<< "$ai_keys" fi fi # Fallback: assume ai namespace if [[ -z "$username" || -z "$repo" ]]; then username="ai" repo="$module" fi echo "${username}:${repo}" } get_base_url_for_module() { local module="$1" local host=$(get_git_config "host" "git.syui.ai") local protocol=$(get_git_config "protocol" "ssh") # Extract username and repo from JSON structure local git_info=$(extract_git_info_from_path "$module") local username="${git_info%%:*}" local repo="${git_info##*:}" local url="" case "$protocol" in "ssh") url="git@${host}:${username}/${repo}" ;; "https") url="https://${host}/${username}/${repo}" ;; *) # Default to ssh url="git@${host}:${username}/${repo}" ;; esac echo "$url" } get_url_for_module() { local module="$1" local url=$(get_base_url_for_module "$module") # Check if there's a custom git_url override in ai.json if [[ -f "$AI_JSON" ]]; then if command -v jq >/dev/null 2>&1; then local custom_url=$(jq -r ".ai.${module}.git_url // empty" "$AI_JSON" 2>/dev/null) if [[ -n "$custom_url" && "$custom_url" != "null" ]]; then url="$custom_url" fi fi fi echo "$url" } # Populate branches and URLs for all modules for module in "${(k)submodules[@]}"; do branches[$module]=$(get_branch_for_module "$module") urls[$module]=$(get_url_for_module "$module") done # Log git configuration for debugging log "INFO" "Git host: $(get_git_config 'host' 'git.syui.ai'), protocol: $(get_git_config 'protocol' 'ssh')" # Debug: Log found submodules log "INFO" "Found ${#submodules} submodules: ${(k)submodules[@]}" # Validation function validate_url_consistency() { echo -e "${BLUE}🔍 Validating URL consistency between ai.json and .gitmodules...${NC}" local inconsistencies=0 for module in "${(k)submodules[@]}"; do local gitmodules_url=$(git config --file .gitmodules --get "submodule.${module}.url") local ai_json_url="${urls[$module]}" if [[ -n "$ai_json_url" ]]; then if [[ "$gitmodules_url" != "$ai_json_url" ]]; then echo -e "${RED}❌ $module: URL mismatch${NC}" echo " .gitmodules: $gitmodules_url" echo " ai.json: $ai_json_url" ((inconsistencies++)) else echo -e "${GREEN}✅ $module: URLs match${NC}" fi else echo -e "${YELLOW}⚠️ $module: No git_url in ai.json${NC}" fi done echo "" if [[ $inconsistencies -eq 0 ]]; then echo -e "${GREEN}🎉 All URLs are consistent!${NC}" log "INFO" "URL validation passed: all URLs consistent" else echo -e "${RED}❌ Found $inconsistencies URL inconsistencies${NC}" echo "Run with --sync to fix inconsistencies" log "ERROR" "URL validation failed: $inconsistencies inconsistencies" return 1 fi } # Sync .gitmodules from ai.json sync_gitmodules() { echo -e "${BLUE}🔄 Syncing .gitmodules from ai.json...${NC}" local changes=0 for module in "${(k)submodules[@]}"; do local ai_json_url="${urls[$module]}" local ai_json_branch="${branches[$module]}" if [[ -n "$ai_json_url" ]]; then local current_url=$(git config --file .gitmodules --get "submodule.${module}.url") if [[ "$current_url" != "$ai_json_url" ]]; then echo "📝 Updating $module URL: $current_url -> $ai_json_url" git config --file .gitmodules "submodule.${module}.url" "$ai_json_url" ((changes++)) fi fi done if [[ $changes -gt 0 ]]; then echo -e "${GREEN}✅ Updated $changes URL(s) in .gitmodules${NC}" log "INFO" "Synced .gitmodules: updated $changes URLs" if [[ "$AUTO_COMMIT" == true ]]; then git add .gitmodules git commit -m "Sync .gitmodules URLs from ai.json 🔄 Updated $changes submodule URL(s) 🤖 Generated with submodule sync" echo -e "${GREEN}✅ Changes committed to .gitmodules${NC}" fi else echo -e "${GREEN}✅ .gitmodules is already in sync${NC}" fi } # Add new submodule add_submodule() { local module="$1" echo -e "${BLUE}➕ Adding submodule: $module${NC}" local ai_json_url=$(get_url_for_module "$module") local ai_json_branch=$(get_branch_for_module "$module") if [[ -z "$ai_json_url" ]]; then echo -e "${RED}❌ No git_url found for '$module' in ai.json${NC}" return 1 fi if [[ -d "$module" ]]; then echo -e "${RED}❌ Directory '$module' already exists${NC}" return 1 fi echo "📦 Adding submodule $module" echo " URL: $ai_json_url" echo " Branch: $ai_json_branch" if [[ "$DRY_RUN" == true ]]; then echo -e "${YELLOW}🔍 [DRY RUN] Would add submodule${NC}" return 0 fi if git submodule add -b "$ai_json_branch" "$ai_json_url" "$module"; then echo -e "${GREEN}✅ Successfully added submodule $module${NC}" log "INFO" "Added submodule $module from $ai_json_url (branch: $ai_json_branch)" if [[ "$AUTO_COMMIT" == true ]]; then git commit -m "Add submodule: $module 📦 Added from ai.json configuration 🌐 URL: $ai_json_url 🌿 Branch: $ai_json_branch 🤖 Generated with submodule manager" echo -e "${GREEN}✅ Submodule addition committed${NC}" fi else echo -e "${RED}❌ Failed to add submodule $module${NC}" return 1 fi } # Remove submodule remove_submodule() { local module="$1" echo -e "${BLUE}➖ Removing submodule: $module${NC}" if [[ ! -d "$module" ]]; then echo -e "${RED}❌ Submodule '$module' does not exist${NC}" return 1 fi if [[ "$DRY_RUN" == true ]]; then echo -e "${YELLOW}🔍 [DRY RUN] Would remove submodule${NC}" return 0 fi # Remove from .gitmodules git config --file .gitmodules --remove-section "submodule.$module" 2>/dev/null || true # Remove from .git/config git config --remove-section "submodule.$module" 2>/dev/null || true # Remove from git index git rm --cached "$module" 2>/dev/null || true # Remove directory rm -rf "$module" # Remove from .git/modules rm -rf ".git/modules/$module" echo -e "${GREEN}✅ Successfully removed submodule $module${NC}" log "INFO" "Removed submodule $module" if [[ "$AUTO_COMMIT" == true ]]; then git add .gitmodules "$module" 2>/dev/null || true git commit -m "Remove submodule: $module 🗑️ Completely removed submodule 🤖 Generated with submodule manager" echo -e "${GREEN}✅ Submodule removal committed${NC}" fi } # Consistency check function check_project_consistency() { echo -e "${BLUE}🔍 Checking project consistency...${NC}" local issues=0 local warnings=0 # Check for uncommitted changes in submodules echo -e "${BLUE}📝 Checking for uncommitted changes...${NC}" for module in "${(k)submodules[@]}"; do local module_path="${submodules[$module]}" if [[ -d "$module_path" ]]; then cd "$module_path" if ! git diff --quiet || ! git diff --cached --quiet; then echo -e "${YELLOW}⚠️ $module: Has uncommitted changes${NC}" git status --short | sed 's/^/ /' ((warnings++)) else echo -e "${GREEN}✅ $module: Clean working directory${NC}" fi cd "$REPO_ROOT" fi done # Check for branch inconsistencies echo -e "${BLUE}🌿 Checking branch consistency...${NC}" for module in "${(k)submodules[@]}"; do local module_path="${submodules[$module]}" local expected_branch="${branches[$module]}" if [[ -d "$module_path" ]]; then cd "$module_path" local current_branch=$(git branch --show-current) if [[ -z "$current_branch" ]]; then current_branch="(detached HEAD)" fi if [[ "$current_branch" != "$expected_branch" ]]; then echo -e "${YELLOW}⚠️ $module: Branch mismatch${NC}" echo " Current: $current_branch" echo " Expected: $expected_branch" ((warnings++)) else echo -e "${GREEN}✅ $module: Correct branch ($current_branch)${NC}" fi cd "$REPO_ROOT" fi done # Check for remote synchronization echo -e "${BLUE}🌐 Checking remote synchronization...${NC}" for module in "${(k)submodules[@]}"; do local module_path="${submodules[$module]}" if [[ -d "$module_path" ]]; then cd "$module_path" git fetch --quiet 2>/dev/null || true local ahead=$(git rev-list --count HEAD ^origin/HEAD 2>/dev/null || echo "0") local behind=$(git rev-list --count origin/HEAD ^HEAD 2>/dev/null || echo "0") if [[ "$ahead" -gt 0 || "$behind" -gt 0 ]]; then echo -e "${YELLOW}⚠️ $module: Out of sync with remote${NC}" if [[ "$ahead" -gt 0 ]]; then echo " $ahead commits ahead" fi if [[ "$behind" -gt 0 ]]; then echo " $behind commits behind" fi ((warnings++)) else echo -e "${GREEN}✅ $module: Synchronized with remote${NC}" fi cd "$REPO_ROOT" fi done # Check parent repository status echo -e "${BLUE}🏠 Checking parent repository...${NC}" if ! git diff --quiet || ! git diff --cached --quiet; then echo -e "${YELLOW}⚠️ Parent repository has uncommitted changes${NC}" git status --short | sed 's/^/ /' ((warnings++)) else echo -e "${GREEN}✅ Parent repository is clean${NC}" fi # Check for submodule pointer mismatches echo -e "${BLUE}🔗 Checking submodule pointers...${NC}" for module in "${(k)submodules[@]}"; do local module_path="${submodules[$module]}" if [[ -d "$module_path" ]]; then local submodule_commit=$(git ls-tree HEAD "$module_path" | awk '{print $3}') local actual_commit=$(cd "$module_path" && git rev-parse HEAD) if [[ "$submodule_commit" != "$actual_commit" ]]; then echo -e "${YELLOW}⚠️ $module: Submodule pointer mismatch${NC}" echo " Pointer: $submodule_commit" echo " Actual: $actual_commit" ((warnings++)) else echo -e "${GREEN}✅ $module: Submodule pointer correct${NC}" fi fi done # Summary echo "" echo -e "${BLUE}📊 Consistency Check Summary:${NC}" echo " 📦 Modules checked: ${#submodules}" if [[ $issues -eq 0 && $warnings -eq 0 ]]; then echo -e "${GREEN} ✅ No issues found${NC}" echo -e "${GREEN}🎉 All projects are consistent!${NC}" log "INFO" "Consistency check passed: no issues found" return 0 else if [[ $issues -gt 0 ]]; then echo -e "${RED} ❌ Issues: $issues${NC}" fi if [[ $warnings -gt 0 ]]; then echo -e "${YELLOW} ⚠️ Warnings: $warnings${NC}" fi echo "" echo -e "${BLUE}💡 Recommended actions:${NC}" echo " • Commit uncommitted changes in affected modules" echo " • Switch to expected branches where needed" echo " • Run: ./claude/scripts/update-submodules.sh --all --auto" echo " • Use session-end.sh for proper session cleanup" log "WARNING" "Consistency check found $issues issues and $warnings warnings" return 1 fi } # Handle special operations if [[ "$VALIDATE_ONLY" == true ]]; then validate_url_consistency exit $? fi if [[ "$SYNC_GITMODULES" == true ]]; then sync_gitmodules exit 0 fi if [[ -n "$ADD_MODULE" ]]; then add_submodule "$ADD_MODULE" exit $? fi if [[ -n "$REMOVE_MODULE" ]]; then remove_submodule "$REMOVE_MODULE" exit $? fi if [[ "$CHECK_CONSISTENCY" == true ]]; then check_project_consistency exit $? fi # Main execution success_count=0 total_count=0 if [[ "$UPDATE_ALL" == true ]]; then echo -e "${BLUE}📚 Updating all submodules...${NC}" for module_name in "${(k)submodules[@]}"; do module_path="${submodules[$module_name]}" ((total_count++)) if update_submodule "$module_name" "$module_path"; then ((success_count++)) fi done else echo -e "${BLUE}📖 Updating submodule: $SPECIFIC_MODULE${NC}" if [[ -z "${submodules[$SPECIFIC_MODULE]}" ]]; then echo -e "${RED}❌ Submodule '$SPECIFIC_MODULE' not found${NC}" echo "Available submodules:" for name in "${(k)submodules[@]}"; do echo " - $name (path: ${submodules[$name]})" done exit 1 fi module_path="${submodules[$SPECIFIC_MODULE]}" ((total_count++)) if update_submodule "$SPECIFIC_MODULE" "$module_path"; then ((success_count++)) fi fi # Summary echo "" echo -e "${BLUE}📊 Summary:${NC}" echo " 📦 Modules processed: $total_count" echo " ✅ Updates applied: $success_count" echo " 📝 No changes: $((total_count - success_count))" if [[ "$DRY_RUN" == true ]]; then echo "" echo -e "${YELLOW}🔍 This was a dry run. To apply changes, run without --dry-run${NC}" exit 0 fi # Auto-commit if requested and changes exist if [[ "$success_count" -gt 0 ]]; then if [[ "$AUTO_COMMIT" == true ]]; then echo "" echo -e "${BLUE}💾 Auto-committing submodule updates...${NC}" commit_message="Update submodules 📦 Updated modules: $success_count/$total_count $(git diff --cached --name-only | sed 's/^/- /') 🤖 Generated with submodule auto-update $(date_cmd '+%Y-%m-%d %H:%M:%S')" if git commit -m "$commit_message"; then echo -e "${GREEN}✅ Changes committed successfully${NC}" log "INFO" "Auto-committed $success_count submodule updates" else echo -e "${RED}❌ Failed to commit changes${NC}" log "ERROR" "Failed to auto-commit submodule updates" fi else echo "" echo -e "${YELLOW}📝 Changes staged but not committed. Run 'git commit' to commit them.${NC}" echo "Or use --auto flag to commit automatically." fi fi echo "" echo -e "${GREEN}🎉 Submodule update completed!${NC}" log "INFO" "Submodule update completed: $success_count/$total_count updated"