#!/usr/bin/env bash set -euo pipefail # ── 경로 설정 ──────────────────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" AGENT_OPS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" PROJECT_ROOT="$(cd "$AGENT_OPS_DIR/.." && pwd)" # ── 색상 ───────────────────────────────────────────────────────────────────── RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'; RESET='\033[0m' # ── 진입 파일 공통 관리 ────────────────────────────────────────────────────── source "$SCRIPT_DIR/entry-files.sh" # ── 대상 경로 해석 (폴더명 / 상대경로 / 절대경로) ──────────────────────────── resolve_target() { local input="$1" if [[ "$input" == /* ]]; then [[ -d "$input" ]] && echo "$input" || echo "" return fi if [[ "$input" == */* ]]; then local resolved resolved="$(cd "$input" 2>/dev/null && pwd)" || { echo ""; return; } echo "$resolved" return fi local sibling="$(dirname "$PROJECT_ROOT")/$input" [[ -d "$sibling" ]] && echo "$sibling" || echo "" } # ── 버전 비교: v1 > v2 이면 0 반환 ─────────────────────────────────────────── version_gt() { local v1="$1" v2="$2" [[ "$v1" == "$v2" ]] && return 1 [[ "$(printf '%s\n%s' "$v1" "$v2" | sort -V | head -1)" == "$v2" ]] } # ── 버전 +1 ────────────────────────────────────────────────────────────────── bump_version() { bash "$SCRIPT_DIR/bump-version.sh" "$1" } # ── 폴더 동기화 (rules.md 제외, 삭제된 파일도 반영) ───────────────────────── sync_folder() { local src="$1" dst="$2" exclude="${3:-}" mkdir -p "$dst" # dst에서 src에 없는 항목 제거 (exclude 파일 보존) find "$dst" -mindepth 1 -maxdepth 1 | while IFS= read -r item; do local name name="$(basename "$item")" [[ -n "$exclude" && "$name" == "$exclude" ]] && continue if [[ ! -e "$src/$name" ]]; then rm -rf "$item" fi done # src에서 dst로 복사 (exclude 파일 제외) find "$src" -mindepth 1 -maxdepth 1 | while IFS= read -r item; do local name name="$(basename "$item")" [[ -n "$exclude" && "$name" == "$exclude" ]] && continue rm -rf "$dst/$name" cp -r "$item" "$dst/" done } sync_common() { local src="$1" dst="$2" sync_folder "$src/rules/common" "$dst/rules/common" "rules.md" sync_folder "$src/skills/common" "$dst/skills/common" sync_folder "$src/bin" "$dst/bin" } create_project_agent_ops_dirs() { local agent_ops_dir="$1" mkdir -p "$agent_ops_dir/rules/project/domain" mkdir -p "$agent_ops_dir/rules/private" mkdir -p "$agent_ops_dir/skills/project" } copy_common_scaffold() { local src="$1" dst="$2" mkdir -p "$dst/rules" "$dst/skills" cp "$src/.version" "$dst/" rm -rf "$dst/bin" "$dst/rules/common" "$dst/skills/common" cp -r "$src/bin" "$dst/" cp -r "$src/rules/common" "$dst/rules/" cp -r "$src/skills/common" "$dst/skills/" create_project_agent_ops_dirs "$dst" } discover_agent_ops_targets() { local parent parent="$(dirname "$PROJECT_ROOT")" find "$parent" -mindepth 1 -maxdepth 1 -type d | sort | while IFS= read -r candidate; do local resolved resolved="$(cd "$candidate" && pwd)" [[ "$resolved" == "$PROJECT_ROOT" ]] && continue [[ -d "$resolved/agent-ops" ]] || continue echo "$resolved" done } agent_ops_git_paths() { printf '%s\n' "agent-ops/.version" printf '%s\n' "agent-ops/bin" printf '%s\n' "agent-ops/rules/common" printf '%s\n' "agent-ops/skills/common" local f for f in "${AGENT_OPS_ENTRY_FILES[@]}"; do if [[ -e "$f" ]] || git ls-files --error-unmatch "$f" >/dev/null 2>&1; then printf '%s\n' "$f" fi done } commit_and_push_agent_ops_scope() { local repo="$1" message="$2" if ! git -C "$repo" rev-parse --is-inside-work-tree >/dev/null 2>&1; then echo -e "${RED}Error: git 저장소가 아닙니다: $repo${RESET}" return 1 fi ( cd "$repo" mapfile -t paths < <(agent_ops_git_paths) git add -- "${paths[@]}" if git diff --cached --quiet -- "${paths[@]}"; then echo " agent-ops 변경 없음: commit 건너뜀" else git commit -m "$message" -- "${paths[@]}" fi git push ) } sync_framework_to_target() { local target="$1" local dst_ao="$target/agent-ops" local src_ver src_ver="$(cat "$SRC_AO/.version" 2>/dev/null || echo "0.0.0")" echo "▶ $(basename "$PROJECT_ROOT") → $(basename "$target")" if ! git -C "$target" rev-parse --is-inside-work-tree >/dev/null 2>&1; then echo -e "${RED}Error: git 저장소가 아닙니다: $target${RESET}" return 1 fi if [[ ! -d "$dst_ao" ]]; then echo " 최초 진행: agent-ops 공통 scaffold 복사" copy_common_scaffold "$SRC_AO" "$dst_ao" apply_agent_ops_entry_files "$SRC_AO/rules/common/rules.md" "$target" echo -e "${YELLOW} init-agent-ops 스킬로 초기화를 진행하세요.${RESET}" else echo " 이후 진행: common/ 동기화" sync_common "$SRC_AO" "$dst_ao" cp "$SRC_AO/.version" "$dst_ao/.version" apply_agent_ops_entry_files "$SRC_AO/rules/common/rules.md" "$target" fi commit_and_push_agent_ops_scope "$target" "sync: agent-ops from $(basename "$PROJECT_ROOT") v$src_ver" echo -e "${GREEN}✓ 완료 → $(basename "$target")${RESET}" } # ── 사용법 ──────────────────────────────────────────────────────────────────── usage() { echo "사용법: $0 [--pull] [target]" echo "" echo " (기본) push 동기화" echo " --pull target(agentic-framework) → 현재 프로젝트 로 pull" echo "" echo " target: 폴더명 (동일 레벨 탐색) | 상대경로 | 절대경로" echo " agentic-framework에서 target 생략 시 상위 폴더의 agent-ops 적용 프로젝트 전체 동기화" } # ───────────────────────────────────────────────────────────────────────────── PULL_MODE="0" if [[ "${1:-}" == "--pull" ]]; then PULL_MODE="1" shift fi TARGET_INPUT="${1:-}" SRC_AO="$AGENT_OPS_DIR" IS_FRAMEWORK="$([[ -f "$PROJECT_ROOT/.agent-ops-source" ]] && echo "1" || echo "0")" # ── pull 모드: agentic-framework → 현재 프로젝트 ───────────────────────────── if [[ "$PULL_MODE" == "1" ]]; then if [[ -z "$TARGET_INPUT" ]]; then usage; exit 1 fi TARGET="$(resolve_target "$TARGET_INPUT")" if [[ -z "$TARGET" ]]; then echo -e "${RED}Error: 대상을 찾을 수 없습니다: $TARGET_INPUT${RESET}" exit 1 fi DST_AO="$TARGET/agent-ops" if [[ "$IS_FRAMEWORK" == "1" ]]; then echo -e "${RED}Error: agentic-framework에서는 --pull을 사용할 수 없습니다.${RESET}" exit 1 fi if [[ ! -f "$TARGET/.agent-ops-source" ]]; then echo -e "${RED}Error: target이 agentic-framework가 아닙니다 (.agent-ops-source 없음)${RESET}" exit 1 fi SRC_VER="$(cat "$SRC_AO/.version" 2>/dev/null || echo "0.0.0")" DST_VER="$(cat "$DST_AO/.version" 2>/dev/null || echo "0.0.0")" echo "▶ $(basename "$TARGET") → $(basename "$PROJECT_ROOT") (pull)" echo " 현재 버전: $SRC_VER | framework 버전: $DST_VER" sync_common "$DST_AO" "$SRC_AO" cp "$DST_AO/.version" "$SRC_AO/.version" apply_agent_ops_entry_files "$DST_AO/rules/common/rules.md" "$PROJECT_ROOT" cd "$PROJECT_ROOT" git add agent-ops/rules/common agent-ops/skills/common agent-ops/bin agent-ops/.version \ "${AGENT_OPS_ENTRY_FILES[@]}" git commit -m "sync: pull from agentic-framework v$DST_VER" git push echo -e "${GREEN}✓ 완료 (pull v$DST_VER)${RESET}" exit 0 fi # ── agentic-framework에서 다른 프로젝트로 ──────────────────────────────────── if [[ "$IS_FRAMEWORK" == "1" ]]; then if [[ -n "$TARGET_INPUT" ]]; then TARGET="$(resolve_target "$TARGET_INPUT")" if [[ -z "$TARGET" ]]; then echo -e "${RED}Error: 대상을 찾을 수 없습니다: $TARGET_INPUT${RESET}" exit 1 fi sync_framework_to_target "$TARGET" else mapfile -t TARGETS < <(discover_agent_ops_targets) if [[ "${#TARGETS[@]}" -eq 0 ]]; then echo -e "${YELLOW}agent-ops가 적용된 sibling 프로젝트가 없습니다.${RESET}" exit 0 fi STATUS=0 for TARGET in "${TARGETS[@]}"; do if ! sync_framework_to_target "$TARGET"; then STATUS=1 fi done exit "$STATUS" fi exit 0 fi # ── 일반 프로젝트에서 agentic-framework로 (push) ───────────────────────────── if [[ -z "$TARGET_INPUT" ]]; then TARGET_INPUT="agentic-framework" fi TARGET="$(resolve_target "$TARGET_INPUT")" if [[ -z "$TARGET" ]]; then echo -e "${RED}Error: 대상을 찾을 수 없습니다: $TARGET_INPUT${RESET}" exit 1 fi DST_AO="$TARGET/agent-ops" echo "▶ $(basename "$PROJECT_ROOT") → $(basename "$TARGET")" if [[ ! -f "$TARGET/.agent-ops-source" ]]; then echo -e "${RED}Error: target이 agentic-framework가 아닙니다 (.agent-ops-source 없음)${RESET}" exit 1 fi SRC_VER="$(cat "$SRC_AO/.version" 2>/dev/null || echo "0.0.0")" DST_VER="$(cat "$DST_AO/.version" 2>/dev/null || echo "0.0.0")" echo " 현재 버전: $SRC_VER | framework 버전: $DST_VER" if version_gt "$DST_VER" "$SRC_VER"; then echo -e "${RED}⚠ 버전 충돌: framework($DST_VER) > current($SRC_VER)${RESET}" echo -e "${YELLOW} 먼저 sync-pull로 내려받은 뒤 재시도하세요.${RESET}" exit 1 fi NEW_VER="$(bump_version "$SRC_VER")" sync_common "$SRC_AO" "$DST_AO" echo "$NEW_VER" > "$SRC_AO/.version" echo "$NEW_VER" > "$DST_AO/.version" commit_and_push_agent_ops_scope "$TARGET" "sync: from $(basename "$PROJECT_ROOT") v$NEW_VER" commit_and_push_agent_ops_scope "$PROJECT_ROOT" "sync: to agentic-framework v$NEW_VER" echo -e "${GREEN}✓ 완료 (v$SRC_VER → v$NEW_VER)${RESET}"