@walwal-harness/cli 6.0.3 → 6.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,262 +0,0 @@
1
- #!/bin/bash
2
- # init-ref-docs.sh — 감지된 스택에 따라 .harness/ref/<role>-<stack>.md 를 준비한다.
3
- #
4
- # bash 혼자서는 Claude 의 WebSearch/WebFetch 를 호출할 수 없으므로 이 스크립트는:
5
- # 1. scan-result.json 에서 감지된 스택 목록을 계산
6
- # 2. --placeholder 모드: 최소 frontmatter + TODO 본문을 저장 (AC 검증용 · 파이프라인 unblock 용)
7
- # 3. --claude-prompt 모드: Claude 세션이 실제 WebSearch + WebFetch + synthesis 로 채울 수 있도록 프롬프트 템플릿을 stdout 출력
8
- # 4. --register 모드: Claude 가 실제 본문을 작성한 뒤 호출해 .generated.json 에 sources / generated_at 을 등록
9
- #
10
- # 사용 예:
11
- # bash scripts/init-ref-docs.sh --dry-run .
12
- # bash scripts/init-ref-docs.sh --yes --placeholder --stack swift --role fe .
13
- # bash scripts/init-ref-docs.sh --claude-prompt --stack swift --role fe .
14
- # bash scripts/init-ref-docs.sh --register --stack swift --role fe --sources "https://a,https://b" .
15
- set -e
16
-
17
- # ───── 옵션 파싱 ─────
18
- MODE="interactive" # interactive | dry-run | placeholder | claude-prompt | register
19
- YES=false
20
- REFRESH=false
21
- STACK=""
22
- ROLE=""
23
- SOURCES=""
24
- PROJECT_ROOT="."
25
-
26
- while [[ $# -gt 0 ]]; do
27
- case "$1" in
28
- --dry-run) MODE="dry-run"; shift ;;
29
- --placeholder) MODE="placeholder"; shift ;;
30
- --claude-prompt) MODE="claude-prompt"; shift ;;
31
- --register) MODE="register"; shift ;;
32
- --yes) YES=true; shift ;;
33
- --refresh) REFRESH=true; shift ;;
34
- --stack) STACK="$2"; shift 2 ;;
35
- --role) ROLE="$2"; shift 2 ;;
36
- --sources) SOURCES="$2"; shift 2 ;;
37
- -h|--help)
38
- sed -n '2,16p' "$0" | sed 's/^# //; s/^#//'
39
- exit 0 ;;
40
- *) PROJECT_ROOT="$1"; shift ;;
41
- esac
42
- done
43
-
44
- SCAN="${PROJECT_ROOT}/.harness/actions/scan-result.json"
45
- REF_DIR="${PROJECT_ROOT}/.harness/ref"
46
- META="${REF_DIR}/.generated.json"
47
- ARCHIVE_DIR="${PROJECT_ROOT}/.harness/archive"
48
- mkdir -p "$REF_DIR" "$ARCHIVE_DIR"
49
- [ -f "$META" ] || echo "{}" > "$META"
50
-
51
- # ───── 감지된 스택 목록 ─────
52
- detect_stacks() {
53
- if [ ! -f "$SCAN" ]; then
54
- echo "ERROR: scan-result.json 이 없습니다. 먼저 bash scripts/scan-project.sh ${PROJECT_ROOT} 실행하세요." >&2
55
- exit 1
56
- fi
57
- local fe be native
58
- fe=$(jq -r '.tech_stack.frontend' "$SCAN")
59
- be=$(jq -r '.tech_stack.backend' "$SCAN")
60
- native=$(jq -r '.tech_stack.is_native_app // false' "$SCAN")
61
-
62
- # 네이티브 앱은 fe 만 기록 (be 는 null)
63
- if [ "$fe" != "unknown" ] && [ "$fe" != "null" ]; then
64
- echo "fe:$fe"
65
- fi
66
- if [ "$native" != "true" ] && [ "$be" != "unknown" ] && [ "$be" != "null" ]; then
67
- echo "be:$be"
68
- fi
69
- }
70
-
71
- # ───── placeholder 본문 ─────
72
- write_placeholder() {
73
- local role="$1" stack="$2" path="$3"
74
- local ts
75
- ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
76
- cat > "$path" <<REF
77
- ---
78
- docmeta:
79
- id: ref-${role}-${stack}
80
- stack: ${stack}
81
- role: ${role}
82
- language: ${stack}
83
- generated_at: ${ts}
84
- generator: init-ref-docs.sh (placeholder)
85
- sources: []
86
- version: 0
87
- runner:
88
- dev_command: null
89
- start_command: null
90
- install_command: null
91
- paths:
92
- source_roots: []
93
- test_roots: []
94
- config_files: []
95
- api:
96
- base_url: null
97
- gateway: null
98
- validation:
99
- pre_eval_gate: []
100
- functional_tests: []
101
- visual:
102
- enabled: false
103
- reason: "placeholder — not yet filled by Claude"
104
- manual_check: "bash init.sh refresh-ref ${role} ${stack} 실행 후 Claude 세션에서 WebSearch + WebFetch 로 채우기"
105
- anti_pattern_rules: []
106
- ---
107
-
108
- # Ref — ${stack} (${role})
109
-
110
- > **Status**: PLACEHOLDER — Claude 가 WebSearch + WebFetch 로 채워야 함.
111
- > 실행: bash scripts/init-ref-docs.sh --claude-prompt --stack ${stack} --role ${role}
112
-
113
- ## 1. Runner
114
- TODO
115
-
116
- ## 2. Paths / Source Layout
117
- TODO
118
-
119
- ## 3. Best Practices
120
- TODO
121
-
122
- ## 4. Anti-Patterns (→ gotchas 시드 후보)
123
- TODO
124
- REF
125
- }
126
-
127
- # ───── Claude 용 프롬프트 ─────
128
- # React 계열 스택 (react / nextjs / vite-react) 일 때는
129
- # skills/generator-frontend/references/_web-react-legacy/ 의 4개 문서를
130
- # 로컬 seed 로 프롬프트에 포함시킨다. 이 seed 는 새 ref-docs 작성 시
131
- # "기존 웹 React 가이드" 참조용이며 WebSearch 결과와 병합된다.
132
- LEGACY_SEED_DIR="$(dirname "$0")/../skills/generator-frontend/references/_web-react-legacy"
133
- emit_claude_prompt() {
134
- local role="$1" stack="$2" path="$3"
135
- local legacy_hint=""
136
- if [ "$role" = "fe" ] && [ -d "$LEGACY_SEED_DIR" ]; then
137
- case "$stack" in
138
- react|nextjs|vite-react|nuxt|vue|svelte|angular|*web*)
139
- legacy_hint="
140
- Local seed (병합 대상 · 이미 검증된 웹/React best practice):
141
- $(ls "$LEGACY_SEED_DIR" 2>/dev/null | sed 's|^| - '"$LEGACY_SEED_DIR"'/|')
142
- "
143
- ;;
144
- esac
145
- fi
146
- cat <<PROMPT
147
- ───────────────────────────────────────────────
148
- Claude 세션에 아래 프롬프트를 전달하세요
149
- (또는 현재 세션이라면 그대로 실행):
150
- ───────────────────────────────────────────────
151
-
152
- 목표: ${path} 를 생성한다.
153
-
154
- 1. WebSearch 로 다음 쿼리 실행: "${stack} best practices 2025 site:docs.*"
155
- 2. 상위 3개 공식 문서 URL 을 WebFetch 로 가져온다.
156
- 3. 결과를 종합해 다음 스키마로 YAML frontmatter + 본문을 작성한다:
157
- - docmeta: { id, stack, role, language, generated_at, generator, sources, version }
158
- - runner: { dev_command, start_command, install_command }
159
- - paths: { source_roots, test_roots, config_files }
160
- - api: { base_url, gateway }
161
- - validation:
162
- pre_eval_gate: [...]
163
- functional_tests: [...]
164
- visual: { enabled, reason, manual_check }
165
- anti_pattern_rules: [{ id, pattern_type: "grep"|"lint_tool", pattern|tool+args, paths, severity }]
166
- 4. 본문에는 Runner / Paths / Best Practices / Anti-Patterns 섹션 작성.
167
- 5. 완료 후: bash scripts/init-ref-docs.sh --register --stack ${stack} --role ${role} --sources "<url1>,<url2>,<url3>" ${PROJECT_ROOT}
168
- ${legacy_hint}
169
- ───────────────────────────────────────────────
170
- PROMPT
171
- }
172
-
173
- # ───── register: 메타 기록 ─────
174
- register_meta() {
175
- local role="$1" stack="$2" sources_csv="$3"
176
- local ts key sources_json
177
- ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
178
- key="${role}-${stack}"
179
- sources_json=$(echo "$sources_csv" | awk -F',' 'BEGIN{printf "["} {for(i=1;i<=NF;i++){printf (i>1?",":"") "\"" $i "\""}} END{printf "]"}')
180
- [ -z "$sources_csv" ] && sources_json="[]"
181
- jq --arg k "$key" --arg ts "$ts" --argjson s "$sources_json" '.[$k] = {"generated_at": $ts, "sources": $s, "status": "filled"}' "$META" > "${META}.tmp" && mv "${META}.tmp" "$META"
182
- echo "registered: $key (sources=$(echo "$sources_json" | jq 'length'))"
183
- }
184
-
185
- # ───── 한 스택 처리 ─────
186
- process_one() {
187
- local role="$1" stack="$2"
188
- local path="${REF_DIR}/${role}-${stack}.md"
189
-
190
- if [ -f "$path" ] && ! $REFRESH; then
191
- echo "skip: $path 이미 존재 (--refresh 로 강제 재생성)"
192
- return 0
193
- fi
194
- if [ -f "$path" ] && $REFRESH; then
195
- local backup="${ARCHIVE_DIR}/ref-${role}-${stack}-$(date +%Y%m%d%H%M%S).md"
196
- cp "$path" "$backup"
197
- echo "archived: $backup"
198
- fi
199
-
200
- local answer="y"
201
- if ! $YES; then
202
- printf "Generate ref-docs for %s-%s? [y/N] " "$role" "$stack"
203
- read -r answer </dev/tty || answer="n"
204
- fi
205
- case "$answer" in
206
- y|Y|yes|YES) ;;
207
- *) echo "skipped: $role-$stack"; return 3 ;;
208
- esac
209
-
210
- case "$MODE" in
211
- placeholder)
212
- write_placeholder "$role" "$stack" "$path"
213
- local ts; ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
214
- jq --arg k "${role}-${stack}" --arg ts "$ts" '.[$k] = {"generated_at": $ts, "sources": [], "status": "placeholder"}' "$META" > "${META}.tmp" && mv "${META}.tmp" "$META"
215
- echo "wrote: $path (placeholder)"
216
- ;;
217
- claude-prompt)
218
- emit_claude_prompt "$role" "$stack" "$path"
219
- ;;
220
- *)
221
- echo "ERROR: interactive 모드는 --placeholder 또는 --claude-prompt 중 하나를 지정하세요." >&2
222
- return 1
223
- ;;
224
- esac
225
- }
226
-
227
- # ───── main ─────
228
- case "$MODE" in
229
- dry-run)
230
- echo "detected stacks:"
231
- detect_stacks | sed 's/^/ - /'
232
- echo ""
233
- echo "ref files 예상 경로 (미존재 시 생성 대상):"
234
- while IFS=':' read -r role stack; do
235
- path="${REF_DIR}/${role}-${stack}.md"
236
- if [ -f "$path" ]; then
237
- echo " [exists] $path"
238
- else
239
- echo " [create] $path"
240
- fi
241
- done < <(detect_stacks)
242
- ;;
243
- register)
244
- [ -z "$STACK" ] || [ -z "$ROLE" ] && { echo "ERROR: --register 는 --stack --role --sources 필요" >&2; exit 1; }
245
- register_meta "$ROLE" "$STACK" "$SOURCES"
246
- ;;
247
- placeholder|claude-prompt)
248
- if [ -n "$STACK" ] && [ -n "$ROLE" ]; then
249
- process_one "$ROLE" "$STACK"
250
- else
251
- # 감지된 전체 스택을 순회
252
- while IFS=':' read -r role stack; do
253
- process_one "$role" "$stack"
254
- done < <(detect_stacks)
255
- fi
256
- ;;
257
- interactive)
258
- echo "ERROR: 모드 지정 필요 — --dry-run | --placeholder | --claude-prompt | --register" >&2
259
- echo "자세한 사용법: bash $0 --help" >&2
260
- exit 1
261
- ;;
262
- esac
@@ -1,113 +0,0 @@
1
- #!/bin/bash
2
- # harness-audit.sh — Audit log 헬퍼
3
- #
4
- # 라이프사이클: Planner 시작 → Eval 통과 (1 sprint cycle)
5
- # 새 Planner/Dispatcher 사이클 시작 시 이전 로그는 archive로 이동.
6
- #
7
- # 사용법:
8
- # source scripts/lib/harness-audit.sh
9
- # audit_log "gen-backend" "develop" "start" "apps/service-user/" "User 서비스 CRUD 구현"
10
- # audit_log "eval-func" "review" "complete" "POST /api/auth" "FAIL: 409→400 contract 불일치"
11
- # audit_log "gen-backend" "handoff" "start" "→eval-functional" ""
12
-
13
- AUDIT_LOG_FILE=""
14
-
15
- # ─────────────────────────────────────────
16
- # init_audit — audit.log 경로 설정 + 헤더 생성
17
- # Args: $1 = project root
18
- # ─────────────────────────────────────────
19
- init_audit() {
20
- local project_root="${1:-.}"
21
- AUDIT_LOG_FILE="$project_root/.harness/actions/audit.log"
22
-
23
- # 디렉토리 보장
24
- mkdir -p "$(dirname "$AUDIT_LOG_FILE")"
25
- }
26
-
27
- # ─────────────────────────────────────────
28
- # reset_audit — 새 사이클 시작 시 로그 초기화
29
- # 이전 로그가 있으면 archive로 이동
30
- # Args: $1 = project root, $2 = sprint number (optional)
31
- # ─────────────────────────────────────────
32
- reset_audit() {
33
- local project_root="${1:-.}"
34
- local sprint_num="${2:-0}"
35
- init_audit "$project_root"
36
-
37
- # 기존 로그가 있으면 archive로 이동
38
- if [ -f "$AUDIT_LOG_FILE" ] && [ -s "$AUDIT_LOG_FILE" ]; then
39
- local archive_dir="$project_root/.harness/archive"
40
- mkdir -p "$archive_dir"
41
- local ts
42
- ts=$(date +%Y%m%d_%H%M%S)
43
- mv "$AUDIT_LOG_FILE" "$archive_dir/audit_s${sprint_num}_${ts}.log"
44
- fi
45
-
46
- # 새 로그 헤더
47
- cat > "$AUDIT_LOG_FILE" <<HEADER
48
- # Harness Audit Log
49
- # Cycle started: $(date -u +%Y-%m-%dT%H:%M:%SZ)
50
- # Format: TIMESTAMP | AGENT | ACTION | STATUS | TARGET | DETAIL
51
- #─────────────────────────────────────────────────────────────────────────
52
- HEADER
53
- }
54
-
55
- # ─────────────────────────────────────────
56
- # audit_log — 로그 엔트리 추가
57
- #
58
- # Args:
59
- # $1 = agent (planner, gen-backend, gen-frontend, eval-func, eval-visual)
60
- # $2 = action (develop, review, handoff, plan, gate)
61
- # $3 = status (start, complete, fail, skip)
62
- # $4 = target (파일 경로, 엔드포인트, 에이전트명 등)
63
- # $5 = detail (설명, 결과 등)
64
- # ─────────────────────────────────────────
65
- audit_log() {
66
- if [ -z "$AUDIT_LOG_FILE" ] || [ ! -f "$AUDIT_LOG_FILE" ]; then
67
- return 0 # audit 미초기화 시 silent pass
68
- fi
69
-
70
- local agent="${1:-unknown}"
71
- local action="${2:-unknown}"
72
- local log_status="${3:-unknown}"
73
- local target="${4:-}"
74
- local detail="${5:-}"
75
- local ts
76
- ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
77
-
78
- # 고정폭 포맷
79
- printf "%-20s | %-14s | %-8s | %-8s | %-30s | %s\n" \
80
- "$ts" "$agent" "$action" "$log_status" "$target" "$detail" >> "$AUDIT_LOG_FILE"
81
- }
82
-
83
- # ─────────────────────────────────────────
84
- # audit_handoff — 핸즈오프 기록 (편의 함수)
85
- # Args: $1=from_agent, $2=to_agent, $3=status(start|complete)
86
- # ─────────────────────────────────────────
87
- audit_handoff() {
88
- audit_log "$1" "handoff" "$3" "→${2}" ""
89
- }
90
-
91
- # ─────────────────────────────────────────
92
- # audit_gate — 게이트 체크 기록 (편의 함수)
93
- # Args: $1=gate_name, $2=status(pass|fail), $3=detail
94
- # ─────────────────────────────────────────
95
- audit_gate() {
96
- audit_log "system" "gate" "$2" "$1" "$3"
97
- }
98
-
99
- # ─────────────────────────────────────────
100
- # render_audit — audit.log를 읽기 좋게 출력
101
- # Args: $1 = project root (optional)
102
- # ─────────────────────────────────────────
103
- render_audit() {
104
- local project_root="${1:-.}"
105
- local log_file="$project_root/.harness/actions/audit.log"
106
-
107
- if [ ! -f "$log_file" ]; then
108
- echo " (audit log not yet created)"
109
- return 0
110
- fi
111
-
112
- cat "$log_file"
113
- }
@@ -1,148 +0,0 @@
1
- #!/bin/bash
2
- # harness-guardrail.sh — 런타임 파일 소유권 검증
3
- # 에이전트 전환 시 이전 에이전트가 권한 밖 파일을 수정했는지 git diff로 검증.
4
- #
5
- # 사용법: source lib/harness-guardrail.sh && verify_file_ownership "$PROJECT_ROOT"
6
- # 반환값: 0=통과, 1=위반 발견 (위반 목록을 stdout에 출력)
7
-
8
- # ─────────────────────────────────────────
9
- # Agent → 허용 경로 매핑
10
- # ─────────────────────────────────────────
11
- get_allowed_paths() {
12
- local agent="$1"
13
- case "$agent" in
14
- planner)
15
- echo "AGENTS.md"
16
- echo ".harness/actions/"
17
- echo ".harness/progress.json"
18
- echo ".harness/progress.log"
19
- ;;
20
- generator-backend)
21
- echo "apps/gateway/"
22
- echo "apps/service-"
23
- echo "libs/"
24
- echo ".harness/actions/sprint-contract.md"
25
- echo ".harness/actions/feature-list.json"
26
- echo ".harness/progress.json"
27
- echo "package.json"
28
- echo "nest-cli.json"
29
- echo "tsconfig"
30
- echo "docker-compose"
31
- ;;
32
- generator-frontend)
33
- echo "apps/web/"
34
- echo "apps/flutter/"
35
- echo "lib/"
36
- echo ".harness/actions/sprint-contract.md"
37
- echo ".harness/actions/feature-list.json"
38
- echo ".harness/progress.json"
39
- echo "package.json"
40
- echo "tsconfig"
41
- echo "pubspec"
42
- ;;
43
- evaluator-code-quality)
44
- echo ".harness/actions/evaluation-code-quality.md"
45
- echo ".harness/progress.json"
46
- ;;
47
- evaluator-functional)
48
- echo ".harness/actions/evaluation-functional.md"
49
- echo ".harness/progress.json"
50
- echo "tests/"
51
- echo "test/"
52
- echo "e2e/"
53
- echo "playwright"
54
- ;;
55
- evaluator-visual)
56
- echo ".harness/actions/evaluation-visual.md"
57
- echo ".harness/progress.json"
58
- ;;
59
- dispatcher)
60
- echo ".harness/"
61
- ;;
62
- *)
63
- echo "**" # unknown agent — allow all (safe fallback)
64
- ;;
65
- esac
66
- }
67
-
68
- # ─────────────────────────────────────────
69
- # verify_file_ownership — git diff 기반 검증
70
- #
71
- # Args:
72
- # $1 — project root
73
- #
74
- # Reads:
75
- # .harness/progress.json (current_agent)
76
- #
77
- # Returns: 0=pass, 1=violation found
78
- # ─────────────────────────────────────────
79
- verify_file_ownership() {
80
- local PROJECT_ROOT="$1"
81
- local PROGRESS="$PROJECT_ROOT/.harness/progress.json"
82
-
83
- if [ ! -f "$PROGRESS" ]; then return 0; fi
84
-
85
- local agent
86
- agent=$(jq -r '.current_agent // "null"' "$PROGRESS" 2>/dev/null)
87
- if [ "$agent" = "null" ] || [ -z "$agent" ]; then return 0; fi
88
-
89
- # git이 없으면 skip
90
- if ! command -v git &>/dev/null; then return 0; fi
91
-
92
- # 마지막 커밋 이후 변경된 파일 목록
93
- local -a changed_files
94
- mapfile -t changed_files < <(cd "$PROJECT_ROOT" && git diff --name-only HEAD 2>/dev/null)
95
-
96
- # staged 파일도 포함
97
- local -a staged_files
98
- mapfile -t staged_files < <(cd "$PROJECT_ROOT" && git diff --name-only --cached 2>/dev/null)
99
-
100
- # 합치기 (중복 제거)
101
- local -a all_files
102
- mapfile -t all_files < <(printf '%s\n' "${changed_files[@]}" "${staged_files[@]}" | sort -u | grep -v '^$')
103
-
104
- if [ ${#all_files[@]} -eq 0 ]; then return 0; fi
105
-
106
- # 허용 경로 목록
107
- local -a allowed
108
- mapfile -t allowed < <(get_allowed_paths "$agent")
109
-
110
- # 위반 검사
111
- local -a violations=()
112
- for file in "${all_files[@]}"; do
113
- local is_allowed=false
114
- for pattern in "${allowed[@]}"; do
115
- if [[ "$file" == $pattern* ]]; then
116
- is_allowed=true
117
- break
118
- fi
119
- done
120
- if [ "$is_allowed" = false ]; then
121
- violations+=("$file")
122
- fi
123
- done
124
-
125
- if [ ${#violations[@]} -eq 0 ]; then
126
- return 0
127
- fi
128
-
129
- # 위반 보고
130
- echo ""
131
- echo " ── Guardrail Violation ────────────────"
132
- echo " Agent '$agent' modified files outside its allowed paths:"
133
- echo ""
134
- for v in "${violations[@]}"; do
135
- echo " ✗ $v"
136
- done
137
- echo ""
138
- echo " Allowed paths for $agent:"
139
- for p in "${allowed[@]}"; do
140
- echo " ✓ $p*"
141
- done
142
- echo ""
143
- echo " Action: Review these changes before proceeding."
144
- echo " ─────────────────────────────────────────"
145
- echo ""
146
-
147
- return 1
148
- }
@@ -1,57 +0,0 @@
1
- #!/bin/bash
2
- # harness-keywait.sh — 패널 refresh/quit 키 대기 헬퍼
3
- #
4
- # 사용법:
5
- # source scripts/lib/harness-keywait.sh
6
- # while true; do
7
- # render_panel
8
- # echo " [r] refresh [q] quit"
9
- # if wait_or_refresh 3; then
10
- # # 타임아웃 → 일반 주기 리프레시
11
- # :
12
- # else
13
- # # 'r' 키 즉시 리프레시 (반환값 != 0)
14
- # :
15
- # fi
16
- # done
17
- #
18
- # 키 처리:
19
- # 'r' / 'R' / Enter → 즉시 복귀 (return 1 — 강제 리프레시 신호)
20
- # 'q' / 'Q' → exit 0 (패널 종료)
21
- # 그 외 / 타임아웃 → return 0 (정상 주기 리프레시)
22
- #
23
- # TTY 가 아니면 plain sleep 으로 동작 (Claude Code Bash 도구 등에서도 안전).
24
-
25
- wait_or_refresh() {
26
- local secs="${1:-3}"
27
-
28
- # Non-TTY fallback
29
- if ! [ -t 0 ]; then
30
- sleep "$secs"
31
- return 0
32
- fi
33
-
34
- local key=""
35
- # -t: timeout, -n 1: 1글자, -s: 에코 안함
36
- # read 는 Enter 에서도 타임아웃 전에 즉시 복귀 (빈 키)
37
- if IFS= read -rsn 1 -t "$secs" key 2>/dev/null; then
38
- case "$key" in
39
- q|Q)
40
- # 커서 복원 후 종료
41
- tput cnorm 2>/dev/null || true
42
- clear
43
- exit 0
44
- ;;
45
- r|R|"")
46
- # 강제 리프레시 시그널
47
- return 1
48
- ;;
49
- *)
50
- # 기타 키는 무시하고 일반 복귀
51
- return 0
52
- ;;
53
- esac
54
- fi
55
- # 타임아웃
56
- return 0
57
- }