#!/bin/bash # =================================================== # 文件操作脚本 - file-operations.sh # 用于处理文件同步、路径处理和目录结构管理 # =================================================== # 颜色定义 if [[ -t 1 ]]; then # 检查是否在终端中运行 RED=$(tput setaf 1) GREEN=$(tput setaf 2) YELLOW=$(tput setaf 3) BLUE=$(tput setaf 4) MAGENTA=$(tput setaf 5) CYAN=$(tput setaf 6) BOLD=$(tput bold) DIM=$(tput dim) NC=$(tput sgr0) # No Color else RED="" GREEN="" YELLOW="" BLUE="" MAGENTA="" CYAN="" BOLD="" DIM="" NC="" fi # 工具函数 log_info() { printf "%s%s[INFO]%s %s\n" "${BOLD}" "${GREEN}" "${NC}" "$1" } log_debug() { if [[ "$DEBUG_MODE" == "true" ]]; then printf "%s%s[DEBUG]%s %s\n" "${BOLD}" "${MAGENTA}" "${NC}" "$1" fi } log_warn() { printf "%s%s[WARN]%s %s\n" "${BOLD}" "${YELLOW}" "${NC}" "$1" } log_error() { printf "%s%s[ERROR]%s %s\n" "${BOLD}" "${RED}" "${NC}" "$1" } # 显示分隔线 show_separator() { printf "%s%s%s\n" "${DIM}" "──────────────────────────────────────────" "${NC}" } # 显示标题 show_title() { printf "\n%s%s%s%s\n\n" "${BOLD}" "${CYAN}" "$1" "${NC}" } # 显示进度条 show_progress() { local current=$1 local total=$2 local width=30 local percentage=$((current * 100 / total)) local completed=$((width * current / total)) local remaining=$((width - completed)) printf "\r%s%s[%s%s%s] %d%%" \ "${BOLD}" "${CYAN}" \ "$(printf '%*s' "$completed" | tr ' ' '●')" \ "$(printf '%*s' "$remaining" | tr ' ' '○')" \ "${NC}" \ "$percentage" } # 初始化同步目录结构 init_sync_dirs() { if [[ -z "$PROJECT_ROOT" ]]; then log_error "项目根目录未初始化" return 1 fi # 创建同步配置目录 SYNC_DIR="$PROJECT_ROOT/.sync" if [[ ! -d "$SYNC_DIR" ]]; then mkdir -p "$SYNC_DIR" || { log_error "创建同步配置目录失败: $SYNC_DIR" return 1 } log_info "已创建同步配置目录: $SYNC_DIR" fi # 创建 Git 仓库目录 GIT_DIR="$PROJECT_ROOT/.git-sync" if [[ ! -d "$GIT_DIR" ]]; then mkdir -p "$GIT_DIR" || { log_error "创建 Git 仓库目录失败: $GIT_DIR" return 1 } log_info "已创建 Git 仓库目录: $GIT_DIR" fi # 初始化历史记录文件 HISTORY_FILE="$SYNC_DIR/history" if [[ ! -f "$HISTORY_FILE" ]]; then touch "$HISTORY_FILE" || { log_error "创建历史记录文件失败: $HISTORY_FILE" return 1 } log_info "已创建历史记录文件: $HISTORY_FILE" fi # 初始化插件目录 PLUGINS_DIR="$SYNC_DIR/plugins" if [[ ! -d "$PLUGINS_DIR" ]]; then mkdir -p "$PLUGINS_DIR" || { log_error "创建插件目录失败: $PLUGINS_DIR" return 1 } log_info "已创建插件目录: $PLUGINS_DIR" fi # 初始化同步配置文件 SYNC_CONFIG_FILE="$SYNC_DIR/sync-config.yaml" if [[ ! -f "$SYNC_CONFIG_FILE" ]]; then cat > "$SYNC_CONFIG_FILE" << EOF # 工具配置 config: parallel_build: false # 是否并行编译 dry_run: false # 是否干运行 # 工作区配置 workspaces: # 示例配置 # app-name: # sync_mappings: # - source: # git_url: "https://github.com/user/repo.git" # branch: "main" # target: # sync_dir: "dist" # 要同步的目录 # git_dir: "dist" # Git 仓库中的目标目录 EOF log_info "已创建同步配置文件: $SYNC_CONFIG_FILE" fi return 0 } # 查找项目根目录 find_project_root() { local current_dir="$PWD" while [[ "$current_dir" != "/" ]]; do if [[ -f "$current_dir/pnpm-workspace.yaml" ]]; then PROJECT_ROOT="$current_dir" log_info "找到项目根目录: $PROJECT_ROOT" # 在找到根目录后,初始化相关路径 SYNC_DIR="$PROJECT_ROOT/.sync" GIT_DIR="$PROJECT_ROOT/.git-sync" SYNC_CONFIG_FILE="$SYNC_DIR/sync-config.yaml" HISTORY_FILE="$SYNC_DIR/history" PLUGINS_DIR="$SYNC_DIR/plugins" return 0 fi current_dir="$(dirname "$current_dir")" done log_error "未找到项目根目录" return 1 } # 检查并创建工作区配置目录 check_workspace_config_dir() { # 检查工作区配置目录是否存在 if [[ ! -d "$WORKSPACE_CONFIG_DIR" ]]; then log_info "工作区配置目录不存在,正在创建..." mkdir -p "$WORKSPACE_CONFIG_DIR" || { log_error "创建工作区配置目录失败: $WORKSPACE_CONFIG_DIR" return 1 } log_info "已创建工作区配置目录: $WORKSPACE_CONFIG_DIR" # 创建 .gitignore 文件 cat > "$WORKSPACE_CONFIG_DIR/.gitignore" << EOF # 忽略所有文件 * # 不忽略 .gitignore !.gitignore EOF log_info "已创建 .gitignore 文件" # 创建 README.md 文件 cat > "$WORKSPACE_CONFIG_DIR/README.md" << EOF # 工作区配置目录 此目录用于存储各个工作区的同步配置信息。 ## 配置文件格式 每个工作区对应一个 YAML 配置文件,命名格式为 \`{workspace}.yaml\`。 ### 配置示例 \`\`\`yaml # 工作区同步配置 workspace: "app-name" sync_mappings: - source: git_url: "https://github.com/user/repo.git" branch: "main" target: sync_dir: "dist" # 要同步的目录 git_dir: "dist" # Git 仓库中的目标目录 \`\`\` ## 注意事项 1. 此目录不应被 Git 追踪 2. 配置文件包含敏感信息,请妥善保管 3. 建议定期备份配置文件 EOF log_info "已创建 README.md 文件" fi # 检查目录权限 if [[ ! -w "$WORKSPACE_CONFIG_DIR" ]]; then log_error "工作区配置目录没有写入权限: $WORKSPACE_CONFIG_DIR" return 1 fi return 0 } # 同步文件操作函数 sync_files() { local source_path="$PROJECT_ROOT/$SELECTED_WORKSPACE" local dist_path="$source_path/dist" # 检查源目录 if [[ ! -d "$dist_path" ]]; then log_error "源目录不存在: $dist_path" return 1 fi # 询问是否同步项目结构 read -p "是否同步项目结构?(y/n): " sync_structure if [[ "$sync_structure" == "y" ]]; then # 同步项目结构 log_info "开始同步项目结构..." # 创建临时目录 local temp_dir=$(mktemp -d) # 复制项目结构 find "$source_path" -type f -not -path "*/node_modules/*" -not -path "*/dist/*" | while IFS= read -r file; do local rel_path="${file#$source_path/}" local target_path="$temp_dir/$rel_path" mkdir -p "$(dirname "$target_path")" cp "$file" "$target_path" done # 移动临时目录内容到目标目录 cp -r "$temp_dir"/* "$TARGET_GIT_DIR/" rm -rf "$temp_dir" fi # 同步编译结果 log_info "开始同步编译结果..." if [[ "$OS" == "windows" ]]; then # Windows 路径处理 local target_dist="${TARGET_GIT_DIR}\\dist" if [[ -d "$target_dist" ]]; then rm -rf "$target_dist" fi cp -r "$dist_path" "$target_dist" else # Unix 路径处理 local target_dist="$TARGET_GIT_DIR/dist" if [[ -d "$target_dist" ]]; then rm -rf "$target_dist" fi cp -r "$dist_path" "$target_dist" fi log_info "文件同步完成" return 0 } # 同步所有源码文件 sync_all_source_files() { local files=("$@") local total=${#files[@]} local current=0 for target_dir in "${TARGET_GIT_DIRS[@]}"; do log_info "开始同步到: $target_dir" for file in "${files[@]}"; do local rel_path="${file#$PROJECT_ROOT/$SELECTED_WORKSPACE/}" local target_path="$target_dir/$rel_path" mkdir -p "$(dirname "$target_path")" cp "$file" "$target_path" ((current++)) show_progress "$current" "$total" done printf "\n" done log_info "源码同步完成" return 0 } # 仅同步配置文件 sync_config_files() { local files=("$@") local config_files=() for file in "${files[@]}"; do if [[ "$file" =~ \.(json|yaml|yml|config\.js|config\.ts)$ ]]; then config_files+=("$file") fi done if [[ ${#config_files[@]} -eq 0 ]]; then log_error "未找到配置文件" return 1 fi sync_all_source_files "${config_files[@]}" } # 仅同步源代码 sync_source_files() { local files=("$@") local source_files=() for file in "${files[@]}"; do if [[ "$file" =~ \.(js|ts|jsx|tsx|vue|css|scss|less)$ ]]; then source_files+=("$file") fi done if [[ ${#source_files[@]} -eq 0 ]]; then log_error "未找到源代码文件" return 1 fi sync_all_source_files "${source_files[@]}" } # 记录操作历史 record_history() { if [[ -z "$HISTORY_FILE" ]]; then log_error "历史记录文件未初始化,请确保已找到项目根目录" return 1 fi local operation="$1" # 操作 local timestamp=$(date '+%Y-%m-%d %H:%M:%S') # 时间戳 echo "$timestamp|$operation" >> "$HISTORY_FILE" # 写入历史记录文件 # 保持历史记录在最大限制内 if [[ -f "$HISTORY_FILE" ]]; then tail -n "$MAX_HISTORY" "$HISTORY_FILE" > "${HISTORY_FILE}.tmp" mv "${HISTORY_FILE}.tmp" "$HISTORY_FILE" fi } # 显示操作历史 show_history() { if [[ -z "$HISTORY_FILE" ]]; then log_error "历史记录文件未初始化,请确保已找到项目根目录" return 1 fi if [[ -f "$HISTORY_FILE" ]]; then log_info "最近的操作历史:" while IFS='|' read -r timestamp operation; do echo "$timestamp - $operation" done < "$HISTORY_FILE" else log_info "暂无操作历史" fi } # 同步源码 sync_source_code() { local workspace_path="$PROJECT_ROOT/$SELECTED_WORKSPACE" local source_files=() # 收集需要同步的源码文件 for file in $(find "$workspace_path" -type f); do if [[ ! "$file" =~ /(node_modules|dist|\.git)/ ]]; then source_files+=("$file") fi done if [[ ${#source_files[@]} -eq 0 ]]; then log_error "未找到需要同步的源码文件" return 1 fi # 显示同步选项 show_title "同步源码选项" printf "%s%s%s\n" "${DIM}" "选择要同步的内容" "${NC}" show_separator local sync_options=( "同步所有源码文件" "仅同步配置文件" "仅同步源代码" "自定义同步" ) local selected_index=0 local max_index=$((${#sync_options[@]}-1)) local padding=2 while true; do clear # 显示选项列表 show_title "同步源码选项" printf "%s%s%s\n" "${DIM}" "选择要同步的内容" "${NC}" show_separator for i in "${!sync_options[@]}"; do if [[ $i -eq $selected_index ]]; then printf "%s%s%s%*s%s%s%s\n" \ "${BOLD}" "${CYAN}" "❯" \ "$padding" "" \ "${GREEN}" "${sync_options[$i]}" "${NC}" else printf "%*s%s%s%s\n" \ "$((padding + 1))" "" \ "${DIM}" "${sync_options[$i]}" "${NC}" fi done show_separator # 读取用户输入 local key_pressed="" local key read -r -n 1 key # 获取ASCII码用于调试(针对回车键) if [[ -z "$key" ]]; then [[ "$DEBUG_MODE" == "true" ]] && printf "空字符,可能是回车键\n" key_pressed="ENTER" else [[ "$DEBUG_MODE" == "true" ]] && printf "按键ASCII码: %d\n" "'$key" fi # 处理按键 case "$key" in $'\x1b') # ESC 序列 read -r -n 2 key case "$key" in "[A") # 上箭头 if [[ $selected_index -gt 0 ]]; then selected_index=$((selected_index-1)) else # 循环到最后一项 selected_index=$max_index fi ;; "[B") # 下箭头 if [[ $selected_index -lt $max_index ]]; then selected_index=$((selected_index+1)) else # 循环到第一项 selected_index=0 fi ;; esac ;; "") # 回车 case $selected_index in 0) # 同步所有源码文件 sync_all_source_files "${source_files[@]}" ;; 1) # 仅同步配置文件 sync_config_files "${source_files[@]}" ;; 2) # 仅同步源代码 sync_source_files "${source_files[@]}" ;; 3) # 自定义同步 sync_custom_files "${source_files[@]}" ;; esac return $? ;; "q") # 退出 log_error "操作已取消" return 1 ;; esac done } # 自定义同步文件选择 sync_custom_files() { local files=("$@") local selected_files=() local selected_indices=() local cursor_index=0 local max_index=$((${#files[@]}-1)) local padding=2 while true; do clear show_title "选择要同步的文件" printf "%s%s%s\n" "${DIM}" "使用数字键1选中/0取消,回车确认,q 退出" "${NC}" show_separator # 显示文件列表 for i in "${!files[@]}"; do local is_selected=false # 检查当前文件是否已被选中 for idx in "${selected_indices[@]}"; do if [[ $i -eq $idx ]]; then is_selected=true break fi done if [[ $i -eq $cursor_index ]]; then # 当前光标位置 if [[ "$is_selected" == "true" ]]; then printf "%s%s%s%*s%s%s%s\n" \ "${BOLD}" "${CYAN}" "❯" \ "$padding" "" \ "${GREEN}" "[✓] ${files[$i]#$PROJECT_ROOT/}" "${NC}" else printf "%s%s%s%*s%s%s%s\n" \ "${BOLD}" "${CYAN}" "❯" \ "$padding" "" \ "${DIM}" "[ ] ${files[$i]#$PROJECT_ROOT/}" "${NC}" fi else # 非光标位置 if [[ "$is_selected" == "true" ]]; then printf "%*s%s%s%s\n" \ "$((padding + 1))" "" \ "${GREEN}" "[✓] ${files[$i]#$PROJECT_ROOT/}" "${NC}" else printf "%*s%s%s%s\n" \ "$((padding + 1))" "" \ "${DIM}" "[ ] ${files[$i]#$PROJECT_ROOT/}" "${NC}" fi fi done show_separator # 显示操作提示 printf "\n%s%s使用上下箭头选择,数字键1选中/0取消,回车确认,q键退出%s\n" \ "${BOLD}" "${CYAN}" "${NC}" # 读取用户输入 - 统一处理方式 local key_pressed="" local key read -r -n 1 key # 获取ASCII码用于调试 if [[ -z "$key" ]]; then [[ "$DEBUG_MODE" == "true" ]] && printf "空字符,可能是回车键\n" key_pressed="ENTER" # 空字符通常是回车键 elif [[ "$key" == "1" ]]; then printf "检测到数字键1,执行选中操作\n" # 始终输出,不依赖DEBUG_MODE key_pressed="SELECT" # 选中 elif [[ "$key" == "0" ]]; then printf "检测到数字键0,执行取消选中操作\n" # 始终输出,不依赖DEBUG_MODE key_pressed="DESELECT" # 取消选中 else [[ "$DEBUG_MODE" == "true" ]] && printf "按键ASCII码: %d\n" "'$key" # 处理其他按键 case "$key" in $'\x1b') # ESC 序列,包括方向键 read -r -n 2 seq [[ "$DEBUG_MODE" == "true" ]] && printf "ESC序列: %s\n" "$seq" case "$seq" in "[A") key_pressed="UP" ;; # 上箭头 "[B") key_pressed="DOWN" ;; # 下箭头 *) key_pressed="ESC" ;; # 其他ESC序列 esac ;; "q"|"Q") # q键退出 key_pressed="QUIT" ;; *) # 其他按键忽略 key_pressed="OTHER" ;; esac fi # 调试信息 [[ "$DEBUG_MODE" == "true" ]] && log_debug "按键被解析为: $key_pressed" # 处理操作 case "$key_pressed" in "UP") # 上箭头 if [[ $cursor_index -gt 0 ]]; then cursor_index=$((cursor_index-1)) else # 如果已经是第一项,跳到最后一项 cursor_index=$max_index fi ;; "DOWN") # 下箭头 if [[ $cursor_index -lt $max_index ]]; then cursor_index=$((cursor_index+1)) else # 如果已经是最后一项,回到第一项 cursor_index=0 fi ;; "SELECT") # 数字键1 - 选中当前项 printf "处理SELECT操作 - 选中当前项: ${cursor_index}\n" # 检查当前索引是否已在选中列表中 local found=false for idx in "${selected_indices[@]}"; do if [[ $idx -eq $cursor_index ]]; then found=true break fi done # 如果未选中,则添加到选中列表 if [[ "$found" == "false" ]]; then selected_indices+=($cursor_index) printf "已添加索引 ${cursor_index} 到选中列表\n" # 对选中项排序 if [[ ${#selected_indices[@]} -gt 0 ]]; then IFS=$'\n' selected_indices=($(sort -n <<<"${selected_indices[*]}")) unset IFS fi else printf "索引 ${cursor_index} 已在选中列表中\n" fi ;; "DESELECT") # 数字键0 - 取消选中当前项 printf "处理DESELECT操作 - 取消选中当前项: ${cursor_index}\n" # 检查当前索引是否已在选中列表中 local found=false local new_indices=() # 创建新数组,排除当前索引 for idx in "${selected_indices[@]}"; do if [[ $idx -ne $cursor_index ]]; then new_indices+=($idx) else found=true # 标记找到了要删除的索引 fi done # 只有在找到并删除了索引的情况下才更新选中列表 if [[ "$found" == "true" ]]; then selected_indices=("${new_indices[@]}") printf "已从选中列表中移除索引 ${cursor_index}\n" else printf "索引 ${cursor_index} 不在选中列表中\n" fi ;; "ENTER") # 回车 - 确认选择 if [[ ${#selected_indices[@]} -eq 0 ]]; then log_error "请至少选择一个文件" sleep 2 # 暂停显示错误信息 continue fi # 重建选中文件列表 selected_files=() for idx in "${selected_indices[@]}"; do selected_files+=("${files[$idx]}") done sync_all_source_files "${selected_files[@]}" return $? ;; "QUIT") # 退出 log_error "操作已取消" return 1 ;; esac done } # 导出函数 export -f log_info export -f log_debug export -f log_warn export -f log_error export -f show_separator export -f show_title export -f show_progress export -f init_sync_dirs export -f find_project_root export -f check_workspace_config_dir export -f sync_files export -f sync_all_source_files export -f sync_config_files export -f sync_source_files export -f record_history export -f show_history export -f sync_source_code export -f sync_custom_files