@kaydash9999/banker-plugins 0.16.3 → 0.17.0

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.
Files changed (31) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +33 -0
  4. package/README.md +14 -11
  5. package/bin/banker.js +2 -2
  6. package/codex/manifest.json +2 -1
  7. package/codex/transform-matrix.md +2 -1
  8. package/commands/setup.md +2 -2
  9. package/hooks/progress-plan.mjs +408 -20
  10. package/hooks/progress.mjs +202 -50
  11. package/hooks/register.mjs +7 -1
  12. package/package.json +1 -1
  13. package/skills/3d-intro-build/references/assemble.mjs +10 -2
  14. package/skills/3d-intro-build/references/curate.mjs +10 -2
  15. package/skills/3d-intro-build/references/preview-lib.mjs +2 -0
  16. package/skills/3d-intro-build/references/serve.mjs +10 -2
  17. package/skills/3d-intro-build/references/verify-intro.mjs +10 -2
  18. package/skills/motion-graphic-make/references/curate.mjs +10 -2
  19. package/skills/motion-graphic-make/references/preview-lib.mjs +2 -0
  20. package/skills/motion-graphic-make/references/serve.mjs +10 -2
  21. package/skills/obsidizer/obsidize.mjs +10 -2
  22. package/skills/omc-patch/SKILL.md +96 -21
  23. package/skills/omc-patch/scripts/omc-patch.mjs +164 -62
  24. package/skills/omc-patch/scripts/probe-pm.mjs +192 -0
  25. package/skills/ultra-chain/SKILL.md +475 -0
  26. package/skills/ultra-chain/references/capture.mjs +232 -0
  27. package/skills/ultra-chain/references/chain-ledger.mjs +491 -0
  28. package/skills/ultra-chain/references/chain-report.mjs +257 -0
  29. package/skills/ultra-chain/references/preview-lib.mjs +258 -0
  30. package/skills/ultra-chain/references/serve.mjs +139 -0
  31. package/skills/vertical-pptx/verify_a4p.mjs +10 -2
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: omc-patch
3
- description: "OMC 플러그인이 자동 업데이트된 뒤 사라진 훅 패치를 활성 버전에 다시 적용하고 OMC 마켓플레이스 자동 업데이트를 막음. 진단, 적용, 검증, 되돌리기. 'omc-patch'/'OMC 패치'/'OMC 업데이트 뒤 패치'/'훅 timed out'/'훅 프로세스 누적'/'PC 느려짐' 시 사용."
3
+ description: "OMC 플러그인이 자동 업데이트된 뒤 사라진 훅 패치를 활성 버전에 다시 적용하고 OMC 마켓플레이스 자동 업데이트를 막음. 옛 킷이 남긴 헬퍼 자리 결함(project-memory 꺼짐)도 찾아 고침. 진단, 적용, 검증, 되돌리기. 'omc-patch'/'OMC 패치'/'OMC 업데이트 뒤 패치'/'훅 timed out'/'훅 프로세스 누적'/'PC 느려짐'/'project-memory 안 됨' 시 사용."
4
4
  argument-hint: "[check | revert]"
5
5
  ---
6
6
 
7
7
  # omc-patch — OMC 훅 패치 재적용과 자동 업데이트 고정
8
8
 
9
9
  OMC(oh-my-claudecode)가 새 버전으로 자동 업데이트되면 로컬 훅 패치가 사라진다. 그러면 도구 호출마다 훅 프로세스가 많이 생기고, Windows 에서는 커널 풀(paged pool)이 계속 늘어 PC 가 느려진다.
10
- 이 스킬은 같은 폴더의 `scripts/omc-patch.mjs` 로 패치를 다시 적용하고 결과를 검증한다. 답변은 한글.
10
+ 이 스킬은 같은 폴더의 `scripts/omc-patch.mjs` 로 패치를 다시 적용하고, `scripts/probe-pm.mjs` 로 훅이 실제로 일하는지 확인한다. 답변은 한글.
11
11
 
12
12
  패치 내용은 두 가지다.
13
13
  - 훅 스크립트 최상위의 `await import(...)` 에 10초 시간 제한을 건다. 로드가 멈춰도 훅이 끝까지 가고 프로세스가 남지 않는다.
@@ -15,6 +15,14 @@ OMC(oh-my-claudecode)가 새 버전으로 자동 업데이트되면 로컬 훅
15
15
 
16
16
  추가로 OMC 마켓플레이스 저장소의 `origin` 을 `omc-pinned://<원래 URL>` 로 바꿔 자동 업데이트를 막는다.
17
17
 
18
+ ## 헬퍼 자리 결함
19
+
20
+ - 패치는 시간 제한 헬퍼(`__omcRaceImport`)를 훅 파일에 넣는다. 이 도구는 헬퍼를 모듈 맨 앞에 넣는다.
21
+ - 이 스킬 밖에서 쓰던 이전 판의 패치 도구(아래 옛 킷)는 헬퍼를 첫 사용 지점 앞에 넣었다. 그 자리가 최상위 `try { }` 안이면 헬퍼도 그 블록에 갇힌다.
22
+ - 그러면 다른 블록의 import 가 ReferenceError 로 실패하고, 훅의 `catch` 가 그 오류를 삼킨다. 예: `project-memory-session` 은 종료 코드 0 으로 끝나지만 project-memory 를 만들지 않는다.
23
+ - banker 0.16.x 의 도구는 새로 패치할 때 맨 앞에 넣었지만, 옛 킷이 남긴 이 결함은 진단하지 못하고 `결과: 모두 적용된 상태입니다.` 를 냈다.
24
+ - 지금 도구의 `--check` 는 이 결함을 `헬퍼 자리 결함` 으로 알린다. 이미 감싼 파일이라 다시 적용만으로는 고쳐지지 않는다. 활성 폴더를 되돌린 뒤 다시 적용한다.
25
+
18
26
  ## 인자
19
27
 
20
28
  | 인자 | 동작 |
@@ -26,8 +34,9 @@ OMC(oh-my-claudecode)가 새 버전으로 자동 업데이트되면 로컬 훅
26
34
  ## 규칙
27
35
 
28
36
  - 이 작업은 사용자 설정 폴더(`~/.claude`)의 플러그인 캐시를 고치는 설정 변경이다. 서브에이전트에 맡기지 않고 메인 세션에서 직접 실행하고 직접 검증한다.
29
- - 적용은 `--no-update` 로만 실행한다. 인자 없는 apply 는 쓰지 않는다. 활성 OMC 를 도구의 `TARGET_VERSION`(5.3.0)으로 내리고, `revert` 로도 버전이 돌아오지 않는다.
30
- - 도구가 출력하는 `인자 없이 다시 실행하십시오.` 문구는 따르지 않는다. 이 스킬에서는 항상 `--no-update` 를 붙인다.
37
+ - 적용은 `--no-update` 로만 실행한다. 인자 없는 apply 는 쓰지 않는다. 활성 OMC 를 도구의 `TARGET_VERSION`(5.7.0)으로 바꾸고, `revert` 로도 버전이 돌아오지 않는다.
38
+ - 도구가 출력하는 `인자 없이 다시 실행하십시오.` 문구는 따르지 않는다. 이 스킬에서는 적용과 진단에 항상 `--no-update` 를 붙인다.
39
+ - 헬퍼 자리 결함과 함께 나오는 `--revert 로 되돌린 뒤 다시 적용하십시오.` 도 그대로 따르지 않는다. 인자 없는 `--revert` 는 설치된 모든 버전을 되돌리고 고정까지 푼다. 이 스킬은 활성 폴더만 `--revert --root` 로 되돌린다(3단계 1).
31
40
  - 패치 규칙은 OMC 훅 코드의 모양에 맞춰져 있다. 처음 보는 OMC 버전이면 반드시 `check` 를 먼저 실행한다.
32
41
  - `실패 N건` 이 1건 이상이면 멈추고 출력을 그대로 보고한다. 문법 오류가 난 파일은 도구가 원본으로 자동 롤백한다.
33
42
  - 판정은 출력 문구로 한다. 종료 코드 0 만으로 완료라고 보고하지 않는다.
@@ -37,7 +46,18 @@ OMC(oh-my-claudecode)가 새 버전으로 자동 업데이트되면 로컬 훅
37
46
 
38
47
  ## 절차
39
48
 
40
- 스크립트 경로는 `<이 스킬 디렉터리 절대경로>/scripts/omc-patch.mjs` 이다. Node 18 이상과 `git` 이 필요하다. 모든 명령은 OS 구분 없이 같다.
49
+ 스크립트는 `<이 스킬 디렉터리 절대경로>/scripts/` 의 `omc-patch.mjs` 와 `probe-pm.mjs` 다. Node 18 이상과 `git` 이 필요하다.
50
+ 활성 플러그인 폴더(아래 `R`, `$r`)는 명령마다 다시 구한다. 명령마다 새 셸일 수 있다.
51
+
52
+ bash:
53
+ ```bash
54
+ R=$(node -e 'const j=require(require("path").join(require("os").homedir(),".claude/plugins/installed_plugins.json"));console.log(j.plugins["oh-my-claudecode@omc"][0].installPath)')
55
+ ```
56
+
57
+ PowerShell:
58
+ ```powershell
59
+ $r = (Get-Content "$env:USERPROFILE\.claude\plugins\installed_plugins.json" -Raw | ConvertFrom-Json).plugins.'oh-my-claudecode@omc'[0].installPath
60
+ ```
41
61
 
42
62
  ### 1. 진단
43
63
 
@@ -48,7 +68,13 @@ node "<이 스킬 디렉터리 절대경로>/scripts/omc-patch.mjs" --check --no
48
68
  - `활성 버전: X (installed_plugins.json)` 줄로 실제 활성 버전을 확인한다.
49
69
  - 그 줄이 `(추정: ...)` 로 끝나면 도구가 `installed_plugins.json` 에서 활성 버전을 찾지 못한 것이다. 적용하지 않고 출력을 그대로 보고하고 끝낸다. 추정한 폴더를 패치하면 실제 활성 버전은 그대로 남는데 `check` 는 적용된 것으로 보일 수 있다.
50
70
  - `패치 필요 N건` 은 설치된 모든 버전 폴더를 합친 수다. 판정은 맨 아래 `결과:` 줄로 한다.
51
- - `결과: 모두 적용된 상태입니다.` 이면 4단계 검증으로 건너뛴다.
71
+
72
+ | `결과:` 줄 | 다음 |
73
+ |---|---|
74
+ | `모두 적용된 상태입니다.` | 4단계 검증으로 건너뜀 |
75
+ | `헬퍼 자리 결함 N건이 있습니다.` | 2단계 확인 뒤 3단계에서 활성 폴더부터 되돌림 |
76
+ | `적용되지 않은 항목이 있습니다.` | 2단계 확인 뒤 3단계 적용 |
77
+
52
78
  - `참고: 비활성 버전 ...` 은 지금 쓰지 않는 버전이라 판정에 들어가지 않는다.
53
79
  - 인자가 `check` 이면 여기서 결과만 보고하고 끝낸다.
54
80
 
@@ -56,41 +82,64 @@ node "<이 스킬 디렉터리 절대경로>/scripts/omc-patch.mjs" --check --no
56
82
 
57
83
  `--check` 결과를 보여 주고 사용자에게 적용해도 되는지 묻는다. 묻기 전에 아래를 알린다.
58
84
  - 활성 OMC 버전의 `scripts/*.mjs` 와 `hooks/hooks.json` 을 고친다. 원본은 `.omcbak` 로 남는다.
85
+ - 헬퍼 자리 결함이 있으면 활성 폴더를 `.omcbak` 원본으로 먼저 되돌린 뒤 다시 패치한다.
59
86
  - OMC 마켓플레이스 `origin` 을 `omc-pinned://` 로 바꾼다. OMC 가 더 이상 자동 업데이트되지 않는다. 되돌리기는 `revert`.
60
87
  - 승인이 없으면 적용하지 않는다.
61
88
 
62
89
  ### 3. 적용
63
90
 
64
- ```bash
65
- node "<이 스킬 디렉터리 절대경로>/scripts/omc-patch.mjs" --no-update
66
- ```
91
+ 1. 1단계가 헬퍼 자리 결함을 냈으면 활성 폴더만 먼저 되돌린다. `--root` 는 그 폴더의 훅과 `hooks.json` 만 다룬다. 마켓플레이스 고정과 다른 버전 폴더는 건드리지 않는다.
67
92
 
68
- - `패치 N건 / 실패 0건`, `[3/4]` 의 PostToolUse 제거, `[4/4]` 의 고정 결과를 확인한다.
69
- - 실패가 1건 이상이면 멈추고 출력 그대로 보고한다.
93
+ ```bash
94
+ R=$(node -e 'const j=require(require("path").join(require("os").homedir(),".claude/plugins/installed_plugins.json"));console.log(j.plugins["oh-my-claudecode@omc"][0].installPath)')
95
+ node "<이 스킬 디렉터리 절대경로>/scripts/omc-patch.mjs" --revert --root "$R"
96
+ ```
70
97
 
71
- ### 4. 검증 (세 가지 모두)
98
+ PowerShell:
99
+ ```powershell
100
+ $r = (Get-Content "$env:USERPROFILE\.claude\plugins\installed_plugins.json" -Raw | ConvertFrom-Json).plugins.'oh-my-claudecode@omc'[0].installPath
101
+ node "<이 스킬 디렉터리 절대경로>\scripts\omc-patch.mjs" --revert --root $r
102
+ ```
72
103
 
73
- 1. 다시 진단한다. `결과: 모두 적용된 상태입니다.` 가 나와야 한다.
104
+ - `되돌림 <훅 이름>` 줄과 `되돌리기 완료.` 를 확인한다. 끝의 `재시작해야 반영됩니다` 는 이 중간 단계에서는 무시한다. 2 에서 바로 다시 패치한다.
105
+ - 결함이 있던 훅 이름이 `되돌림` 줄에 없으면 그 파일에 `.omcbak` 백업이 없는 것이다. 이 도구로는 되돌릴 원본이 없으니 멈추고 보고한다. 원본은 같은 버전의 OMC 저장소에서 다시 받아야 한다. 파이프 대신 파일을 거친다(Windows PowerShell 5.1 의 파이프는 바이너리를 텍스트로 바꿔 파일을 깨뜨린다): `git -C "<마켓플레이스 폴더>" archive -o "<임시 tar>" v<버전> scripts/<이름>.mjs` 뒤 `tar -xf "<임시 tar>" -C "<활성 폴더>"`. 이 명령은 사용자 승인 뒤에만 실행한다.
106
+
107
+ 2. 적용한다.
108
+
109
+ ```bash
110
+ node "<이 스킬 디렉터리 절대경로>/scripts/omc-patch.mjs" --no-update
111
+ ```
112
+
113
+ - `패치 N건 / 실패 0건`, `[3/4]` 의 PostToolUse 제거, `[4/4]` 의 고정 결과를 확인한다.
114
+ - 실패가 1건 이상이면 멈추고 출력 그대로 보고한다.
115
+ - `[!] 헬퍼 자리 결함 N건은 고치지 않았습니다.` 가 나오면 1 을 건너뛰었거나 1 이 그 파일을 되돌리지 못한 것이다. 1 과 2 를 한 번만 다시 한다. 다시 해도 같으면 멈추고 보고한다.
116
+
117
+ ### 4. 검증 (다섯 가지 모두)
118
+
119
+ 1. 다시 진단한다. `결과: 모두 적용된 상태입니다.` 가 나와야 하고, 활성 버전에 `헬퍼 자리 결함` 줄이 없어야 한다.
74
120
 
75
121
  ```bash
76
122
  node "<이 스킬 디렉터리 절대경로>/scripts/omc-patch.mjs" --check --no-update
77
123
  ```
78
124
 
79
- 2. 패치된 훅의 문법을 확인한다. `*.omcbak` 와 짝인 파일마다 `node --check` 를 실행하고 종료 코드 0 을 확인한다.
125
+ 2. 패치된 훅의 문법과 헬퍼 자리를 확인한다. `*.omcbak` 와 짝인 파일마다 `node --check` 의 종료 코드가 0 이고, `--scan` 이 낸 헬퍼 깊이가 모두 0 이면 정상이다.
80
126
 
81
127
  bash:
82
128
  ```bash
83
129
  R=$(node -e 'const j=require(require("path").join(require("os").homedir(),".claude/plugins/installed_plugins.json"));console.log(j.plugins["oh-my-claudecode@omc"][0].installPath)')
84
- for b in "$R"/scripts/*.omcbak; do [ -e "$b" ] || continue; f="${b%.omcbak}"; node --check "$f"; rc=$?; echo "$(basename "$f") syntax=$rc"; done
130
+ set --; for b in "$R"/scripts/*.omcbak; do [ -e "$b" ] || continue; f="${b%.omcbak}"; node --check "$f"; rc=$?; echo "$(basename "$f") syntax=$rc"; set -- "$@" "$f"; done
131
+ node "<이 스킬 디렉터리 절대경로>/scripts/omc-patch.mjs" --scan "$@" | node -e 'let s="";process.stdin.on("data",(d)=>{s+=d}).on("end",()=>{for(const x of JSON.parse(s))console.log(require("path").basename(x.file),"depth="+x.helperDecls.map((d)=>d.depth).join(","))})'
85
132
  ```
86
133
 
87
134
  PowerShell:
88
135
  ```powershell
89
136
  $r = (Get-Content "$env:USERPROFILE\.claude\plugins\installed_plugins.json" -Raw | ConvertFrom-Json).plugins.'oh-my-claudecode@omc'[0].installPath
90
- Get-ChildItem "$r\scripts\*.omcbak" | % { $f = $_.FullName -replace '\.omcbak$',''; node --check $f; "{0} syntax={1}" -f (Split-Path $f -Leaf), $LASTEXITCODE }
137
+ $f = @(Get-ChildItem "$r\scripts\*.omcbak" | % { $_.FullName -replace '\.omcbak$','' })
138
+ $f | % { node --check $_; "{0} syntax={1}" -f (Split-Path $_ -Leaf), $LASTEXITCODE }
139
+ (node "<이 스킬 디렉터리 절대경로>\scripts\omc-patch.mjs" --scan @f | ConvertFrom-Json) | % { "{0} depth={1}" -f (Split-Path $_.file -Leaf), (@($_.helperDecls | % depth) -join ',') }
91
140
  ```
92
141
 
93
- 3. 패치된 훅 하나를 플러그인의 `scripts/run.cjs` 로 실제 실행한다. 명령마다 새 셸일 수 있으니 경로를 다시 구한다.
142
+ 3. 패치된 훅 하나를 플러그인의 `scripts/run.cjs` 로 실제 실행한다.
94
143
  `OMC_NOTIFY=0` 은 이 시험 실행이 사용자의 알림 채널로 `session-idle` 알림을 보내지 않게 한다.
95
144
 
96
145
  bash:
@@ -109,7 +158,29 @@ node "<이 스킬 디렉터리 절대경로>/scripts/omc-patch.mjs" --no-update
109
158
 
110
159
  `{"continue":true,...}` 형태의 출력과 종료 코드 0 이면 정상. 훅 이름이 없으면 `*.omcbak` 와 짝인 다른 훅으로 바꾼다.
111
160
 
112
- 4. 마켓플레이스 고정을 확인한다. `omc-pinned://` 로 시작하면 고정된 것이다.
161
+ 4. project-memory 훅이 실제로 일하는지 확인한다. 이 확인은 임시 폴더의 스크래치 프로젝트에서 훅을 돌린다. 홈, 설정 폴더, 상태 폴더도 그 안을 가리키게 하고 끝나면 지운다. 사용자의 `~/.claude` 와 작업 폴더의 `.omc` 는 건드리지 않는다.
162
+
163
+ ```bash
164
+ R=$(node -e 'const j=require(require("path").join(require("os").homedir(),".claude/plugins/installed_plugins.json"));console.log(j.plugins["oh-my-claudecode@omc"][0].installPath)')
165
+ node "<이 스킬 디렉터리 절대경로>/scripts/probe-pm.mjs" --plugin "$R"
166
+ ```
167
+
168
+ PowerShell:
169
+ ```powershell
170
+ $r = (Get-Content "$env:USERPROFILE\.claude\plugins\installed_plugins.json" -Raw | ConvertFrom-Json).plugins.'oh-my-claudecode@omc'[0].installPath
171
+ node "<이 스킬 디렉터리 절대경로>\scripts\probe-pm.mjs" --plugin $r
172
+ ```
173
+
174
+ | 결과 | 뜻과 다음 |
175
+ |---|---|
176
+ | `PROBE GREEN` | 정상 |
177
+ | `PROBE RED: no project-memory.json` | 훅이 돌았지만 아무 일도 안 했다. 1단계나 4.1, 4.2 가 헬퍼 자리 결함을 보였고 2단계 승인이 있으면 3단계를 한 번만 다시 한다. 결함이 보이지 않거나 다시 해도 RED 면 출력을 그대로 보고하고 멈춘다. 느린 PC 에서 import 가 10초 제한에 걸린 경우처럼 원인이 다를 수 있다 |
178
+ | 그 밖의 `PROBE RED` | 출력을 그대로 보고하고 멈춘다 |
179
+ | 종료 코드 2 와 `훅 스크립트가 없습니다` 줄 | 그 버전에 project-memory 훅이 없다. 해당 없음으로 적는다 |
180
+ | 그 밖의 `probe-pm:` 줄(종료 코드 2) | 명령 오류다. 경로를 다시 구해 한 번만 다시 실행한다. 같으면 출력을 보고하고 멈춘다 |
181
+ | 판정 줄 뒤의 `probe-pm: 임시 폴더를 지우지 못함` | 판정은 그대로 쓴다. 남은 임시 폴더 경로를 보고에 적어 사용자가 지우게 한다 |
182
+
183
+ 5. 마켓플레이스 고정을 확인한다. `omc-pinned://` 로 시작하면 고정된 것이다.
113
184
 
114
185
  ```bash
115
186
  git -C "$HOME/.claude/plugins/marketplaces/omc" remote get-url origin
@@ -119,10 +190,11 @@ node "<이 스킬 디렉터리 절대경로>/scripts/omc-patch.mjs" --no-update
119
190
 
120
191
  ### 5. 보고
121
192
 
122
- - 활성 버전, 패치 건수, PostToolUse 제거 목록, 고정 상태
193
+ - 활성 버전, 패치 건수, 헬퍼 자리 결함 처리 여부, PostToolUse 제거 목록, 고정 상태, project-memory 확인 결과
123
194
  - 훅 스크립트 패치는 바로 적용된다. 훅이 호출될 때마다 파일을 새로 읽기 때문이다.
124
195
  - PostToolUse 제거는 `hooks.json` 변경이라 각 세션을 다시 시작해야 적용된다.
125
196
  - Windows 에서 이미 쌓인 커널 풀은 재부팅으로만 회수된다. 재부팅은 사용자가 직접 한다.
197
+ - Windows 에서 효과를 재려면 부하가 비슷한 24시간끼리 커널 풀 증가량을 비교한다. 유휴 시간에 늘지 않은 것은 증거가 아니다.
126
198
 
127
199
  ## 되돌리기 (인자 `revert`)
128
200
 
@@ -130,14 +202,17 @@ node "<이 스킬 디렉터리 절대경로>/scripts/omc-patch.mjs" --no-update
130
202
  node "<이 스킬 디렉터리 절대경로>/scripts/omc-patch.mjs" --revert
131
203
  ```
132
204
 
133
- - `.omcbak` 백업으로 훅 스크립트와 `hooks.json` 을 되돌리고 마켓플레이스 `origin` 을 원래 URL 로 복구한다.
205
+ - `.omcbak` 백업으로 설치된 모든 버전의 훅 스크립트와 `hooks.json` 을 되돌리고 마켓플레이스 `origin` 을 원래 URL 로 복구한다.
134
206
  - 되돌린 뒤 Claude Code 를 다시 시작해야 반영된다.
135
207
  - `이 도구가 만들지 않은 수정이 남아 있습니다` 가 나오면 백업이 없는 손 수정이다. 출력에 적힌 복구 명령을 그대로 보고하고, 사용자 승인 없이 실행하지 않는다.
136
208
 
137
209
  ## 함정
138
210
 
139
- - 도구의 `TARGET_VERSION`(5.3.0)은 소스에 고정돼 있다. 인자 없는 apply 만 이 값을 쓰고, 이 스킬은 그 apply 를 쓰지 않는다.
211
+ - 도구의 `TARGET_VERSION`(5.7.0)은 소스에 고정돼 있다. 인자 없는 apply 만 이 값을 쓰고, 이 스킬은 그 apply 를 쓰지 않는다.
140
212
  - OMC 가 훅 코드 구조를 바꾸면 패치 대상이 0건으로 나오거나 변환이 실패할 수 있다. 이때 `실패 N건` 과 `변환 실패` 를 그대로 보고한다.
213
+ - 헬퍼 자리 결함이 남은 채 `revert` 없이 적용을 다시 돌려도 바뀌는 것이 없다. 이미 감싼 파일은 건너뛰기 때문이다.
214
+ - `--root` 와 `--revert --root` 는 그 폴더만 다룬다. 고정 해제까지 하려면 인자 없는 `--revert` 를 쓴다.
141
215
  - 고정을 풀지 않고 OMC 를 올리려면 먼저 `revert` 한다. 그렇지 않으면 마켓플레이스 fetch 가 계속 실패한다.
142
216
  - 고정 기록(`PINNED`)은 `~/.claude/omc-local-patches/PINNED` 에 생긴다. 기록일 뿐이라 지워도 고정은 풀리지 않고, 이미 고정된 상태에서는 다시 만들어지지 않는다. `revert` 는 origin 값에서 원래 URL 을 되찾는다.
217
+ - `--scan` 은 읽기만 한다. 플러그인 캐시도 `installed_plugins.json` 도 읽지 않고 준 파일만 본다.
143
218
  - 도구의 주석과 출력 문구는 한글이다.
@@ -2,27 +2,33 @@
2
2
  /**
3
3
  * OMC 패치 — 버전 고정 + 훅 결함 수정 + 자동 업데이트 차단.
4
4
  *
5
- * node omc-patch.mjs 업데이트(5.3.0) + 패치 + 고정
5
+ * node omc-patch.mjs 업데이트(5.7.0) + 패치 + 고정
6
6
  * node omc-patch.mjs --check 진단만 (아무것도 바꾸지 않음)
7
7
  * node omc-patch.mjs --revert 패치·고정 되돌리기
8
8
  * node omc-patch.mjs --no-update 버전은 그대로 두고 패치만
9
+ * node omc-patch.mjs --root <플러그인 폴더> [--check | --revert]
10
+ * 그 폴더의 hooks/hooks.json 과 scripts/ 만 다룬다(번들 사본용).
11
+ * installed_plugins.json, 마켓플레이스 origin, 판은 건드리지 않는다.
12
+ * node omc-patch.mjs --scan <파일>...
13
+ * 헬퍼 선언의 블록 깊이와 래핑 자리를 JSON 으로 낸다(읽기 전용).
9
14
  *
10
15
  * 활성 버전은 installed_plugins.json 의 oh-my-claudecode@omc 항목(version, installPath)에서
11
- * 읽는다. 캐시에 5.3.0 이 있다고 5.3.0 을 활성으로 보지 않는다(--no-update 는 활성 버전을 패치).
16
+ * 읽는다. 캐시에 TARGET_VERSION 이 있다고 그것을 활성으로 보지 않는다(--no-update 는 활성 버전을 패치).
12
17
  * OMC_PATCH_INSTALLED=<json 경로> 로 읽을 파일을 바꿀 수 있다(시험용, --check 와 함께).
13
18
  *
14
19
  * OS: Windows / macOS / Linux 공통. 런처는 apply.cmd(Windows), apply.sh(그 외).
15
20
  * 배경과 근거는 같은 폴더의 README.md.
16
21
  */
17
22
  import {
18
- readFileSync, writeFileSync, existsSync, copyFileSync, readdirSync,
23
+ readFileSync, writeFileSync, writeSync, existsSync, copyFileSync, readdirSync,
19
24
  unlinkSync, mkdirSync, rmSync, symlinkSync,
20
25
  } from "node:fs";
21
- import { join, dirname } from "node:path";
26
+ import { join, dirname, resolve } from "node:path";
22
27
  import { homedir, hostname } from "node:os";
23
28
  import { execFileSync } from "node:child_process";
24
29
 
25
- const TARGET_VERSION = "5.3.0";
30
+ // 인자 없는 apply 가 맞춰 놓는 판. banker 의 omc-patch 스킬은 늘 --no-update 로 돌려 이 값으로 판을 바꾸지 않는다.
31
+ const TARGET_VERSION = "5.7.0";
26
32
  // 3초는 좁았다. 이 예산은 «정상이지만 느린» 로드까지 잘라내는데, 파일 스캔이 8.2배 느려진
27
33
  // 이 PC 가 정확히 그 조건이다(실측: 4초 지연에서 session-end 가 조용히 건너뛰어졌다).
28
34
  // 대안이 «무한 정지» 인 자리에만 걸리므로 넉넉히 잡는다.
@@ -36,9 +42,16 @@ const MARKETPLACE = "omc";
36
42
  const PLUGIN_KEY = "oh-my-claudecode@omc";
37
43
  const REPO_URL = "https://github.com/Yeachan-Heo/oh-my-claudecode.git";
38
44
 
39
- const argv = new Set(process.argv.slice(2));
45
+ const args = process.argv.slice(2);
46
+ const argv = new Set(args);
40
47
  const MODE = argv.has("--revert") ? "revert" : argv.has("--check") ? "check" : "apply";
41
48
  const NO_UPDATE = argv.has("--no-update");
49
+ // --root <플러그인 폴더>: 이 PC 의 설치 상태와 상관없이 그 폴더 하나만 고친다. 번들 사본처럼
50
+ // 판 폴더도 installed_plugins.json 도 없는 트리에 쓴다. 값이 없거나 다음 플래그면 멈춘다.
51
+ const ROOT_AT = args.indexOf("--root");
52
+ const ROOT_ARG = ROOT_AT >= 0 ? args[ROOT_AT + 1] : null;
53
+ const ROOT = ROOT_ARG && !ROOT_ARG.startsWith("--") ? resolve(ROOT_ARG) : null;
54
+ const SCAN_AT = args.indexOf("--scan");
42
55
 
43
56
  const CFG = join(homedir(), ".claude");
44
57
  const PLUGINS = join(CFG, "plugins");
@@ -194,7 +207,9 @@ const REGEX_PREV_CH = new Set("(,=:[!&|?{};+-*%~^".split(""));
194
207
  const REGEX_PREV_KW = new Set(["return", "typeof", "instanceof", "in", "of", "new", "delete",
195
208
  "void", "case", "do", "else", "yield", "await"]);
196
209
 
197
- function scanTopLevelAwaitImports(src) {
210
+ // probe(i, fnStack) 를 주면 코드 글자(문자열·주석·정규식·중괄호가 아닌 자리)마다 부른다.
211
+ // --scan 이 같은 토크나이저로 헬퍼 선언의 깊이를 재는 데 쓴다. 패치 경로는 probe 없이 부른다.
212
+ function scanTopLevelAwaitImports(src, probe) {
198
213
  const n = src.length;
199
214
  const fnStack = [];
200
215
  const hits = [];
@@ -285,14 +300,20 @@ function scanTopLevelAwaitImports(src) {
285
300
  if (c === "a" && !(i > 0 && ID_CH.test(src[i - 1])) && /^await\s+import\s*\(/.test(src.slice(i, i + 40))) {
286
301
  if (!fnStack.some(Boolean)) hits.push(i);
287
302
  }
303
+ if (probe) probe(i, fnStack);
288
304
  prev = c; prevIdx = i; i++;
289
305
  }
290
306
  return hits;
291
307
  }
292
308
 
293
309
  // 자리별 상태. 이미 손으로 Promise.race 를 두른 자리는 건드리지 않는다(4.15.7 에 그런 것이 둘 있다).
310
+ // 이미 감싼 파일이라도 헬퍼 선언이 블록 안이면(옛 판의 자리) «패치됨» 이 아니라 «misplaced» 다.
311
+ // 그 상태를 «패치됨» 으로 넘기면 --check 가 결함 트리에 «모두 적용» 을 낸다(2026-10-12 실측).
294
312
  function siteInfo(src) {
295
- if (src.includes("__omcRaceImport")) return { state: "patched", sites: [] };
313
+ if (src.includes("__omcRaceImport")) {
314
+ const decl = scanFacts(src).helperDecls.find((d) => d.depth !== 0);
315
+ return decl ? { state: "misplaced", sites: [], decl } : { state: "patched", sites: [] };
316
+ }
296
317
  const all = scanTopLevelAwaitImports(src);
297
318
  if (!all.length) return { state: "n/a", sites: [] };
298
319
  const open = all.filter((p) => !/Promise\.race/.test(src.slice(Math.max(0, p - 300), p)));
@@ -300,8 +321,6 @@ function siteInfo(src) {
300
321
  return { state: "needs", sites: open };
301
322
  }
302
323
 
303
- const needsPatch = (src) => siteInfo(src).state;
304
-
305
324
  function transform(src) {
306
325
  const info = siteInfo(src);
307
326
  if (info.state !== "needs") return null;
@@ -332,6 +351,28 @@ function transform(src) {
332
351
  return out.slice(0, ls) + HELPER + out.slice(ls);
333
352
  }
334
353
 
354
+ // ── 읽기 전용 진단 (--scan) ──────────────────────────────────────────────────
355
+ // 패치가 «제자리» 에 앉았는지를 위 토크나이저 하나로 잰다. 시험이 토크나이저를 따로 가지면
356
+ // 패치가 고르는 자리와 시험이 재는 자리가 서로 다른 규칙을 따르게 된다.
357
+ // helperDecls 헬퍼 선언마다 { line, depth }. depth 는 둘러싼 중괄호 수(함수 몸통과 블록).
358
+ // 0 이어야 최상위 문장이다. 블록 안 const 는 블록 범위라 다른 블록의 자리에서
359
+ // ReferenceError 가 나고, 훅의 catch 가 그것을 삼킨다(옛 자리 결함).
360
+ // wrapped `await __omcRaceImport(` 자리마다 { line, inFunction }. 패치는 최상위 자리만 감싼다.
361
+ // unwrapped 아직 감싸지지 않은 최상위 `await import(` 의 줄.
362
+ function scanFacts(src) {
363
+ const lineOf = (at) => src.slice(0, at).split("\n").length;
364
+ const helperDecls = [], wrapped = [];
365
+ const unwrapped = scanTopLevelAwaitImports(src, (i, fnStack) => {
366
+ const ch = src[i];
367
+ if (ch !== "c" && ch !== "l" && ch !== "v" && ch !== "f" && ch !== "a") return;
368
+ if (i > 0 && ID_CH.test(src[i - 1])) return;
369
+ const head = src.slice(i, i + 48);
370
+ if (/^(?:const|let|var|function)\s+__omcRaceImport\b/.test(head)) helperDecls.push({ line: lineOf(i), depth: fnStack.length });
371
+ else if (/^await\s+__omcRaceImport\s*\(/.test(head)) wrapped.push({ line: lineOf(i), inFunction: fnStack.some(Boolean) });
372
+ }).map(lineOf);
373
+ return { helperDecls, wrapped, unwrapped };
374
+ }
375
+
335
376
  function syntaxError(file) {
336
377
  try { execFileSync(process.execPath, ["--check", file], { stdio: ["ignore", "ignore", "pipe"] }); return null; }
337
378
  catch (e) {
@@ -346,7 +387,7 @@ const looksHandPatched = (src) =>
346
387
  !src.includes("__omcRaceImport") && /Promise\.race\s*\(\s*\[?[\s\S]{0,120}?\bimport\s*\(/.test(src);
347
388
 
348
389
  function patchHooks(root) {
349
- let done = 0, skip = 0, fail = 0, need = 0;
390
+ let done = 0, skip = 0, fail = 0, need = 0, misplaced = 0;
350
391
  const foreign = [];
351
392
  // revert 는 scripts/ 전체를 훑는다. hooks.json 기반 목록은 두 번 새는데, 실측으로
352
393
  // 4.15.7 이 둘 다 겪었다 — PostToolUse 를 지우면 그 스크립트가 목록에서 빠지고(그래서
@@ -368,8 +409,14 @@ function patchHooks(root) {
368
409
  if (looksHandPatched(readFileSync(f, "utf8"))) foreign.push(n);
369
410
  continue;
370
411
  }
371
- const st = needsPatch(readFileSync(f, "utf8"));
372
- if (st !== "needs") { skip++; continue; }
412
+ const info = siteInfo(readFileSync(f, "utf8"));
413
+ // 헬퍼 자리 결함은 check 와 apply 모두 이름을 댄다. 이미 감싼 파일이라 transform 으로는
414
+ // 고칠 수 없으므로 apply 도 건드리지 않고, 되돌린 뒤 다시 적용하라고 안내한다(MISPLACED_HINT).
415
+ if (info.state === "misplaced") {
416
+ bad(`헬퍼 자리 결함: ${n} (헬퍼 선언 :${info.decl.line} 이 블록 안, 깊이 ${info.decl.depth})`);
417
+ misplaced++; continue;
418
+ }
419
+ if (info.state !== "needs") { skip++; continue; }
373
420
  if (MODE === "check") { bad(`패치 필요: ${n}`); need++; continue; }
374
421
 
375
422
  const out = transform(readFileSync(f, "utf8"));
@@ -380,7 +427,7 @@ function patchHooks(root) {
380
427
  if (err) { copyFileSync(bak, f); bad(`문법오류로 롤백: ${n}\n ${err}`); fail++; continue; }
381
428
  done++;
382
429
  }
383
- return { done, skip, fail, need, foreign };
430
+ return { done, skip, fail, need, misplaced, foreign };
384
431
  }
385
432
 
386
433
  // ── 3. PostToolUse 훅 제거 ───────────────────────────────────────────────────
@@ -466,61 +513,104 @@ function pinMarketplace() {
466
513
  return "pinned";
467
514
  }
468
515
 
516
+ // ── --scan: 사실만 내고 끝낸다 ───────────────────────────────────────────────
517
+ // 아래 main 보다 먼저 끝나야 한다. 이 모드는 installed_plugins.json 도 플러그인 캐시도 읽지 않는다.
518
+ // 결과는 입력 순서대로 JSON 배열 한 줄이다. 동기 쓰기로 내보내야 process.exit 가 잘라먹지 않는다.
519
+ if (SCAN_AT >= 0) {
520
+ const files = args.slice(SCAN_AT + 1);
521
+ if (!files.length) { console.error("--scan 뒤에 파일 경로가 필요합니다."); process.exit(1); }
522
+ const out = files.map((file) => {
523
+ try { return { file, ...scanFacts(readFileSync(file, "utf8")) }; }
524
+ catch (e) { return { file, error: String(e.code || e.message) }; }
525
+ });
526
+ writeSync(1, JSON.stringify(out) + "\n");
527
+ process.exit(out.some((x) => x.error) ? 1 : 0);
528
+ }
529
+
469
530
  // ── main ─────────────────────────────────────────────────────────────────────
470
- log(`OMC 패치 (대상 ${TARGET_VERSION}) 모드: ${MODE}${NO_UPDATE ? " --no-update" : ""}`);
531
+ if (ROOT_AT >= 0 && !ROOT) { bad("--root 뒤에 플러그인 폴더 경로가 필요합니다."); process.exit(1); }
532
+ log(ROOT
533
+ ? `OMC 패치 (대상 폴더 ${ROOT}) 모드: ${MODE}`
534
+ : `OMC 패치 (대상 ${TARGET_VERSION}) 모드: ${MODE}${NO_UPDATE ? " --no-update" : ""}`);
471
535
  log(`플랫폼: ${process.platform} node ${process.version}`);
472
536
  log();
473
537
 
474
- if (!existsSync(INSTALLED)) { bad(`Claude Code 플러그인 설정을 찾을 수 없음: ${INSTALLED}`); process.exit(1); }
538
+ // 헬퍼 자리 결함은 이 판의 apply 로 고칠 수 없다(이미 감싼 파일). 원본으로 되돌린 뒤 다시 적용한다.
539
+ const MISPLACED_HINT = ROOT
540
+ ? "--revert --root 로 되돌린 뒤 같은 --root 로 다시 적용하십시오."
541
+ : "--revert 로 되돌린 뒤 다시 적용하십시오.";
542
+
543
+ // targets 는 «고칠 플러그인 폴더» 의 이름표 목록이다. 기본은 캐시의 판 번호이고, --root 는
544
+ // 그 폴더 경로 하나다. rootOf 가 이름표를 폴더로 바꾼다.
545
+ let active, targets;
546
+ const rootOf = (v) => (ROOT ? v : join(CACHE, v));
547
+ if (ROOT) {
548
+ // 판 폴더도 installed_plugins.json 도 없는 트리(번들 사본)를 고친다. 판 바꾸기([1/4])와
549
+ // 마켓플레이스 고정([4/4])은 하지 않는다 — 그 둘은 이 PC 의 설정이고 이 폴더의 것이 아니다.
550
+ if (!existsSync(join(ROOT, "hooks", "hooks.json")) || !existsSync(join(ROOT, "scripts"))) {
551
+ bad(`플러그인 폴더가 아님 (hooks/hooks.json 과 scripts/ 가 있어야 함): ${ROOT}`);
552
+ process.exit(1);
553
+ }
554
+ log("[1/4] 버전");
555
+ log(" 건너뜀 (--root 는 판을 바꾸지 않음)");
556
+ active = ROOT;
557
+ targets = [ROOT];
558
+ } else {
559
+ if (!existsSync(INSTALLED)) { bad(`Claude Code 플러그인 설정을 찾을 수 없음: ${INSTALLED}`); process.exit(1); }
475
560
 
476
- log("[1/4] 버전");
477
- if (NO_UPDATE) log(" 건너뜀 (--no-update)");
478
- else if (MODE === "revert") log(" 건너뜀 (revert 는 버전을 되돌리지 않음 — README 의 롤백 절차 참고)");
479
- else if (!ensureVersion() && MODE !== "check") { bad("버전 확보 실패 — 중단"); process.exit(1); }
561
+ log("[1/4] 버전");
562
+ if (NO_UPDATE) log(" 건너뜀 (--no-update)");
563
+ else if (MODE === "revert") log(" 건너뜀 (revert 는 버전을 되돌리지 않음 — README 의 롤백 절차 참고)");
564
+ else if (!ensureVersion() && MODE !== "check") { bad("버전 확보 실패 — 중단"); process.exit(1); }
480
565
 
481
- const versions = installedVersions();
482
- // [1/4] 뒤에 계산해야 일반 apply 가 TARGET_VERSION 으로 전환한 결과가 반영된다.
483
- const { v: active, src: activeSrc } = activeVersion();
484
- if (MODE !== "revert" && (MODE === "check" || NO_UPDATE)) {
485
- log(` 활성 버전: ${active} (${activeSrc})`);
486
- if (active !== TARGET_VERSION) {
487
- log(` 활성 버전이 대상(${TARGET_VERSION})과 다름 — 기본 apply 는 ${TARGET_VERSION} 로 전환하고, --no-update 는 활성 버전(${active})을 그 자리에서 패치함`);
566
+ const versions = installedVersions();
567
+ // [1/4] 뒤에 계산해야 일반 apply 가 TARGET_VERSION 으로 전환한 결과가 반영된다.
568
+ const { v, src: activeSrc } = activeVersion();
569
+ active = v;
570
+ if (MODE !== "revert" && (MODE === "check" || NO_UPDATE)) {
571
+ log(` 활성 버전: ${active} (${activeSrc})`);
572
+ if (active !== TARGET_VERSION) {
573
+ log(` 활성 버전이 대상(${TARGET_VERSION})과 다름 — 기본 apply 는 ${TARGET_VERSION} 로 전환하고, --no-update 는 활성 버전(${active})을 그 자리에서 패치함`);
574
+ }
488
575
  }
576
+ // apply 는 활성 버전만 손댄다. revert 와 check 는 설치된 모든 버전을 훑는다 —
577
+ // 여러 버전이 나란히 있을 때 이전 버전에 수정이 남으면, 롤백했을 때 예상과 다른
578
+ // 상태가 된다(실제로 4.15.7 의 hooks.json 수정이 그렇게 남았다).
579
+ targets = MODE === "apply" ? [active] : (versions.length ? versions : [active]);
580
+ if (!existsSync(join(CACHE, active))) { bad(`설치 경로 없음: ${join(CACHE, active)}`); process.exit(1); }
489
581
  }
490
- // apply 는 활성 버전만 손댄다. revert 와 check 는 설치된 모든 버전을 훑는다 —
491
- // 여러 버전이 나란히 있을 때 이전 버전에 수정이 남으면, 롤백했을 때 예상과 다른
492
- // 상태가 된다(실제로 4.15.7 의 hooks.json 수정이 그렇게 남았다).
493
- const targets = MODE === "apply" ? [active] : (versions.length ? versions : [active]);
494
- if (!existsSync(join(CACHE, active))) { bad(`설치 경로 없음: ${join(CACHE, active)}`); process.exit(1); }
495
582
 
496
583
  log();
497
- log(`[2/4] 훅 패치${MODE === "apply" ? ` (${active})` : ` (대상 ${targets.length}개 버전)`}`);
498
- const r = { done: 0, skip: 0, fail: 0, need: 0 };
584
+ log(`[2/4] 훅 패치${MODE === "apply" || ROOT ? ` (${active})` : ` (대상 ${targets.length}개 버전)`}`);
585
+ const r = { done: 0, skip: 0, fail: 0, need: 0, misplaced: 0 };
499
586
  const needBy = {};
587
+ const misplacedBy = {};
500
588
  const foreignBy = {};
501
589
  for (const v of targets) {
502
- const root = join(CACHE, v);
590
+ const root = rootOf(v);
503
591
  if (!existsSync(root)) continue;
504
592
  if (targets.length > 1) log(` — ${v}`);
505
593
  const x = patchHooks(root);
506
594
  needBy[v] = x.need;
507
- for (const k of ["done", "skip", "fail", "need"]) r[k] += x[k];
595
+ misplacedBy[v] = x.misplaced;
596
+ for (const k of ["done", "skip", "fail", "need", "misplaced"]) r[k] += x[k];
508
597
  if (x.foreign?.length) foreignBy[v] = x.foreign;
509
598
  }
510
- log(` ${MODE === "check" ? `패치 필요 ${r.need}건` : MODE === "revert" ? `되돌림 ${r.done}건` : `패치 ${r.done}건 / 실패 ${r.fail}건`} / 대상 아님 ${r.skip}건`);
599
+ log(` ${MODE === "check" ? `패치 필요 ${r.need}건` : MODE === "revert" ? `되돌림 ${r.done}건` : `패치 ${r.done}건 / 실패 ${r.fail}건`}${r.misplaced ? ` / 헬퍼 자리 결함 ${r.misplaced}건` : ""} / 대상 아님 ${r.skip}건`);
511
600
  // 이 도구가 만들지 않은 수정은 되돌릴 원본이 디스크에 없다. 조용히 넘기면 «완전히
512
601
  // 되돌렸다» 는 잘못된 인상을 준다 — 무엇이 남았고 어떻게 되찾는지 정확히 말한다.
513
602
  for (const [v, list] of Object.entries(foreignBy)) {
514
603
  bad(`${v}: 이 도구가 만들지 않은 수정이 남아 있습니다 — ${list.join(", ")}`);
515
604
  log(` 백업(.omcbak)이 없어 되돌릴 원본이 디스크에 없습니다. 원본이 필요하면:`);
516
- log(` git -C "${MARKET}" archive v${v} scripts/<이름>.mjs | tar -x -C "${join(CACHE, v)}"`);
605
+ if (ROOT) log(` 같은 판의 상류 원본에서 scripts/<이름>.mjs 를 다시 가져오십시오.`);
606
+ else log(` git -C "${MARKET}" archive v${v} scripts/<이름>.mjs | tar -x -C "${join(CACHE, v)}"`);
517
607
  }
518
608
 
519
609
  log();
520
610
  log("[3/4] PostToolUse 훅 제거");
521
611
  const hjNeedBy = {};
522
612
  for (const v of targets) {
523
- const root = join(CACHE, v);
613
+ const root = rootOf(v);
524
614
  if (!existsSync(root)) continue;
525
615
  const hj = patchHooksJson(root);
526
616
  const tag = targets.length > 1 ? `${v}: ` : "";
@@ -534,35 +624,46 @@ for (const v of targets) {
534
624
 
535
625
  log();
536
626
  log("[4/4] 자동 업데이트 차단");
537
- // Claude Code 는 세션 시작 후 최대 10분 지연으로 마켓플레이스를 자동 fetch 한다.
538
- // 공식 차단 수단은 두 가지뿐이고 각각 한계가 있어, 현재 상태를 같이 보여준다.
539
- // - DISABLE_AUTOUPDATER=1 : 확실하지만 전역(다른 마켓플레이스도 멈춤)
540
- // - /plugin UI 의 마켓플레이스별 토글 : omc 만 끄지만 UI 상태라 파일로 확인 불가
541
- try {
542
- const st = JSON.parse(readFileSync(join(CFG, "settings.json"), "utf8"));
543
- const off = st?.env?.DISABLE_AUTOUPDATER;
544
- log(` DISABLE_AUTOUPDATER: ${off ? `${off} (전역 차단 중)` : "미설정 (다른 마켓플레이스는 계속 자동 업데이트)"}`);
545
- } catch { /* settings.json 이 없거나 읽을 수 없으면 표시만 생략 */ }
546
- const pin = pinMarketplace();
547
- ({
548
- pinned: () => ok("마켓플레이스 고정 (origin fetch 차단)"),
549
- already: () => ok("이미 고정됨"),
550
- unpinned: () => ok("고정 해제 (origin 복구)"),
551
- "not-pinned": () => ok("고정된 적 없음"),
552
- needs: () => {},
553
- "no-marketplace": () => bad("마켓플레이스 디렉터리 없음 — 고정 불필요"),
554
- "no-git": () => bad("마켓플레이스 폴더가 git 저장소가 아님 — 고정하지 않음"),
555
- "no-remote": () => ok("origin remote 없음 — 이미 fetch 불가"),
556
- }[pin] || (() => {}))();
627
+ let pin = "skipped";
628
+ if (ROOT) {
629
+ log(" 건너뜀 (--root 는 마켓플레이스와 settings.json 을 건드리지 않음)");
630
+ } else {
631
+ // Claude Code 는 세션 시작 후 최대 10분 지연으로 마켓플레이스를 자동 fetch 한다.
632
+ // 공식 차단 수단은 두 가지뿐이고 각각 한계가 있어, 현재 상태를 같이 보여준다.
633
+ // - DISABLE_AUTOUPDATER=1 : 확실하지만 전역(다른 마켓플레이스도 멈춤)
634
+ // - /plugin UI 의 마켓플레이스별 토글 : omc 만 끄지만 UI 상태라 파일로 확인 불가
635
+ try {
636
+ const st = JSON.parse(readFileSync(join(CFG, "settings.json"), "utf8"));
637
+ const off = st?.env?.DISABLE_AUTOUPDATER;
638
+ log(` DISABLE_AUTOUPDATER: ${off ? `${off} (전역 차단 중)` : "미설정 (다른 마켓플레이스는 계속 자동 업데이트)"}`);
639
+ } catch { /* settings.json 이 없거나 읽을 수 없으면 표시만 생략 */ }
640
+ pin = pinMarketplace();
641
+ ({
642
+ pinned: () => ok("마켓플레이스 고정 (origin fetch 차단)"),
643
+ already: () => ok("이미 고정됨"),
644
+ unpinned: () => ok("고정 해제 (origin 복구)"),
645
+ "not-pinned": () => ok("고정된 적 없음"),
646
+ needs: () => {},
647
+ "no-marketplace": () => bad("마켓플레이스 디렉터리 없음 — 고정 불필요"),
648
+ "no-git": () => bad("마켓플레이스 폴더가 git 저장소가 아님 — 고정하지 않음"),
649
+ "no-remote": () => ok("origin remote 없음 — 이미 fetch 불가"),
650
+ }[pin] || (() => {}))();
651
+ }
557
652
 
558
653
  log();
559
654
  if (MODE === "check") {
560
655
  // 판정은 «활성 버전» 기준이다. apply 는 활성 버전만 손대므로, 판정만 모든 버전을 보면
561
656
  // 캐시에 옛 버전이 남아 있는 한 apply 를 몇 번 돌려도 영원히 «미적용» 으로 나온다
562
657
  // (3-OS 에서 동일하게 재현). 비활성 버전은 정보로만 알린다.
563
- const clean = (needBy[active] || 0) === 0 && !hjNeedBy[active] && pin !== "needs";
564
- const others = targets.filter((v) => v !== active && ((needBy[v] || 0) > 0 || hjNeedBy[v]));
565
- log(clean ? "결과: 모두 적용된 상태입니다." : "결과: 적용되지 않은 항목이 있습니다. 인자 없이 다시 실행하십시오.");
658
+ // 헬퍼 자리 결함이 하나라도 있으면 «모두 적용» 이 아니다. 다시 적용만으로는 안 고쳐지므로
659
+ // 그 안내를 먼저 낸다(되돌린 뒤 적용하면 «패치 필요» 항목도 함께 처리된다).
660
+ const misplacedActive = misplacedBy[active] || 0;
661
+ const clean = (needBy[active] || 0) === 0 && !hjNeedBy[active] && pin !== "needs" && misplacedActive === 0;
662
+ const others = targets.filter((v) => v !== active && ((needBy[v] || 0) > 0 || hjNeedBy[v] || (misplacedBy[v] || 0) > 0));
663
+ log(clean ? "결과: 모두 적용된 상태입니다."
664
+ : misplacedActive ? `결과: 헬퍼 자리 결함 ${misplacedActive}건이 있습니다. ${MISPLACED_HINT}`
665
+ : ROOT ? "결과: 적용되지 않은 항목이 있습니다. --check 없이 같은 --root 로 다시 실행하십시오."
666
+ : "결과: 적용되지 않은 항목이 있습니다. 인자 없이 다시 실행하십시오.");
566
667
  if (others.length) {
567
668
  log(` 참고: 비활성 버전 ${others.join(", ")} 에 미적용분이 있습니다.`);
568
669
  log(" 지금 쓰이지 않는 버전이라 판정에 넣지 않았습니다. 그 버전으로 되돌릴 계획이면 함께 적용하십시오.");
@@ -571,6 +672,7 @@ if (MODE === "check") {
571
672
  log("되돌리기 완료. Claude Code 를 재시작해야 반영됩니다.");
572
673
  } else {
573
674
  log(`완료. 실패 ${r.fail}건.`);
675
+ if (r.misplaced) log(`[!] 헬퍼 자리 결함 ${r.misplaced}건은 고치지 않았습니다. ${MISPLACED_HINT}`);
574
676
  log(" 훅 스크립트 패치는 즉시 적용됩니다 (훅이 호출될 때마다 파일을 새로 읽습니다).");
575
677
  log("[!] PostToolUse 훅 제거는 hooks.json 변경이라 각 세션이 다시 시작될 때부터 적용됩니다.");
576
678
  }