@kaydash9999/banker-plugins 0.15.2 → 0.16.1
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +65 -0
- package/README.md +31 -9
- package/codex/manifest.json +6 -3
- package/codex/transform-matrix.md +2 -0
- package/commands/progress.md +10 -0
- package/commands/setup.md +1 -1
- package/hooks/graceful-pause.mjs +31 -71
- package/hooks/hooks.json +11 -1
- package/hooks/progress-fallback.mjs +10 -0
- package/hooks/progress.mjs +427 -0
- package/hooks/register.mjs +82 -5
- package/package.json +1 -1
- package/skills/3d-intro-build/SKILL.md +238 -122
- package/skills/3d-intro-build/references/NOTICE.md +2 -1
- package/skills/3d-intro-build/references/assemble.mjs +164 -108
- package/skills/3d-intro-build/references/azure-adapter.mjs +204 -44
- package/skills/3d-intro-build/references/curate.mjs +573 -267
- package/skills/3d-intro-build/references/intro-fixes.css +61 -0
- package/skills/3d-intro-build/references/panel-glass.css +55 -0
- package/skills/3d-intro-build/references/preview-lib.mjs +256 -0
- package/skills/3d-intro-build/references/prompts.md +32 -9
- package/skills/3d-intro-build/references/serve.mjs +111 -103
- package/skills/3d-intro-build/references/step-nav.css +59 -0
- package/skills/3d-intro-build/references/step-nav.js +503 -0
- package/skills/3d-intro-build/references/verify-intro.mjs +474 -0
- package/skills/3d-intro-build/references/verify.md +45 -0
- package/skills/3d-intro-build/references/video-pool.mjs +817 -0
- package/skills/3d-intro-setup/SKILL.md +120 -47
- package/skills/3d-intro-setup/references/azure-adapter.mjs +204 -44
- package/skills/3d-intro-setup/references/video-pool.mjs +817 -0
- package/skills/motion-graphic-make/SKILL.md +73 -28
- package/skills/motion-graphic-make/references/curate.mjs +607 -0
- package/skills/motion-graphic-make/references/preview-lib.mjs +256 -0
- package/skills/motion-graphic-make/references/serve.mjs +131 -0
- package/skills/omc-patch/SKILL.md +143 -0
- package/skills/omc-patch/scripts/omc-patch.mjs +577 -0
- package/skills/remains/SKILL.md +125 -0
- package/skills/remains/scripts/boxes.mjs +448 -0
- package/skills/trouble-shooting/SKILL.md +50 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OMC 패치 — 버전 고정 + 훅 결함 수정 + 자동 업데이트 차단.
|
|
4
|
+
*
|
|
5
|
+
* node omc-patch.mjs 업데이트(5.3.0) + 패치 + 고정
|
|
6
|
+
* node omc-patch.mjs --check 진단만 (아무것도 바꾸지 않음)
|
|
7
|
+
* node omc-patch.mjs --revert 패치·고정 되돌리기
|
|
8
|
+
* node omc-patch.mjs --no-update 버전은 그대로 두고 패치만
|
|
9
|
+
*
|
|
10
|
+
* 활성 버전은 installed_plugins.json 의 oh-my-claudecode@omc 항목(version, installPath)에서
|
|
11
|
+
* 읽는다. 캐시에 5.3.0 이 있다고 5.3.0 을 활성으로 보지 않는다(--no-update 는 활성 버전을 패치).
|
|
12
|
+
* OMC_PATCH_INSTALLED=<json 경로> 로 읽을 파일을 바꿀 수 있다(시험용, --check 와 함께).
|
|
13
|
+
*
|
|
14
|
+
* OS: Windows / macOS / Linux 공통. 런처는 apply.cmd(Windows), apply.sh(그 외).
|
|
15
|
+
* 배경과 근거는 같은 폴더의 README.md.
|
|
16
|
+
*/
|
|
17
|
+
import {
|
|
18
|
+
readFileSync, writeFileSync, existsSync, copyFileSync, readdirSync,
|
|
19
|
+
unlinkSync, mkdirSync, rmSync, symlinkSync,
|
|
20
|
+
} from "node:fs";
|
|
21
|
+
import { join, dirname } from "node:path";
|
|
22
|
+
import { homedir, hostname } from "node:os";
|
|
23
|
+
import { execFileSync } from "node:child_process";
|
|
24
|
+
|
|
25
|
+
const TARGET_VERSION = "5.3.0";
|
|
26
|
+
// 3초는 좁았다. 이 예산은 «정상이지만 느린» 로드까지 잘라내는데, 파일 스캔이 8.2배 느려진
|
|
27
|
+
// 이 PC 가 정확히 그 조건이다(실측: 4초 지연에서 session-end 가 조용히 건너뛰어졌다).
|
|
28
|
+
// 대안이 «무한 정지» 인 자리에만 걸리므로 넉넉히 잡는다.
|
|
29
|
+
const IMPORT_TIMEOUT_MS = 10000;
|
|
30
|
+
// 타임아웃이 걸렸다는 것은 그 import 가 사실상 끝나지 않는다는 뜻이다. 훅은 완료 신호를
|
|
31
|
+
// 내고 제 할 일을 끝내지만, 매달린 로드가 이벤트 루프를 붙잡아 «프로세스는 남는다»
|
|
32
|
+
// (실측: 신호는 3초에 나왔는데 12초까지 살아 있어 강제종료해야 했다). 신호를 낼 시간을
|
|
33
|
+
// 준 뒤 스스로 끝내 프로세스 누적을 막는다.
|
|
34
|
+
const EXIT_GRACE_MS = 2000;
|
|
35
|
+
const MARKETPLACE = "omc";
|
|
36
|
+
const PLUGIN_KEY = "oh-my-claudecode@omc";
|
|
37
|
+
const REPO_URL = "https://github.com/Yeachan-Heo/oh-my-claudecode.git";
|
|
38
|
+
|
|
39
|
+
const argv = new Set(process.argv.slice(2));
|
|
40
|
+
const MODE = argv.has("--revert") ? "revert" : argv.has("--check") ? "check" : "apply";
|
|
41
|
+
const NO_UPDATE = argv.has("--no-update");
|
|
42
|
+
|
|
43
|
+
const CFG = join(homedir(), ".claude");
|
|
44
|
+
const PLUGINS = join(CFG, "plugins");
|
|
45
|
+
const CACHE = join(PLUGINS, "cache", MARKETPLACE, "oh-my-claudecode");
|
|
46
|
+
const MARKET = join(PLUGINS, "marketplaces", MARKETPLACE);
|
|
47
|
+
// OMC_PATCH_INSTALLED: 시험용. installed_plugins.json 대신 읽을 JSON 경로(읽기 전용 용도).
|
|
48
|
+
const INSTALLED = process.env.OMC_PATCH_INSTALLED || join(PLUGINS, "installed_plugins.json");
|
|
49
|
+
const PINFILE = join(CFG, "omc-local-patches", "PINNED");
|
|
50
|
+
|
|
51
|
+
const log = (s = "") => console.log(s);
|
|
52
|
+
const bad = (s) => console.log(` [!] ${s}`);
|
|
53
|
+
const ok = (s) => console.log(` ok ${s}`);
|
|
54
|
+
|
|
55
|
+
// dist/ 로드가 hang 하면 main() 이 시작조차 못 해 킬스위치·타임아웃·완료신호가
|
|
56
|
+
// 한꺼번에 무력화된다(실측: 도구 호출 하나가 63.8분 정지, 훅 프로세스 34개 누적).
|
|
57
|
+
//
|
|
58
|
+
// 타임아웃 시 null 이 아니라 빈 객체를 돌려주는 것이 중요하다. 호출부 다수가
|
|
59
|
+
// const { a, b } = await import(...)
|
|
60
|
+
// 형태의 구조 분해라서 null 이면 그 자리에서 TypeError 로 죽는다. 빈 객체면 각
|
|
61
|
+
// 심볼이 undefined 가 되고, 호출부에 이미 있는 null 검사/try-catch 가 no-op
|
|
62
|
+
// 경로를 태운다. 정상 로드 시에는 동작이 전혀 바뀌지 않는다.
|
|
63
|
+
const HELPER = `
|
|
64
|
+
// --- OMC local patch: bound top-level dynamic import (see ~/.claude/omc-local-patches/README.md)
|
|
65
|
+
const __omcRaceImport = (p) => {
|
|
66
|
+
let t;
|
|
67
|
+
const timed = new Promise((res) => {
|
|
68
|
+
// 이 타이머는 ref 된 채로 두어야 한다. unref 하면 «대기 중인 I/O 가 없는» 매달림에서
|
|
69
|
+
// Node 가 unsettled top-level await 를 감지해 먼저 exit 13 으로 빠지고, 타이머는
|
|
70
|
+
// 뜨지도 못한다(실측: 원본과 패치본이 똑같이 150ms 안에 죽었다). import 가 정상적으로
|
|
71
|
+
// 끝나면 아래에서 clearTimeout 하므로 정상 경로는 한 톨도 느려지지 않는다.
|
|
72
|
+
t = setTimeout(() => {
|
|
73
|
+
const k = setTimeout(() => process.exit(typeof process.exitCode === 'number' ? process.exitCode : 0), ${EXIT_GRACE_MS});
|
|
74
|
+
if (k && k.unref) k.unref();
|
|
75
|
+
// 빈 객체를 돌려주는 것이 중요하다. 호출부 다수가 \`const { a } = await import(...)\`
|
|
76
|
+
// 형태의 구조 분해라서 null 이면 그 자리에서 TypeError 로 죽는다. 빈 객체면 각
|
|
77
|
+
// 심볼이 undefined 가 되고, 호출부에 이미 있는 null 검사/try-catch 가 no-op 경로를 탄다.
|
|
78
|
+
res({});
|
|
79
|
+
}, ${IMPORT_TIMEOUT_MS});
|
|
80
|
+
});
|
|
81
|
+
const settled = p.then(
|
|
82
|
+
(m) => { clearTimeout(t); return m; },
|
|
83
|
+
(e) => { clearTimeout(t); throw e; },
|
|
84
|
+
);
|
|
85
|
+
return Promise.race([settled, timed]);
|
|
86
|
+
};
|
|
87
|
+
// --- end OMC local patch
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
const semverDesc = (a, b) => {
|
|
91
|
+
const x = a.split(".").map(Number), y = b.split(".").map(Number);
|
|
92
|
+
for (let i = 0; i < 3; i++) if ((x[i] || 0) !== (y[i] || 0)) return (y[i] || 0) - (x[i] || 0);
|
|
93
|
+
return 0;
|
|
94
|
+
};
|
|
95
|
+
const installedVersions = () =>
|
|
96
|
+
existsSync(CACHE) ? readdirSync(CACHE).filter((v) => /^\d+\.\d+\.\d+$/.test(v)).sort(semverDesc) : [];
|
|
97
|
+
|
|
98
|
+
// 실제 활성 버전은 Claude Code 가 읽는 installed_plugins.json 이 정한다(자동 업데이트로
|
|
99
|
+
// 캐시에 더 새 버전이 생겨도 TARGET_VERSION 이 활성이라는 보장은 없다). version 필드 →
|
|
100
|
+
// installPath 마지막 구간 → 옛 규칙(TARGET 이 캐시에 있으면 TARGET, 아니면 최신) 순으로 본다.
|
|
101
|
+
function activeVersion() {
|
|
102
|
+
const versions = installedVersions();
|
|
103
|
+
try {
|
|
104
|
+
const e = JSON.parse(readFileSync(INSTALLED, "utf8")).plugins?.[PLUGIN_KEY]?.[0];
|
|
105
|
+
if (e) {
|
|
106
|
+
if (typeof e.version === "string" && /^\d+\.\d+\.\d+$/.test(e.version) && versions.includes(e.version)) return { v: e.version, src: "installed_plugins.json" };
|
|
107
|
+
const last = typeof e.installPath === "string" ? e.installPath.split(/[\\/]+/).filter(Boolean).pop() : null;
|
|
108
|
+
if (last && versions.includes(last)) return { v: last, src: "installed_plugins.json installPath" };
|
|
109
|
+
}
|
|
110
|
+
} catch { /* 읽기·파싱 실패는 옛 규칙으로 */ }
|
|
111
|
+
return { v: versions.includes(TARGET_VERSION) ? TARGET_VERSION : versions[0] || TARGET_VERSION, src: "추정: installed_plugins.json 에서 확인 못 함" };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function git(args, cwd = MARKET) {
|
|
115
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── 1. 업데이트 ──────────────────────────────────────────────────────────────
|
|
119
|
+
function ensureVersion() {
|
|
120
|
+
const have = installedVersions();
|
|
121
|
+
if (have.includes(TARGET_VERSION)) { ok(`${TARGET_VERSION} 설치되어 있음`); return true; }
|
|
122
|
+
if (MODE === "check") { bad(`${TARGET_VERSION} 미설치 (설치된 버전: ${have.join(", ") || "없음"})`); return false; }
|
|
123
|
+
if (!existsSync(MARKET)) { bad(`마켓플레이스 없음: ${MARKET}\n Claude Code 에서 marketplace 를 먼저 추가하십시오: ${REPO_URL}`); return false; }
|
|
124
|
+
if (!existsSync(join(MARKET, ".git"))) { bad(`마켓플레이스 폴더가 git 저장소가 아님: ${MARKET}`); return false; }
|
|
125
|
+
|
|
126
|
+
// git 과 tar 에 의존한다. Windows 10+ / macOS / 대부분의 Linux 배포판에는 둘 다 있지만,
|
|
127
|
+
// 최소 설치 컨테이너나 tar 없는 환경에서는 없을 수 있다. 여기서 못 찾으면 아래에서
|
|
128
|
+
// execFileSync 가 ENOENT 로 죽어 원인이 안 보이므로, 먼저 확인하고 무엇이 없는지 말한다.
|
|
129
|
+
for (const [cmd, why] of [["git", "마켓플레이스에서 소스를 꺼내는 데"], ["tar", "그 소스를 푸는 데"]]) {
|
|
130
|
+
try { execFileSync(cmd, ["--version"], { stdio: "ignore" }); }
|
|
131
|
+
catch { bad(`${cmd} 를 찾을 수 없습니다 (${why} 필요). 설치한 뒤 다시 실행하거나, --no-update 로 패치만 적용하십시오.`); return false; }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
log(` ${TARGET_VERSION} 설치 중...`);
|
|
135
|
+
try { git(["fetch", "--tags", "--quiet", "origin"]); } catch { /* 오프라인이어도 로컬 태그가 있으면 진행 */ }
|
|
136
|
+
let sha;
|
|
137
|
+
try { sha = git(["rev-parse", `v${TARGET_VERSION}^{}`]); }
|
|
138
|
+
catch { bad(`태그 v${TARGET_VERSION} 를 마켓플레이스에서 찾을 수 없음`); return false; }
|
|
139
|
+
|
|
140
|
+
const dest = join(CACHE, TARGET_VERSION);
|
|
141
|
+
if (existsSync(dest)) rmSync(dest, { recursive: true, force: true });
|
|
142
|
+
mkdirSync(dest, { recursive: true });
|
|
143
|
+
// git archive | tar 로 소스를 편다. 저장소에 dist/ 가 포함되어 빌드는 불필요하다.
|
|
144
|
+
execFileSync("git", ["-C", MARKET, "archive", `v${TARGET_VERSION}`, "-o", join(dest, "_src.tar")], { stdio: "ignore" });
|
|
145
|
+
execFileSync("tar", ["-xf", join(dest, "_src.tar"), "-C", dest], { stdio: "ignore" });
|
|
146
|
+
unlinkSync(join(dest, "_src.tar"));
|
|
147
|
+
|
|
148
|
+
// node_modules 는 복사하지 않고 직전 버전 것을 링크한다. Windows 는 junction
|
|
149
|
+
// (관리자 권한 불필요), POSIX 는 type 인자가 무시되고 디렉터리 symlink 가 된다.
|
|
150
|
+
const prev = have.find((v) => v !== TARGET_VERSION);
|
|
151
|
+
const nm = join(dest, "node_modules");
|
|
152
|
+
if (!existsSync(nm) && prev && existsSync(join(CACHE, prev, "node_modules"))) {
|
|
153
|
+
try { symlinkSync(join(CACHE, prev, "node_modules"), nm, "junction"); ok(`node_modules → ${prev} 링크`); }
|
|
154
|
+
catch (e) { bad(`node_modules 링크 실패(${e.code}). \`npm install\` 이 필요할 수 있음`); }
|
|
155
|
+
} else if (!existsSync(nm)) {
|
|
156
|
+
bad(`node_modules 없음. ${dest} 에서 \`npm install --omit=dev\` 를 실행하십시오`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 활성 버전 전환
|
|
160
|
+
const j = JSON.parse(readFileSync(INSTALLED, "utf8"));
|
|
161
|
+
const e = j.plugins?.[PLUGIN_KEY]?.[0];
|
|
162
|
+
if (!e) { bad(`installed_plugins.json 에 ${PLUGIN_KEY} 항목이 없음 — 수동 확인 필요`); return false; }
|
|
163
|
+
if (!existsSync(`${INSTALLED}.omcbak`)) copyFileSync(INSTALLED, `${INSTALLED}.omcbak`);
|
|
164
|
+
const sep = e.installPath.includes("//") ? "//" : e.installPath.includes("\\") ? "\\" : "/";
|
|
165
|
+
e.installPath = e.installPath.split(sep).slice(0, -1).concat(TARGET_VERSION).join(sep);
|
|
166
|
+
e.version = TARGET_VERSION;
|
|
167
|
+
e.gitCommitSha = sha;
|
|
168
|
+
e.lastUpdated = new Date().toISOString();
|
|
169
|
+
writeFileSync(INSTALLED, JSON.stringify(j, null, 2) + "\n", "utf8");
|
|
170
|
+
ok(`${have[0] || "없음"} → ${TARGET_VERSION} (sha ${sha.slice(0, 12)})`);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── 2. 훅 패치 ───────────────────────────────────────────────────────────────
|
|
175
|
+
function hookNames(root) {
|
|
176
|
+
const files = [join(root, "hooks", "hooks.json"), `${join(root, "hooks", "hooks.json")}.omcbak`];
|
|
177
|
+
const src = files.filter(existsSync).map((f) => readFileSync(f, "utf8")).join("");
|
|
178
|
+
return [...new Set([...src.matchAll(/scripts\/([\w-]+)\.mjs/g)].map((m) => m[1]))].sort();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── 어디가 «진짜» 모듈 최상위인가 ────────────────────────────────────────────
|
|
182
|
+
// 예전 판정은 "`function main(` 보다 텍스트상 앞이면 top-level" 이었다. 이것은 틀렸다.
|
|
183
|
+
// 실측으로 v5.3.0 의 대상 16곳 중 5곳이 다른 함수 몸통 안이었고(recordToolInvocation,
|
|
184
|
+
// loadRuntimeModules ×2, runSessionEndHook, runWikiSessionEndHook), 그 자리들은 모듈
|
|
185
|
+
// 평가를 막지 못한다 — 이미 try/catch 안이라 실패해도 훅은 끝까지 간다. 거기에 예산을
|
|
186
|
+
// 걸면 «정상이지만 느린» 호출만 잘라낸다(실측: session-end 가 4초 지연에서 조용히 스킵).
|
|
187
|
+
//
|
|
188
|
+
// 그래서 문자열·템플릿·주석·정규식을 건너뛰며 중괄호를 세고, 각 중괄호가 «함수 몸통» 인지
|
|
189
|
+
// 단순 «블록» 인지 구분해 함수 스택을 유지한다. 함수 스택이 빈 상태에서 나온 await import
|
|
190
|
+
// 만 진짜 최상위다 — 최상위 `try { }` 안에 있어도 최상위다(persistent-mode 가 그렇다).
|
|
191
|
+
const ID_CH = /[A-Za-z0-9_$]/;
|
|
192
|
+
const BLOCK_KW = new Set(["if", "for", "while", "switch", "catch", "with", "do"]);
|
|
193
|
+
const REGEX_PREV_CH = new Set("(,=:[!&|?{};+-*%~^".split(""));
|
|
194
|
+
const REGEX_PREV_KW = new Set(["return", "typeof", "instanceof", "in", "of", "new", "delete",
|
|
195
|
+
"void", "case", "do", "else", "yield", "await"]);
|
|
196
|
+
|
|
197
|
+
function scanTopLevelAwaitImports(src) {
|
|
198
|
+
const n = src.length;
|
|
199
|
+
const fnStack = [];
|
|
200
|
+
const hits = [];
|
|
201
|
+
let i = 0, prev = "", prevIdx = -1;
|
|
202
|
+
|
|
203
|
+
const wordEndingAt = (j) => {
|
|
204
|
+
let e = j;
|
|
205
|
+
while (j >= 0 && ID_CH.test(src[j])) j--;
|
|
206
|
+
return src.slice(j + 1, e + 1);
|
|
207
|
+
};
|
|
208
|
+
const backSkip = (j) => {
|
|
209
|
+
for (;;) {
|
|
210
|
+
while (j >= 0 && /\s/.test(src[j])) j--;
|
|
211
|
+
if (j >= 1 && src[j] === "/" && src[j - 1] === "*") { // */ 로 끝나는 주석
|
|
212
|
+
const s = src.lastIndexOf("/*", j - 1);
|
|
213
|
+
if (s < 0) return -1;
|
|
214
|
+
j = s - 1; continue;
|
|
215
|
+
}
|
|
216
|
+
return j;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const skipString = (q) => {
|
|
220
|
+
i++;
|
|
221
|
+
while (i < n) {
|
|
222
|
+
const c = src[i];
|
|
223
|
+
if (c === "\\") { i += 2; continue; }
|
|
224
|
+
if (c === q) { i++; return; }
|
|
225
|
+
if (q === "`" && c === "$" && src[i + 1] === "{") {
|
|
226
|
+
i += 2;
|
|
227
|
+
let d = 1;
|
|
228
|
+
while (i < n && d > 0) {
|
|
229
|
+
const k = src[i];
|
|
230
|
+
if (k === "\\") { i += 2; continue; }
|
|
231
|
+
if (k === "'" || k === '"' || k === "`") { skipString(k); continue; }
|
|
232
|
+
if (k === "{") d++;
|
|
233
|
+
else if (k === "}") d--;
|
|
234
|
+
i++;
|
|
235
|
+
}
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
i++;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
// 이 중괄호가 «함수 몸통» 을 여는가
|
|
242
|
+
const isFunctionBrace = (at) => {
|
|
243
|
+
let j = backSkip(at - 1);
|
|
244
|
+
if (j < 0) return false;
|
|
245
|
+
if (src[j] === ">" && src[j - 1] === "=") return true; // () => {
|
|
246
|
+
if (src[j] !== ")") return false; // class/객체/블록
|
|
247
|
+
let d = 0;
|
|
248
|
+
for (; j >= 0; j--) { // 짝 맞는 ( 찾기
|
|
249
|
+
if (src[j] === ")") d++;
|
|
250
|
+
else if (src[j] === "(") { if (--d === 0) break; }
|
|
251
|
+
}
|
|
252
|
+
if (j < 0) return false;
|
|
253
|
+
const k = backSkip(j - 1);
|
|
254
|
+
if (k < 0 || !ID_CH.test(src[k])) return false;
|
|
255
|
+
return !BLOCK_KW.has(wordEndingAt(k)); // if/for/while/catch… 은 블록
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
while (i < n) {
|
|
259
|
+
const c = src[i];
|
|
260
|
+
if (c === "/" && src[i + 1] === "/") { while (i < n && src[i] !== "\n") i++; continue; }
|
|
261
|
+
if (c === "/" && src[i + 1] === "*") { const e = src.indexOf("*/", i + 2); i = e < 0 ? n : e + 2; continue; }
|
|
262
|
+
if (c === "'" || c === '"' || c === "`") { skipString(c); prev = c; continue; }
|
|
263
|
+
if (/\s/.test(c)) { i++; continue; }
|
|
264
|
+
if (c === "/") {
|
|
265
|
+
const asRegex = prev === "" || REGEX_PREV_CH.has(prev)
|
|
266
|
+
|| (prevIdx >= 0 && ID_CH.test(prev) && REGEX_PREV_KW.has(wordEndingAt(prevIdx)));
|
|
267
|
+
if (asRegex) {
|
|
268
|
+
i++;
|
|
269
|
+
let inClass = false;
|
|
270
|
+
while (i < n) {
|
|
271
|
+
const k = src[i];
|
|
272
|
+
if (k === "\\") { i += 2; continue; }
|
|
273
|
+
if (k === "[") inClass = true;
|
|
274
|
+
else if (k === "]") inClass = false;
|
|
275
|
+
else if (k === "/" && !inClass) { i++; break; }
|
|
276
|
+
else if (k === "\n") break;
|
|
277
|
+
i++;
|
|
278
|
+
}
|
|
279
|
+
while (i < n && ID_CH.test(src[i])) i++; // 플래그
|
|
280
|
+
prev = "/"; prevIdx = i - 1; continue;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (c === "{") { fnStack.push(isFunctionBrace(i)); i++; prev = "{"; prevIdx = i - 1; continue; }
|
|
284
|
+
if (c === "}") { fnStack.pop(); i++; prev = "}"; prevIdx = i - 1; continue; }
|
|
285
|
+
if (c === "a" && !(i > 0 && ID_CH.test(src[i - 1])) && /^await\s+import\s*\(/.test(src.slice(i, i + 40))) {
|
|
286
|
+
if (!fnStack.some(Boolean)) hits.push(i);
|
|
287
|
+
}
|
|
288
|
+
prev = c; prevIdx = i; i++;
|
|
289
|
+
}
|
|
290
|
+
return hits;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 자리별 상태. 이미 손으로 Promise.race 를 두른 자리는 건드리지 않는다(4.15.7 에 그런 것이 둘 있다).
|
|
294
|
+
function siteInfo(src) {
|
|
295
|
+
if (src.includes("__omcRaceImport")) return { state: "patched", sites: [] };
|
|
296
|
+
const all = scanTopLevelAwaitImports(src);
|
|
297
|
+
if (!all.length) return { state: "n/a", sites: [] };
|
|
298
|
+
const open = all.filter((p) => !/Promise\.race/.test(src.slice(Math.max(0, p - 300), p)));
|
|
299
|
+
if (!open.length) return { state: "guarded", sites: [] };
|
|
300
|
+
return { state: "needs", sites: open };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const needsPatch = (src) => siteInfo(src).state;
|
|
304
|
+
|
|
305
|
+
function transform(src) {
|
|
306
|
+
const info = siteInfo(src);
|
|
307
|
+
if (info.state !== "needs") return null;
|
|
308
|
+
const edits = [];
|
|
309
|
+
for (const start of info.sites) {
|
|
310
|
+
const open = src.indexOf("(", src.indexOf("import", start));
|
|
311
|
+
let depth = 0, end = -1;
|
|
312
|
+
for (let i = open; i < src.length; i++) {
|
|
313
|
+
const c = src[i];
|
|
314
|
+
if (c === "(") depth++;
|
|
315
|
+
else if (c === ")" && --depth === 0) { end = i; break; }
|
|
316
|
+
}
|
|
317
|
+
if (end !== -1) edits.push({ start, end });
|
|
318
|
+
}
|
|
319
|
+
if (!edits.length) return null;
|
|
320
|
+
let out = src;
|
|
321
|
+
for (const e of edits.reverse()) {
|
|
322
|
+
const expr = out.slice(e.start + out.slice(e.start).indexOf("import"), e.end + 1);
|
|
323
|
+
out = `${out.slice(0, e.start)}await __omcRaceImport(${expr})${out.slice(e.end + 1)}`;
|
|
324
|
+
}
|
|
325
|
+
// 헬퍼는 모듈 맨 앞(셔뱅 줄이 있으면 그 다음)에 넣는다. "첫 사용 지점의 줄 시작" 앞에 넣던
|
|
326
|
+
// 예전 방식은 그 지점이 최상위 `try { }` 같은 블록 안이면 헬퍼도 그 블록에 갇혀, 다른 블록의
|
|
327
|
+
// 자리에서 ReferenceError 가 났다(OMC 5.6.x project-memory-session.mjs. 훅의 catch 가 삼켜
|
|
328
|
+
// 기능이 조용히 꺼졌다). 정적 import 는 끌어올려지므로 그 앞에 두어도 되고, 여러 줄
|
|
329
|
+
// `import {` ... `} from` 한가운데를 뚫을 일도 없다.
|
|
330
|
+
if (!out.includes("await __omcRaceImport")) return null;
|
|
331
|
+
const ls = out.startsWith("#!") ? out.indexOf("\n") + 1 : 0;
|
|
332
|
+
return out.slice(0, ls) + HELPER + out.slice(ls);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function syntaxError(file) {
|
|
336
|
+
try { execFileSync(process.execPath, ["--check", file], { stdio: ["ignore", "ignore", "pipe"] }); return null; }
|
|
337
|
+
catch (e) {
|
|
338
|
+
const msg = String(e.stderr || e.message || "");
|
|
339
|
+
return (msg.split("\n").find((l) => /SyntaxError|Error:/.test(l)) || msg.split("\n")[0] || "unknown").trim().slice(0, 110);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 이 도구가 아닌 «손» 이 import 를 감싼 흔적. 우리 헬퍼는 안 쓰고 Promise.race 를
|
|
344
|
+
// import 바로 앞에 직접 붙인 모양이다.
|
|
345
|
+
const looksHandPatched = (src) =>
|
|
346
|
+
!src.includes("__omcRaceImport") && /Promise\.race\s*\(\s*\[?[\s\S]{0,120}?\bimport\s*\(/.test(src);
|
|
347
|
+
|
|
348
|
+
function patchHooks(root) {
|
|
349
|
+
let done = 0, skip = 0, fail = 0, need = 0;
|
|
350
|
+
const foreign = [];
|
|
351
|
+
// revert 는 scripts/ 전체를 훑는다. hooks.json 기반 목록은 두 번 새는데, 실측으로
|
|
352
|
+
// 4.15.7 이 둘 다 겪었다 — PostToolUse 를 지우면 그 스크립트가 목록에서 빠지고(그래서
|
|
353
|
+
// .omcbak 이 있어도 안 돌아온다), 손으로 고친 파일은 .omcbak 조차 없어 아예 안 보인다.
|
|
354
|
+
const names = MODE === "revert"
|
|
355
|
+
? readdirSync(join(root, "scripts")).filter((f) => f.endsWith(".mjs")).map((f) => f.slice(0, -4)).sort()
|
|
356
|
+
: hookNames(root);
|
|
357
|
+
for (const n of names) {
|
|
358
|
+
const f = join(root, "scripts", `${n}.mjs`);
|
|
359
|
+
const bak = `${f}.omcbak`;
|
|
360
|
+
if (!existsSync(f)) continue;
|
|
361
|
+
|
|
362
|
+
if (MODE === "revert") {
|
|
363
|
+
if (existsSync(bak)) { copyFileSync(bak, f); unlinkSync(bak); log(` 되돌림 ${n}`); done++; continue; }
|
|
364
|
+
// 백업이 없는데 이미 감싸여 있다면 «이 도구가 아닌 손» 이 고친 것이다. 되돌릴 원본이
|
|
365
|
+
// 디스크에 없으니 조용히 넘기지 않고 이름을 댄다(실측: 4.15.7 의 두 파일이 그랬다).
|
|
366
|
+
// 주의: 손 패치는 `await import(` 를 `await Promise.race([import(` 로 «바꿔치기» 하므로
|
|
367
|
+
// await import 패턴 자체가 사라진다. siteInfo 로는 잡히지 않아 원문에서 직접 찾는다.
|
|
368
|
+
if (looksHandPatched(readFileSync(f, "utf8"))) foreign.push(n);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const st = needsPatch(readFileSync(f, "utf8"));
|
|
372
|
+
if (st !== "needs") { skip++; continue; }
|
|
373
|
+
if (MODE === "check") { bad(`패치 필요: ${n}`); need++; continue; }
|
|
374
|
+
|
|
375
|
+
const out = transform(readFileSync(f, "utf8"));
|
|
376
|
+
if (!out) { bad(`변환 실패: ${n}`); fail++; continue; }
|
|
377
|
+
if (!existsSync(bak)) copyFileSync(f, bak);
|
|
378
|
+
writeFileSync(f, out, "utf8");
|
|
379
|
+
const err = syntaxError(f);
|
|
380
|
+
if (err) { copyFileSync(bak, f); bad(`문법오류로 롤백: ${n}\n ${err}`); fail++; continue; }
|
|
381
|
+
done++;
|
|
382
|
+
}
|
|
383
|
+
return { done, skip, fail, need, foreign };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ── 3. PostToolUse 훅 제거 ───────────────────────────────────────────────────
|
|
387
|
+
function patchHooksJson(root) {
|
|
388
|
+
const hj = join(root, "hooks", "hooks.json");
|
|
389
|
+
const bak = `${hj}.omcbak`;
|
|
390
|
+
if (!existsSync(hj)) return "no-file";
|
|
391
|
+
if (MODE === "revert") {
|
|
392
|
+
if (existsSync(bak)) { copyFileSync(bak, hj); unlinkSync(bak); log(" 되돌림 hooks.json"); return "reverted"; }
|
|
393
|
+
// 이 도구가 만든 백업이 없어도, 손으로 만든 백업이 남아 있을 수 있다(실측: 4.15.7 의
|
|
394
|
+
// hooks.json.bak-20260906-postoolremove). PostToolUse 를 담고 있는 유효한 JSON 이
|
|
395
|
+
// 딱 하나면 그것이 제거 직전 상태다 — 무엇을 썼는지 밝히고 되돌린다.
|
|
396
|
+
const dir = join(root, "hooks");
|
|
397
|
+
const cands = readdirSync(dir)
|
|
398
|
+
.filter((f) => f.startsWith("hooks.json.") && f !== "hooks.json.omcbak")
|
|
399
|
+
.filter((f) => {
|
|
400
|
+
try { return !!JSON.parse(readFileSync(join(dir, f), "utf8"))?.hooks?.PostToolUse; }
|
|
401
|
+
catch { return false; }
|
|
402
|
+
});
|
|
403
|
+
if (cands.length === 1) {
|
|
404
|
+
copyFileSync(join(dir, cands[0]), hj);
|
|
405
|
+
log(` 되돌림 hooks.json (수동 백업 사용: ${cands[0]})`);
|
|
406
|
+
return "reverted";
|
|
407
|
+
}
|
|
408
|
+
if (cands.length > 1) { bad(`hooks.json 수동 백업이 여러 개라 자동 복구하지 않음: ${cands.join(", ")}`); return "ambiguous"; }
|
|
409
|
+
return "no-backup";
|
|
410
|
+
}
|
|
411
|
+
const j = JSON.parse(readFileSync(hj, "utf8"));
|
|
412
|
+
if (!j.hooks?.PostToolUse) return "already";
|
|
413
|
+
const list = j.hooks.PostToolUse.flatMap((g) => (g.hooks || []).map((h) => (h.command || "").match(/([\w-]+)\.mjs/)?.[1] || "?"));
|
|
414
|
+
if (MODE === "check") { bad(`hooks.json PostToolUse 남아있음: ${list.join(", ")}`); return "needs"; }
|
|
415
|
+
if (!existsSync(bak)) copyFileSync(hj, bak);
|
|
416
|
+
delete j.hooks.PostToolUse;
|
|
417
|
+
writeFileSync(hj, JSON.stringify(j, null, 2) + "\n", "utf8");
|
|
418
|
+
try { JSON.parse(readFileSync(hj, "utf8")); return list; }
|
|
419
|
+
catch { copyFileSync(bak, hj); bad("hooks.json 손상으로 롤백"); return "failed"; }
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── 4. 자동 업데이트 차단 ────────────────────────────────────────────────────
|
|
423
|
+
//
|
|
424
|
+
// Claude Code 는 마켓플레이스 git 저장소를 갱신한 뒤 플러그인을 새 버전으로
|
|
425
|
+
// 올릴 수 있다. 그러면 위 패치가 통째로 덮어써진다. 로컬에서 막을 수 있는
|
|
426
|
+
// 지점은 "마켓플레이스 저장소가 새 커밋을 가져오지 못하게" 하는 것뿐이다.
|
|
427
|
+
// origin remote 의 URL 을 무효 주소로 바꿔 fetch 를 실패시킨다(원래 URL 은
|
|
428
|
+
// PINNED 파일에 적어 두고 --revert 로 복구한다). 저장소 자체는 그대로라
|
|
429
|
+
// 이미 받은 태그로 재설치·검증이 가능하다.
|
|
430
|
+
function pinMarketplace() {
|
|
431
|
+
if (!existsSync(MARKET)) return "no-marketplace";
|
|
432
|
+
// 마켓플레이스 폴더가 제 git 저장소가 아니면 git 이 위쪽 저장소(예: dotfiles 로 관리하는
|
|
433
|
+
// ~/.claude)를 찾아 그 origin 을 바꾼다. 제 .git 이 있을 때만 고정하고 되돌린다.
|
|
434
|
+
if (!existsSync(join(MARKET, ".git"))) return "no-git";
|
|
435
|
+
let url;
|
|
436
|
+
try { url = git(["remote", "get-url", "origin"]); } catch { return "no-remote"; }
|
|
437
|
+
|
|
438
|
+
if (MODE === "revert") {
|
|
439
|
+
// 원본 URL 은 현재 origin 안에 그대로 들어 있다(`omc-pinned://` + 원본). 파일이 아니라
|
|
440
|
+
// 거기서 복원한다. PINNED 는 배포 폴더에 같이 딸려 오므로 다른 PC 의 값일 수 있고,
|
|
441
|
+
// PINFILE 은 스크립트 위치가 아니라 homedir() 기준이라 그 사실이 눈에 띄지도 않는다.
|
|
442
|
+
const PIN_PREFIX = "omc-pinned://";
|
|
443
|
+
let restored = null;
|
|
444
|
+
if (url.startsWith(PIN_PREFIX)) restored = url.slice(PIN_PREFIX.length);
|
|
445
|
+
if (existsSync(PINFILE)) {
|
|
446
|
+
let saved = {};
|
|
447
|
+
try { saved = JSON.parse(readFileSync(PINFILE, "utf8")); } catch { /* 깨졌으면 무시 */ }
|
|
448
|
+
if (saved.host && saved.host !== hostname()) {
|
|
449
|
+
bad(`PINNED 는 다른 기기(${saved.host})의 기록입니다 — 그 값은 쓰지 않고 현재 origin 에서 복원합니다.`);
|
|
450
|
+
}
|
|
451
|
+
if (!restored && saved.originUrl) restored = saved.originUrl;
|
|
452
|
+
unlinkSync(PINFILE);
|
|
453
|
+
}
|
|
454
|
+
if (!restored) return "not-pinned";
|
|
455
|
+
try { git(["remote", "set-url", "origin", restored]); } catch { /* noop */ }
|
|
456
|
+
return "unpinned";
|
|
457
|
+
}
|
|
458
|
+
if (url.startsWith("omc-pinned://")) return "already";
|
|
459
|
+
if (MODE === "check") { bad(`마켓플레이스 미고정 (origin=${url})`); return "needs"; }
|
|
460
|
+
|
|
461
|
+
mkdirSync(dirname(PINFILE), { recursive: true });
|
|
462
|
+
// host 를 남긴다. 이 폴더는 통째로 복사해 배포하므로 PINNED 도 같이 딸려 가는데,
|
|
463
|
+
// PINFILE 은 스크립트 위치가 아니라 homedir() 기준이라 남의 기록인 줄 모르고 쓰기 쉽다.
|
|
464
|
+
writeFileSync(PINFILE, JSON.stringify({ originUrl: url, pinnedAt: new Date().toISOString(), version: TARGET_VERSION, host: hostname() }, null, 2) + "\n", "utf8");
|
|
465
|
+
git(["remote", "set-url", "origin", `omc-pinned://${url}`]);
|
|
466
|
+
return "pinned";
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ── main ─────────────────────────────────────────────────────────────────────
|
|
470
|
+
log(`OMC 패치 (대상 ${TARGET_VERSION}) 모드: ${MODE}${NO_UPDATE ? " --no-update" : ""}`);
|
|
471
|
+
log(`플랫폼: ${process.platform} node ${process.version}`);
|
|
472
|
+
log();
|
|
473
|
+
|
|
474
|
+
if (!existsSync(INSTALLED)) { bad(`Claude Code 플러그인 설정을 찾을 수 없음: ${INSTALLED}`); process.exit(1); }
|
|
475
|
+
|
|
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); }
|
|
480
|
+
|
|
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})을 그 자리에서 패치함`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
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
|
+
|
|
496
|
+
log();
|
|
497
|
+
log(`[2/4] 훅 패치${MODE === "apply" ? ` (${active})` : ` (대상 ${targets.length}개 버전)`}`);
|
|
498
|
+
const r = { done: 0, skip: 0, fail: 0, need: 0 };
|
|
499
|
+
const needBy = {};
|
|
500
|
+
const foreignBy = {};
|
|
501
|
+
for (const v of targets) {
|
|
502
|
+
const root = join(CACHE, v);
|
|
503
|
+
if (!existsSync(root)) continue;
|
|
504
|
+
if (targets.length > 1) log(` — ${v}`);
|
|
505
|
+
const x = patchHooks(root);
|
|
506
|
+
needBy[v] = x.need;
|
|
507
|
+
for (const k of ["done", "skip", "fail", "need"]) r[k] += x[k];
|
|
508
|
+
if (x.foreign?.length) foreignBy[v] = x.foreign;
|
|
509
|
+
}
|
|
510
|
+
log(` ${MODE === "check" ? `패치 필요 ${r.need}건` : MODE === "revert" ? `되돌림 ${r.done}건` : `패치 ${r.done}건 / 실패 ${r.fail}건`} / 대상 아님 ${r.skip}건`);
|
|
511
|
+
// 이 도구가 만들지 않은 수정은 되돌릴 원본이 디스크에 없다. 조용히 넘기면 «완전히
|
|
512
|
+
// 되돌렸다» 는 잘못된 인상을 준다 — 무엇이 남았고 어떻게 되찾는지 정확히 말한다.
|
|
513
|
+
for (const [v, list] of Object.entries(foreignBy)) {
|
|
514
|
+
bad(`${v}: 이 도구가 만들지 않은 수정이 남아 있습니다 — ${list.join(", ")}`);
|
|
515
|
+
log(` 백업(.omcbak)이 없어 되돌릴 원본이 디스크에 없습니다. 원본이 필요하면:`);
|
|
516
|
+
log(` git -C "${MARKET}" archive v${v} scripts/<이름>.mjs | tar -x -C "${join(CACHE, v)}"`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
log();
|
|
520
|
+
log("[3/4] PostToolUse 훅 제거");
|
|
521
|
+
const hjNeedBy = {};
|
|
522
|
+
for (const v of targets) {
|
|
523
|
+
const root = join(CACHE, v);
|
|
524
|
+
if (!existsSync(root)) continue;
|
|
525
|
+
const hj = patchHooksJson(root);
|
|
526
|
+
const tag = targets.length > 1 ? `${v}: ` : "";
|
|
527
|
+
if (Array.isArray(hj)) ok(`${tag}제거: ${hj.join(", ")}`);
|
|
528
|
+
else if (hj === "already") ok(`${tag}이미 제거됨`);
|
|
529
|
+
else if (hj === "needs") hjNeedBy[v] = true;
|
|
530
|
+
else if (hj === "reverted" || hj === "ambiguous") { /* patchHooksJson 이 출력함 */ }
|
|
531
|
+
else if (hj === "no-backup") ok(`${tag}되돌릴 백업 없음 (이 도구가 지운 적 없음)`);
|
|
532
|
+
else if (hj === "no-file") bad(`${tag}hooks.json 파일 자체가 없음`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
log();
|
|
536
|
+
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] || (() => {}))();
|
|
557
|
+
|
|
558
|
+
log();
|
|
559
|
+
if (MODE === "check") {
|
|
560
|
+
// 판정은 «활성 버전» 기준이다. apply 는 활성 버전만 손대므로, 판정만 모든 버전을 보면
|
|
561
|
+
// 캐시에 옛 버전이 남아 있는 한 apply 를 몇 번 돌려도 영원히 «미적용» 으로 나온다
|
|
562
|
+
// (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 ? "결과: 모두 적용된 상태입니다." : "결과: 적용되지 않은 항목이 있습니다. 인자 없이 다시 실행하십시오.");
|
|
566
|
+
if (others.length) {
|
|
567
|
+
log(` 참고: 비활성 버전 ${others.join(", ")} 에 미적용분이 있습니다.`);
|
|
568
|
+
log(" 지금 쓰이지 않는 버전이라 판정에 넣지 않았습니다. 그 버전으로 되돌릴 계획이면 함께 적용하십시오.");
|
|
569
|
+
}
|
|
570
|
+
} else if (MODE === "revert") {
|
|
571
|
+
log("되돌리기 완료. Claude Code 를 재시작해야 반영됩니다.");
|
|
572
|
+
} else {
|
|
573
|
+
log(`완료. 실패 ${r.fail}건.`);
|
|
574
|
+
log(" 훅 스크립트 패치는 즉시 적용됩니다 (훅이 호출될 때마다 파일을 새로 읽습니다).");
|
|
575
|
+
log("[!] PostToolUse 훅 제거는 hooks.json 변경이라 각 세션이 다시 시작될 때부터 적용됩니다.");
|
|
576
|
+
}
|
|
577
|
+
process.exit(r.fail > 0 ? 1 : 0);
|