@kaydash9999/banker-plugins 0.17.0 → 0.18.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +44 -0
- package/README.md +10 -3
- package/codex/manifest.json +2 -1
- package/codex/transform-matrix.md +4 -2
- package/commands/front-qa.md +3 -3
- package/package.json +1 -1
- package/skills/3d-intro-build/SKILL.md +1 -1
- package/skills/3d-intro-build/references/assemble.mjs +3 -3
- package/skills/3d-intro-setup/SKILL.md +3 -3
- package/skills/anti-drm/SKILL.md +230 -0
- package/skills/anti-drm/scripts/anti-drm.mjs +552 -0
- package/skills/anti-drm/scripts/instruction-toggle.mjs +527 -0
- package/skills/lineage/SKILL.md +3 -3
- package/skills/lineage/lineage.py +16 -7
- package/skills/omc-patch/SKILL.md +12 -4
- package/skills/omc-patch/scripts/omc-patch.mjs +477 -225
- package/skills/omc-reference/SKILL.md +1 -1
- package/skills/ralph-qa/SKILL.md +19 -19
- package/skills/tone-compact/SKILL.md +1 -1
- package/skills/tone-compact/scripts/instruction-toggle.mjs +527 -0
- package/skills/tone-compact/scripts/tone-compact.mjs +46 -394
- package/skills/ultra-chain/SKILL.md +7 -6
- package/skills/update-banker/SKILL.md +2 -1
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* node omc-patch.mjs --revert 패치·고정 되돌리기
|
|
8
8
|
* node omc-patch.mjs --no-update 버전은 그대로 두고 패치만
|
|
9
9
|
* node omc-patch.mjs --root <플러그인 폴더> [--check | --revert]
|
|
10
|
-
* 그 폴더의 hooks/hooks.json 과 scripts/ 만 다룬다(번들
|
|
10
|
+
* 그 폴더의 hooks/hooks.json 과 scripts/ 만 다룬다(번들 사본, 활성 캐시 폴더 하나).
|
|
11
11
|
* installed_plugins.json, 마켓플레이스 origin, 판은 건드리지 않는다.
|
|
12
|
+
* 이름표(.claude-plugin/plugin.json 의 name, 없으면 package.json 의 name)가
|
|
13
|
+
* OMC 가 아니면 멈춘다.
|
|
12
14
|
* node omc-patch.mjs --scan <파일>...
|
|
13
15
|
* 헬퍼 선언의 블록 깊이와 래핑 자리를 JSON 으로 낸다(읽기 전용).
|
|
14
16
|
*
|
|
@@ -16,14 +18,14 @@
|
|
|
16
18
|
* 읽는다. 캐시에 TARGET_VERSION 이 있다고 그것을 활성으로 보지 않는다(--no-update 는 활성 버전을 패치).
|
|
17
19
|
* OMC_PATCH_INSTALLED=<json 경로> 로 읽을 파일을 바꿀 수 있다(시험용, --check 와 함께).
|
|
18
20
|
*
|
|
19
|
-
* OS: Windows / macOS / Linux 공통.
|
|
20
|
-
*
|
|
21
|
+
* OS: Windows / macOS / Linux 공통. 런처(apply.cmd, apply.sh)와 배경 문서(README.md)는
|
|
22
|
+
* 킷 배포 폴더에만 있다. 이 파일만 옮겨 쓰는 곳(예: 스킬 폴더)에는 없다.
|
|
21
23
|
*/
|
|
22
24
|
import {
|
|
23
25
|
readFileSync, writeFileSync, writeSync, existsSync, copyFileSync, readdirSync,
|
|
24
|
-
unlinkSync, mkdirSync, rmSync, symlinkSync,
|
|
26
|
+
unlinkSync, mkdirSync, rmSync, symlinkSync, lstatSync, realpathSync,
|
|
25
27
|
} from "node:fs";
|
|
26
|
-
import { join, dirname, resolve } from "node:path";
|
|
28
|
+
import { join, dirname, resolve, sep as PATH_SEP, delimiter as PATH_LIST_SEP } from "node:path";
|
|
27
29
|
import { homedir, hostname } from "node:os";
|
|
28
30
|
import { execFileSync } from "node:child_process";
|
|
29
31
|
|
|
@@ -47,7 +49,8 @@ const argv = new Set(args);
|
|
|
47
49
|
const MODE = argv.has("--revert") ? "revert" : argv.has("--check") ? "check" : "apply";
|
|
48
50
|
const NO_UPDATE = argv.has("--no-update");
|
|
49
51
|
// --root <플러그인 폴더>: 이 PC 의 설치 상태와 상관없이 그 폴더 하나만 고친다. 번들 사본처럼
|
|
50
|
-
// 판 폴더도 installed_plugins.json 도 없는
|
|
52
|
+
// 판 폴더도 installed_plugins.json 도 없는 트리나, 활성 캐시 폴더 하나만 고칠 때 쓴다.
|
|
53
|
+
// 값이 없거나 다음 플래그면 멈춘다.
|
|
51
54
|
const ROOT_AT = args.indexOf("--root");
|
|
52
55
|
const ROOT_ARG = ROOT_AT >= 0 ? args[ROOT_AT + 1] : null;
|
|
53
56
|
const ROOT = ROOT_ARG && !ROOT_ARG.startsWith("--") ? resolve(ROOT_ARG) : null;
|
|
@@ -113,19 +116,44 @@ const installedVersions = () =>
|
|
|
113
116
|
// installPath 마지막 구간 → 옛 규칙(TARGET 이 캐시에 있으면 TARGET, 아니면 최신) 순으로 본다.
|
|
114
117
|
function activeVersion() {
|
|
115
118
|
const versions = installedVersions();
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (e) {
|
|
119
|
-
if (typeof e.version === "string" && /^\d+\.\d+\.\d+$/.test(e.version) && versions.includes(e.version)) return { v: e.version, src: "installed_plugins.json" };
|
|
120
|
-
const last = typeof e.installPath === "string" ? e.installPath.split(/[\\/]+/).filter(Boolean).pop() : null;
|
|
121
|
-
if (last && versions.includes(last)) return { v: last, src: "installed_plugins.json installPath" };
|
|
122
|
-
}
|
|
123
|
-
} catch { /* 읽기·파싱 실패는 옛 규칙으로 */ }
|
|
119
|
+
const found = entryVersion(installedEntry(), versions);
|
|
120
|
+
if (found) return found;
|
|
124
121
|
return { v: versions.includes(TARGET_VERSION) ? TARGET_VERSION : versions[0] || TARGET_VERSION, src: "추정: installed_plugins.json 에서 확인 못 함" };
|
|
125
122
|
}
|
|
126
123
|
|
|
124
|
+
// installed_plugins.json 의 omc 항목. 읽기·파싱 실패는 null(옛 규칙으로).
|
|
125
|
+
function installedEntry() {
|
|
126
|
+
try { return JSON.parse(readFileSync(INSTALLED, "utf8")).plugins?.[PLUGIN_KEY]?.[0] || null; }
|
|
127
|
+
catch { return null; }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 항목의 version 필드, 없으면 installPath 마지막 구간. 캐시에 있는 판이어야 한다.
|
|
131
|
+
function entryVersion(e, versions) {
|
|
132
|
+
if (!e) return null;
|
|
133
|
+
if (typeof e.version === "string" && /^\d+\.\d+\.\d+$/.test(e.version) && versions.includes(e.version)) return { v: e.version, src: "installed_plugins.json" };
|
|
134
|
+
const last = typeof e.installPath === "string" ? e.installPath.split(/[\\/]+/).filter(Boolean).pop() : null;
|
|
135
|
+
if (last && versions.includes(last)) return { v: last, src: "installed_plugins.json installPath" };
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// git 이 일할 저장소를 cwd 와 상관없이 정하는 변수. git 훅처럼 GIT_DIR 을 내보낸 셸에서 이 도구를 부르면
|
|
140
|
+
// 마켓플레이스가 아니라 그 저장소의 origin 을 고정했다(/tmp 에서 재현). git 이 다른 저장소로 옮겨 갈 때
|
|
141
|
+
// 지우는 목록(`git rev-parse --local-env-vars`, git 2.43)과 같다. Windows 는 변수 이름의 대소문자를 가리지 않는다.
|
|
142
|
+
const GIT_REPO_VARS = new Set(["GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_CONFIG", "GIT_CONFIG_PARAMETERS",
|
|
143
|
+
"GIT_CONFIG_COUNT", "GIT_OBJECT_DIRECTORY", "GIT_DIR", "GIT_WORK_TREE", "GIT_IMPLICIT_WORK_TREE", "GIT_GRAFT_FILE",
|
|
144
|
+
"GIT_INDEX_FILE", "GIT_NO_REPLACE_OBJECTS", "GIT_REPLACE_REF_BASE", "GIT_PREFIX", "GIT_SHALLOW_FILE", "GIT_COMMON_DIR"]);
|
|
145
|
+
// 마켓플레이스의 .git 이 깨졌으면(중단된 clone) git 은 위로 올라가 다음 저장소(dotfiles 로 관리하는 ~/.claude 등)를
|
|
146
|
+
// 고른다. 바로 위 폴더를 천장(GIT_CEILING_DIRECTORIES)으로 두어 마켓플레이스 밖으로 나가지 못하게 한다.
|
|
147
|
+
// 마켓플레이스가 링크면 실제 경로 쪽 위 폴더도 막는다. 셸에 있던 천장 값은 대소문자와 상관없이 이 값으로 바꾼다.
|
|
148
|
+
const gitEnv = () => {
|
|
149
|
+
const drop = (k) => GIT_REPO_VARS.has(k.toUpperCase()) || k.toUpperCase() === "GIT_CEILING_DIRECTORIES";
|
|
150
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !drop(k)));
|
|
151
|
+
env.GIT_CEILING_DIRECTORIES = [...new Set([dirname(MARKET), dirname(realOr(MARKET))])].join(PATH_LIST_SEP);
|
|
152
|
+
return env;
|
|
153
|
+
};
|
|
154
|
+
|
|
127
155
|
function git(args, cwd = MARKET) {
|
|
128
|
-
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
156
|
+
return execFileSync("git", args, { cwd, env: gitEnv(), encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
129
157
|
}
|
|
130
158
|
|
|
131
159
|
// ── 1. 업데이트 ──────────────────────────────────────────────────────────────
|
|
@@ -133,6 +161,19 @@ function ensureVersion() {
|
|
|
133
161
|
const have = installedVersions();
|
|
134
162
|
if (have.includes(TARGET_VERSION)) { ok(`${TARGET_VERSION} 설치되어 있음`); return true; }
|
|
135
163
|
if (MODE === "check") { bad(`${TARGET_VERSION} 미설치 (설치된 버전: ${have.join(", ") || "없음"})`); return false; }
|
|
164
|
+
if (!canInstallTarget()) return false;
|
|
165
|
+
|
|
166
|
+
log(` ${TARGET_VERSION} 설치 중...`);
|
|
167
|
+
const sha = targetSha();
|
|
168
|
+
if (sha === null) return false;
|
|
169
|
+
const dest = join(CACHE, TARGET_VERSION);
|
|
170
|
+
unpackTarget(dest);
|
|
171
|
+
linkNodeModules(dest, have);
|
|
172
|
+
return switchActive(sha, have);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 마켓플레이스가 제 git 저장소이고 git 과 tar 가 있어야 TARGET_VERSION 을 꺼낼 수 있다.
|
|
176
|
+
function canInstallTarget() {
|
|
136
177
|
if (!existsSync(MARKET)) { bad(`마켓플레이스 없음: ${MARKET}\n Claude Code 에서 marketplace 를 먼저 추가하십시오: ${REPO_URL}`); return false; }
|
|
137
178
|
if (!existsSync(join(MARKET, ".git"))) { bad(`마켓플레이스 폴더가 git 저장소가 아님: ${MARKET}`); return false; }
|
|
138
179
|
|
|
@@ -143,23 +184,28 @@ function ensureVersion() {
|
|
|
143
184
|
try { execFileSync(cmd, ["--version"], { stdio: "ignore" }); }
|
|
144
185
|
catch { bad(`${cmd} 를 찾을 수 없습니다 (${why} 필요). 설치한 뒤 다시 실행하거나, --no-update 로 패치만 적용하십시오.`); return false; }
|
|
145
186
|
}
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
146
189
|
|
|
147
|
-
|
|
190
|
+
// 태그 v<TARGET_VERSION> 의 커밋. 못 찾으면 null.
|
|
191
|
+
function targetSha() {
|
|
148
192
|
try { git(["fetch", "--tags", "--quiet", "origin"]); } catch { /* 오프라인이어도 로컬 태그가 있으면 진행 */ }
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
193
|
+
try { return git(["rev-parse", `v${TARGET_VERSION}^{}`]); }
|
|
194
|
+
catch { bad(`태그 v${TARGET_VERSION} 를 마켓플레이스에서 찾을 수 없음`); return null; }
|
|
195
|
+
}
|
|
152
196
|
|
|
153
|
-
|
|
197
|
+
// git archive | tar 로 소스를 편다. 저장소에 dist/ 가 포함되어 빌드는 불필요하다.
|
|
198
|
+
function unpackTarget(dest) {
|
|
154
199
|
if (existsSync(dest)) rmSync(dest, { recursive: true, force: true });
|
|
155
200
|
mkdirSync(dest, { recursive: true });
|
|
156
|
-
|
|
157
|
-
execFileSync("git", ["-C", MARKET, "archive", `v${TARGET_VERSION}`, "-o", join(dest, "_src.tar")], { stdio: "ignore" });
|
|
201
|
+
git(["archive", `v${TARGET_VERSION}`, "-o", join(dest, "_src.tar")]);
|
|
158
202
|
execFileSync("tar", ["-xf", join(dest, "_src.tar"), "-C", dest], { stdio: "ignore" });
|
|
159
203
|
unlinkSync(join(dest, "_src.tar"));
|
|
204
|
+
}
|
|
160
205
|
|
|
161
|
-
|
|
162
|
-
|
|
206
|
+
// node_modules 는 복사하지 않고 직전 버전 것을 링크한다. Windows 는 junction
|
|
207
|
+
// (관리자 권한 불필요), POSIX 는 type 인자가 무시되고 디렉터리 symlink 가 된다.
|
|
208
|
+
function linkNodeModules(dest, have) {
|
|
163
209
|
const prev = have.find((v) => v !== TARGET_VERSION);
|
|
164
210
|
const nm = join(dest, "node_modules");
|
|
165
211
|
if (!existsSync(nm) && prev && existsSync(join(CACHE, prev, "node_modules"))) {
|
|
@@ -168,8 +214,10 @@ function ensureVersion() {
|
|
|
168
214
|
} else if (!existsSync(nm)) {
|
|
169
215
|
bad(`node_modules 없음. ${dest} 에서 \`npm install --omit=dev\` 를 실행하십시오`);
|
|
170
216
|
}
|
|
217
|
+
}
|
|
171
218
|
|
|
172
|
-
|
|
219
|
+
// 활성 버전 전환
|
|
220
|
+
function switchActive(sha, have) {
|
|
173
221
|
const j = JSON.parse(readFileSync(INSTALLED, "utf8"));
|
|
174
222
|
const e = j.plugins?.[PLUGIN_KEY]?.[0];
|
|
175
223
|
if (!e) { bad(`installed_plugins.json 에 ${PLUGIN_KEY} 항목이 없음 — 수동 확인 필요`); return false; }
|
|
@@ -210,109 +258,160 @@ const REGEX_PREV_KW = new Set(["return", "typeof", "instanceof", "in", "of", "ne
|
|
|
210
258
|
// probe(i, fnStack) 를 주면 코드 글자(문자열·주석·정규식·중괄호가 아닌 자리)마다 부른다.
|
|
211
259
|
// --scan 이 같은 토크나이저로 헬퍼 선언의 깊이를 재는 데 쓴다. 패치 경로는 probe 없이 부른다.
|
|
212
260
|
function scanTopLevelAwaitImports(src, probe) {
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
let e = j;
|
|
220
|
-
while (j >= 0 && ID_CH.test(src[j])) j--;
|
|
221
|
-
return src.slice(j + 1, e + 1);
|
|
222
|
-
};
|
|
223
|
-
const backSkip = (j) => {
|
|
224
|
-
for (;;) {
|
|
225
|
-
while (j >= 0 && /\s/.test(src[j])) j--;
|
|
226
|
-
if (j >= 1 && src[j] === "/" && src[j - 1] === "*") { // */ 로 끝나는 주석
|
|
227
|
-
const s = src.lastIndexOf("/*", j - 1);
|
|
228
|
-
if (s < 0) return -1;
|
|
229
|
-
j = s - 1; continue;
|
|
230
|
-
}
|
|
231
|
-
return j;
|
|
232
|
-
}
|
|
233
|
-
};
|
|
234
|
-
const skipString = (q) => {
|
|
235
|
-
i++;
|
|
236
|
-
while (i < n) {
|
|
237
|
-
const c = src[i];
|
|
238
|
-
if (c === "\\") { i += 2; continue; }
|
|
239
|
-
if (c === q) { i++; return; }
|
|
240
|
-
if (q === "`" && c === "$" && src[i + 1] === "{") {
|
|
241
|
-
i += 2;
|
|
242
|
-
let d = 1;
|
|
243
|
-
while (i < n && d > 0) {
|
|
244
|
-
const k = src[i];
|
|
245
|
-
if (k === "\\") { i += 2; continue; }
|
|
246
|
-
if (k === "'" || k === '"' || k === "`") { skipString(k); continue; }
|
|
247
|
-
if (k === "{") d++;
|
|
248
|
-
else if (k === "}") d--;
|
|
249
|
-
i++;
|
|
250
|
-
}
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
i++;
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
// 이 중괄호가 «함수 몸통» 을 여는가
|
|
257
|
-
const isFunctionBrace = (at) => {
|
|
258
|
-
let j = backSkip(at - 1);
|
|
259
|
-
if (j < 0) return false;
|
|
260
|
-
if (src[j] === ">" && src[j - 1] === "=") return true; // () => {
|
|
261
|
-
if (src[j] !== ")") return false; // class/객체/블록
|
|
262
|
-
let d = 0;
|
|
263
|
-
for (; j >= 0; j--) { // 짝 맞는 ( 찾기
|
|
264
|
-
if (src[j] === ")") d++;
|
|
265
|
-
else if (src[j] === "(") { if (--d === 0) break; }
|
|
266
|
-
}
|
|
267
|
-
if (j < 0) return false;
|
|
268
|
-
const k = backSkip(j - 1);
|
|
269
|
-
if (k < 0 || !ID_CH.test(src[k])) return false;
|
|
270
|
-
return !BLOCK_KW.has(wordEndingAt(k)); // if/for/while/catch… 은 블록
|
|
271
|
-
};
|
|
261
|
+
const s = { src, n: src.length, i: 0, prev: "", prevIdx: -1, fnStack: [], hits: [] };
|
|
262
|
+
while (s.i < s.n) {
|
|
263
|
+
if (!skipNonCode(s)) stepCode(s, probe);
|
|
264
|
+
}
|
|
265
|
+
return s.hits;
|
|
266
|
+
}
|
|
272
267
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (k === "\\") { i += 2; continue; }
|
|
288
|
-
if (k === "[") inClass = true;
|
|
289
|
-
else if (k === "]") inClass = false;
|
|
290
|
-
else if (k === "/" && !inClass) { i++; break; }
|
|
291
|
-
else if (k === "\n") break;
|
|
292
|
-
i++;
|
|
293
|
-
}
|
|
294
|
-
while (i < n && ID_CH.test(src[i])) i++; // 플래그
|
|
295
|
-
prev = "/"; prevIdx = i - 1; continue;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
if (c === "{") { fnStack.push(isFunctionBrace(i)); i++; prev = "{"; prevIdx = i - 1; continue; }
|
|
299
|
-
if (c === "}") { fnStack.pop(); i++; prev = "}"; prevIdx = i - 1; continue; }
|
|
300
|
-
if (c === "a" && !(i > 0 && ID_CH.test(src[i - 1])) && /^await\s+import\s*\(/.test(src.slice(i, i + 40))) {
|
|
301
|
-
if (!fnStack.some(Boolean)) hits.push(i);
|
|
302
|
-
}
|
|
303
|
-
if (probe) probe(i, fnStack);
|
|
304
|
-
prev = c; prevIdx = i; i++;
|
|
268
|
+
const wordEndingAt = (src, j) => {
|
|
269
|
+
const e = j;
|
|
270
|
+
while (j >= 0 && ID_CH.test(src[j])) j--;
|
|
271
|
+
return src.slice(j + 1, e + 1);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// j 에서 거꾸로 공백과 /* */ 주석을 건너뛴 자리. 주석의 시작이 없으면 -1.
|
|
275
|
+
function backSkip(src, j) {
|
|
276
|
+
for (;;) {
|
|
277
|
+
while (j >= 0 && /\s/.test(src[j])) j--;
|
|
278
|
+
if (!(j >= 1 && src[j] === "/" && src[j - 1] === "*")) return j; // */ 로 끝나는 주석이 아님
|
|
279
|
+
const s = src.lastIndexOf("/*", j - 1);
|
|
280
|
+
if (s < 0) return -1;
|
|
281
|
+
j = s - 1;
|
|
305
282
|
}
|
|
306
|
-
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 아래 함수들은 토크나이저 상태 s 를 함께 쓴다. src 와 n(길이), i(지금 자리), prev 와
|
|
286
|
+
// prevIdx(직전 코드 글자와 그 자리), fnStack(열린 중괄호마다 함수 몸통이면 true), hits.
|
|
287
|
+
|
|
288
|
+
// 주석, 문자열, 공백, 정규식이면 건너뛰고 true. 코드 글자면 그대로 두고 false.
|
|
289
|
+
function skipNonCode(s) {
|
|
290
|
+
const c = s.src[s.i];
|
|
291
|
+
if (skipComment(s)) return true;
|
|
292
|
+
if (c === "'" || c === '"' || c === "`") { skipString(s, c); s.prev = c; return true; }
|
|
293
|
+
if (/\s/.test(c)) { s.i++; return true; }
|
|
294
|
+
if (c === "/" && startsRegex(s)) { skipRegex(s); return true; }
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function skipComment(s) {
|
|
299
|
+
const { src, n } = s;
|
|
300
|
+
if (src[s.i] !== "/") return false;
|
|
301
|
+
if (src[s.i + 1] === "/") {
|
|
302
|
+
while (s.i < n && src[s.i] !== "\n") s.i++;
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
if (src[s.i + 1] !== "*") return false;
|
|
306
|
+
const e = src.indexOf("*/", s.i + 2);
|
|
307
|
+
s.i = e < 0 ? n : e + 2;
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// s.i 는 여는 따옴표. 닫는 따옴표 다음으로 옮긴다. 템플릿의 ${ } 는 skipTemplateExpr 가 맡는다.
|
|
312
|
+
function skipString(s, q) {
|
|
313
|
+
const { src, n } = s;
|
|
314
|
+
s.i++;
|
|
315
|
+
while (s.i < n) {
|
|
316
|
+
const c = src[s.i];
|
|
317
|
+
if (c === "\\") { s.i += 2; continue; }
|
|
318
|
+
if (c === q) { s.i++; return; }
|
|
319
|
+
if (q === "`" && c === "$" && src[s.i + 1] === "{") { skipTemplateExpr(s); continue; }
|
|
320
|
+
s.i++;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// s.i 는 ${ 의 $. 짝 맞는 } 다음으로 옮긴다. 그 안의 문자열은 다시 skipString 으로 건너뛴다.
|
|
325
|
+
function skipTemplateExpr(s) {
|
|
326
|
+
const { src, n } = s;
|
|
327
|
+
s.i += 2;
|
|
328
|
+
let d = 1;
|
|
329
|
+
while (s.i < n && d > 0) {
|
|
330
|
+
const k = src[s.i];
|
|
331
|
+
if (k === "\\") { s.i += 2; continue; }
|
|
332
|
+
if (k === "'" || k === '"' || k === "`") { skipString(s, k); continue; }
|
|
333
|
+
if (k === "{") d++;
|
|
334
|
+
else if (k === "}") d--;
|
|
335
|
+
s.i++;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 여기의 / 가 나눗셈이 아니라 정규식을 여는가. 직전 코드 글자나 직전 낱말로 정한다.
|
|
340
|
+
function startsRegex(s) {
|
|
341
|
+
const { src, prev, prevIdx } = s;
|
|
342
|
+
return prev === "" || REGEX_PREV_CH.has(prev)
|
|
343
|
+
|| (prevIdx >= 0 && ID_CH.test(prev) && REGEX_PREV_KW.has(wordEndingAt(src, prevIdx)));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// s.i 는 정규식을 여는 /. 닫는 / 와 플래그 다음으로 옮긴다. 줄이 끝나면 거기서 멈춘다.
|
|
347
|
+
function skipRegex(s) {
|
|
348
|
+
const { src, n } = s;
|
|
349
|
+
s.i++;
|
|
350
|
+
let inClass = false;
|
|
351
|
+
while (s.i < n) {
|
|
352
|
+
const k = src[s.i];
|
|
353
|
+
if (k === "\\") { s.i += 2; continue; }
|
|
354
|
+
if (k === "[") inClass = true;
|
|
355
|
+
else if (k === "]") inClass = false;
|
|
356
|
+
else if (k === "/" && !inClass) { s.i++; break; }
|
|
357
|
+
else if (k === "\n") break;
|
|
358
|
+
s.i++;
|
|
359
|
+
}
|
|
360
|
+
skipWord(s); // 플래그
|
|
361
|
+
s.prev = "/"; s.prevIdx = s.i - 1;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function skipWord(s) {
|
|
365
|
+
while (s.i < s.n && ID_CH.test(s.src[s.i])) s.i++;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 코드 글자 하나. 중괄호면 함수 스택을 바꾸고, 함수 밖의 `await import(` 면 hits 에 넣는다.
|
|
369
|
+
function stepCode(s, probe) {
|
|
370
|
+
const { src } = s;
|
|
371
|
+
const c = src[s.i];
|
|
372
|
+
if (c === "{" || c === "}") {
|
|
373
|
+
if (c === "{") s.fnStack.push(isFunctionBrace(src, s.i));
|
|
374
|
+
else s.fnStack.pop();
|
|
375
|
+
s.i++; s.prev = c; s.prevIdx = s.i - 1;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (c === "a" && isAwaitImportAt(src, s.i) && !s.fnStack.some(Boolean)) s.hits.push(s.i);
|
|
379
|
+
if (probe) probe(s.i, s.fnStack);
|
|
380
|
+
s.prev = c; s.prevIdx = s.i; s.i++;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const isAwaitImportAt = (src, i) => !(i > 0 && ID_CH.test(src[i - 1])) && /^await\s+import\s*\(/.test(src.slice(i, i + 40));
|
|
384
|
+
|
|
385
|
+
// 이 중괄호가 «함수 몸통» 을 여는가
|
|
386
|
+
function isFunctionBrace(src, at) {
|
|
387
|
+
let j = backSkip(src, at - 1);
|
|
388
|
+
if (j < 0) return false;
|
|
389
|
+
if (src[j] === ">" && src[j - 1] === "=") return true; // () => {
|
|
390
|
+
if (src[j] !== ")") return false; // class/객체/블록
|
|
391
|
+
j = openParenOf(src, j); // 짝 맞는 ( 찾기
|
|
392
|
+
if (j < 0) return false;
|
|
393
|
+
const k = backSkip(src, j - 1);
|
|
394
|
+
if (k < 0 || !ID_CH.test(src[k])) return false;
|
|
395
|
+
return !BLOCK_KW.has(wordEndingAt(src, k)); // if/for/while/catch… 은 블록
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// j 의 ) 와 짝인 ( 의 자리. 없으면 -1.
|
|
399
|
+
function openParenOf(src, j) {
|
|
400
|
+
let d = 0;
|
|
401
|
+
for (; j >= 0; j--) {
|
|
402
|
+
if (src[j] === ")") d++;
|
|
403
|
+
else if (src[j] === "(" && --d === 0) return j;
|
|
404
|
+
}
|
|
405
|
+
return -1;
|
|
307
406
|
}
|
|
308
407
|
|
|
309
408
|
// 자리별 상태. 이미 손으로 Promise.race 를 두른 자리는 건드리지 않는다(4.15.7 에 그런 것이 둘 있다).
|
|
310
|
-
// 이미 감싼 파일이라도
|
|
409
|
+
// 이미 감싼 파일이라도 헬퍼가 쓰일 수 없는 자리면(옛 판의 자리) «패치됨» 이 아니라 «misplaced» 다.
|
|
311
410
|
// 그 상태를 «패치됨» 으로 넘기면 --check 가 결함 트리에 «모두 적용» 을 낸다(2026-10-12 실측).
|
|
312
411
|
function siteInfo(src) {
|
|
313
412
|
if (src.includes("__omcRaceImport")) {
|
|
314
|
-
const
|
|
315
|
-
return
|
|
413
|
+
const why = helperDefect(scanFacts(src));
|
|
414
|
+
return why ? { state: "misplaced", sites: [], why } : { state: "patched", sites: [] };
|
|
316
415
|
}
|
|
317
416
|
const all = scanTopLevelAwaitImports(src);
|
|
318
417
|
if (!all.length) return { state: "n/a", sites: [] };
|
|
@@ -321,20 +420,26 @@ function siteInfo(src) {
|
|
|
321
420
|
return { state: "needs", sites: open };
|
|
322
421
|
}
|
|
323
422
|
|
|
423
|
+
// 헬퍼가 쓰일 수 없는 자리면 그 이유를, 제자리면 null 을 낸다.
|
|
424
|
+
// - 선언이 블록 안(깊이 1 이상): 다른 블록의 자리에서 ReferenceError(옛 킷이 남긴 자리)
|
|
425
|
+
// - 선언을 못 찾음: 이름만으로는 «패치됨» 이라 할 근거가 없다. 앞쪽 글을 토크나이저가 잘못
|
|
426
|
+
// 읽어 선언이 문자열 안으로 보였을 수도 있다
|
|
427
|
+
// - 최상위 사용이 선언보다 앞: 그 자리가 먼저 돌아 const, let 은 TDZ 의 ReferenceError, var 는
|
|
428
|
+
// undefined 호출. function 선언은 끌어올려져 앞에서 불러도 된다. 순서는 줄이 아니라 글자 자리로 본다
|
|
429
|
+
function helperDefect({ helperDecls, wrapped }) {
|
|
430
|
+
const inBlock = helperDecls.find((d) => d.depth !== 0);
|
|
431
|
+
if (inBlock) return `헬퍼 선언 :${inBlock.line} 이 블록 안, 깊이 ${inBlock.depth}`;
|
|
432
|
+
if (!helperDecls.length) return "헬퍼 선언을 찾지 못함";
|
|
433
|
+
const decl = helperDecls[0];
|
|
434
|
+
const first = wrapped.find((w) => !w.inFunction);
|
|
435
|
+
if (first && first.at < decl.at && decl.kind !== "function") return `헬퍼 선언 :${decl.line} 이 첫 사용 :${first.line} 보다 뒤`;
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
|
|
324
439
|
function transform(src) {
|
|
325
440
|
const info = siteInfo(src);
|
|
326
441
|
if (info.state !== "needs") return null;
|
|
327
|
-
const edits =
|
|
328
|
-
for (const start of info.sites) {
|
|
329
|
-
const open = src.indexOf("(", src.indexOf("import", start));
|
|
330
|
-
let depth = 0, end = -1;
|
|
331
|
-
for (let i = open; i < src.length; i++) {
|
|
332
|
-
const c = src[i];
|
|
333
|
-
if (c === "(") depth++;
|
|
334
|
-
else if (c === ")" && --depth === 0) { end = i; break; }
|
|
335
|
-
}
|
|
336
|
-
if (end !== -1) edits.push({ start, end });
|
|
337
|
-
}
|
|
442
|
+
const edits = importSpans(src, info.sites);
|
|
338
443
|
if (!edits.length) return null;
|
|
339
444
|
let out = src;
|
|
340
445
|
for (const e of edits.reverse()) {
|
|
@@ -351,6 +456,27 @@ function transform(src) {
|
|
|
351
456
|
return out.slice(0, ls) + HELPER + out.slice(ls);
|
|
352
457
|
}
|
|
353
458
|
|
|
459
|
+
// 감쌀 자리마다 `await` 에서 import( 의 짝 맞는 ) 까지.
|
|
460
|
+
function importSpans(src, sites) {
|
|
461
|
+
const edits = [];
|
|
462
|
+
for (const start of sites) {
|
|
463
|
+
const end = closingParenOf(src, src.indexOf("(", src.indexOf("import", start)));
|
|
464
|
+
if (end !== -1) edits.push({ start, end });
|
|
465
|
+
}
|
|
466
|
+
return edits;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// open 의 ( 와 짝인 ) 의 자리. 없으면 -1.
|
|
470
|
+
function closingParenOf(src, open) {
|
|
471
|
+
let depth = 0;
|
|
472
|
+
for (let i = open; i < src.length; i++) {
|
|
473
|
+
const c = src[i];
|
|
474
|
+
if (c === "(") depth++;
|
|
475
|
+
else if (c === ")" && --depth === 0) return i;
|
|
476
|
+
}
|
|
477
|
+
return -1;
|
|
478
|
+
}
|
|
479
|
+
|
|
354
480
|
// ── 읽기 전용 진단 (--scan) ──────────────────────────────────────────────────
|
|
355
481
|
// 패치가 «제자리» 에 앉았는지를 위 토크나이저 하나로 잰다. 시험이 토크나이저를 따로 가지면
|
|
356
482
|
// 패치가 고르는 자리와 시험이 재는 자리가 서로 다른 규칙을 따르게 된다.
|
|
@@ -367,12 +493,20 @@ function scanFacts(src) {
|
|
|
367
493
|
if (ch !== "c" && ch !== "l" && ch !== "v" && ch !== "f" && ch !== "a") return;
|
|
368
494
|
if (i > 0 && ID_CH.test(src[i - 1])) return;
|
|
369
495
|
const head = src.slice(i, i + 48);
|
|
370
|
-
|
|
371
|
-
|
|
496
|
+
const decl = /^(const|let|var|function)\s+__omcRaceImport\b/.exec(head);
|
|
497
|
+
if (decl) helperDecls.push({ line: lineOf(i), depth: fnStack.length, at: i, kind: decl[1] });
|
|
498
|
+
else if (/^await\s+__omcRaceImport\s*\(/.test(head)) wrapped.push({ line: lineOf(i), inFunction: fnStack.some(Boolean), at: i });
|
|
372
499
|
}).map(lineOf);
|
|
373
500
|
return { helperDecls, wrapped, unwrapped };
|
|
374
501
|
}
|
|
375
502
|
|
|
503
|
+
// --scan 이 내는 칸. at 과 kind 는 판정용이라 내보내지 않는다(출력 형식은 그대로 둔다).
|
|
504
|
+
const publicFacts = ({ helperDecls, wrapped, unwrapped }) => ({
|
|
505
|
+
helperDecls: helperDecls.map(({ line, depth }) => ({ line, depth })),
|
|
506
|
+
wrapped: wrapped.map(({ line, inFunction }) => ({ line, inFunction })),
|
|
507
|
+
unwrapped,
|
|
508
|
+
});
|
|
509
|
+
|
|
376
510
|
function syntaxError(file) {
|
|
377
511
|
try { execFileSync(process.execPath, ["--check", file], { stdio: ["ignore", "ignore", "pipe"] }); return null; }
|
|
378
512
|
catch (e) {
|
|
@@ -387,8 +521,7 @@ const looksHandPatched = (src) =>
|
|
|
387
521
|
!src.includes("__omcRaceImport") && /Promise\.race\s*\(\s*\[?[\s\S]{0,120}?\bimport\s*\(/.test(src);
|
|
388
522
|
|
|
389
523
|
function patchHooks(root) {
|
|
390
|
-
|
|
391
|
-
const foreign = [];
|
|
524
|
+
const r = { done: 0, skip: 0, fail: 0, need: 0, misplaced: 0, foreign: [], orphan: [], failed: [] };
|
|
392
525
|
// revert 는 scripts/ 전체를 훑는다. hooks.json 기반 목록은 두 번 새는데, 실측으로
|
|
393
526
|
// 4.15.7 이 둘 다 겪었다 — PostToolUse 를 지우면 그 스크립트가 목록에서 빠지고(그래서
|
|
394
527
|
// .omcbak 이 있어도 안 돌아온다), 손으로 고친 파일은 .omcbak 조차 없어 아예 안 보인다.
|
|
@@ -397,64 +530,138 @@ function patchHooks(root) {
|
|
|
397
530
|
: hookNames(root);
|
|
398
531
|
for (const n of names) {
|
|
399
532
|
const f = join(root, "scripts", `${n}.mjs`);
|
|
400
|
-
const bak = `${f}.omcbak`;
|
|
401
533
|
if (!existsSync(f)) continue;
|
|
534
|
+
if (MODE === "revert") revertScript(root, n, f, r);
|
|
535
|
+
else checkOrPatchScript(root, n, f, r);
|
|
536
|
+
}
|
|
537
|
+
return r;
|
|
538
|
+
}
|
|
402
539
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if (looksHandPatched(readFileSync(f, "utf8"))) foreign.push(n);
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
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; }
|
|
420
|
-
if (MODE === "check") { bad(`패치 필요: ${n}`); need++; continue; }
|
|
421
|
-
|
|
422
|
-
const out = transform(readFileSync(f, "utf8"));
|
|
423
|
-
if (!out) { bad(`변환 실패: ${n}`); fail++; continue; }
|
|
424
|
-
if (!existsSync(bak)) copyFileSync(f, bak);
|
|
425
|
-
writeFileSync(f, out, "utf8");
|
|
426
|
-
const err = syntaxError(f);
|
|
427
|
-
if (err) { copyFileSync(bak, f); bad(`문법오류로 롤백: ${n}\n ${err}`); fail++; continue; }
|
|
428
|
-
done++;
|
|
540
|
+
function revertScript(root, n, f, r) {
|
|
541
|
+
const bak = `${f}.omcbak`;
|
|
542
|
+
if (existsSync(bak)) {
|
|
543
|
+
const why = whyNotWrite(root, f, bak);
|
|
544
|
+
if (why) { bad(`되돌리지 않음: ${shown(n)} (${why})`); r.fail++; r.failed.push(f); return; }
|
|
545
|
+
copyFileSync(bak, f); unlinkSync(bak); log(` 되돌림 ${shown(n)}`); r.done++; return;
|
|
429
546
|
}
|
|
430
|
-
|
|
547
|
+
// 백업이 없는데 이미 감싸여 있으면 되돌릴 원본이 디스크에 없다. 조용히 넘기지 않고 이름을 댄다.
|
|
548
|
+
// - 우리 헬퍼가 있으면: 이 도구나 옛 킷이 고쳤는데 백업이 사라진 파일(복사해 온 폴더 등)
|
|
549
|
+
// - 손 패치: `await import(` 를 `await Promise.race([import(` 로 «바꿔치기» 해서 await import
|
|
550
|
+
// 패턴 자체가 사라진다. siteInfo 로는 잡히지 않아 원문에서 직접 찾는다(실측: 4.15.7 의 두 파일)
|
|
551
|
+
const src = readFileSync(f, "utf8");
|
|
552
|
+
if (src.includes("__omcRaceImport")) r.orphan.push(n);
|
|
553
|
+
else if (looksHandPatched(src)) r.foreign.push(n);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function checkOrPatchScript(root, n, f, r) {
|
|
557
|
+
const info = siteInfo(readFileSync(f, "utf8"));
|
|
558
|
+
// 헬퍼 자리 결함은 check 와 apply 모두 이름을 댄다. 이미 감싼 파일이라 transform 으로는
|
|
559
|
+
// 고칠 수 없으므로 apply 도 건드리지 않고, 되돌린 뒤 다시 적용하라고 안내한다(misplacedHint).
|
|
560
|
+
if (info.state === "misplaced") { bad(`헬퍼 자리 결함: ${n} (${info.why})`); r.misplaced++; return; }
|
|
561
|
+
if (info.state !== "needs") { r.skip++; return; }
|
|
562
|
+
if (MODE === "check") { bad(`패치 필요: ${n}`); r.need++; return; }
|
|
563
|
+
|
|
564
|
+
const bak = `${f}.omcbak`;
|
|
565
|
+
const why = whyNotWrite(root, f, bak);
|
|
566
|
+
if (why) { bad(`쓰지 않음: ${n} (${why})`); r.fail++; return; }
|
|
567
|
+
const out = transform(readFileSync(f, "utf8"));
|
|
568
|
+
if (!out) { bad(`변환 실패: ${n}`); r.fail++; return; }
|
|
569
|
+
if (!existsSync(bak)) copyFileSync(f, bak);
|
|
570
|
+
writeFileSync(f, out, "utf8");
|
|
571
|
+
const err = syntaxError(f);
|
|
572
|
+
if (err) { copyFileSync(bak, f); bad(`문법오류로 롤백: ${n}\n ${err}`); r.fail++; return; }
|
|
573
|
+
r.done++;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// 쓰기 전에 본다. 쓸 파일이 (링크를 따라가서) 플러그인 폴더 밖이거나 백업 자리가 링크면 그 이유를,
|
|
577
|
+
// 아니면 null. 백업 자리가 깨진 링크면 existsSync 는 false 라, copyFileSync 가 링크가 가리키는 곳에 쓴다.
|
|
578
|
+
function whyNotWrite(root, file, bak) {
|
|
579
|
+
if (isLink(bak)) return "백업 자리가 링크";
|
|
580
|
+
if (!insideRoot(root, file)) return "파일이 플러그인 폴더 밖에 있음";
|
|
581
|
+
return null;
|
|
431
582
|
}
|
|
432
583
|
|
|
584
|
+
const isLink = (p) => {
|
|
585
|
+
try { return lstatSync(p).isSymbolicLink(); } catch { return false; }
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// 실제 경로로 p 가 root 안인가. 버전 폴더 자체가 링크(옛 판이 새 판을 가리킴)여도 root 를 같이
|
|
589
|
+
// 풀므로 맞다. Windows 는 경로의 대소문자를 구분하지 않아 소문자로 비교한다.
|
|
590
|
+
function insideRoot(root, p) {
|
|
591
|
+
const fold = (x) => (process.platform === "win32" ? x.toLowerCase() : x);
|
|
592
|
+
try { return fold(realpathSync(p)).startsWith(fold(realpathSync(root)) + PATH_SEP); }
|
|
593
|
+
catch { return false; }
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// 실제 경로. 풀 수 없으면 받은 그대로.
|
|
597
|
+
const realOr = (p) => {
|
|
598
|
+
try { return realpathSync(p); } catch { return p; }
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// 같은 파일인지 셀 때 쓰는 열쇠. Windows 는 경로의 대소문자를 가리지 않아 소문자로 접는다. junction 이 대상을
|
|
602
|
+
// 캐시 경로와 다른 글자로 적어 두면 같은 파일이 다른 철자로 풀리기 때문이다.
|
|
603
|
+
const fileKey = (p) => (process.platform === "win32" ? realOr(p).toLowerCase() : realOr(p));
|
|
604
|
+
|
|
433
605
|
// ── 3. PostToolUse 훅 제거 ───────────────────────────────────────────────────
|
|
434
606
|
function patchHooksJson(root) {
|
|
435
607
|
const hj = join(root, "hooks", "hooks.json");
|
|
436
|
-
const bak = `${hj}.omcbak`;
|
|
437
608
|
if (!existsSync(hj)) return "no-file";
|
|
438
|
-
if (MODE
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
609
|
+
if (MODE !== "check") {
|
|
610
|
+
const why = whyNotWrite(root, hj, `${hj}.omcbak`);
|
|
611
|
+
if (why) { bad(`hooks.json 을 건드리지 않음 (${why})`); return "unsafe"; }
|
|
612
|
+
}
|
|
613
|
+
return MODE === "revert" ? revertHooksJson(root, hj) : removePostToolUse(hj);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// 되돌린 결과. ambiguous 와 linked 는 hooks.json 을 되돌리지 못하고 남긴 것이라 끝 줄이 센다.
|
|
617
|
+
function revertHooksJson(root, hj) {
|
|
618
|
+
const bak = `${hj}.omcbak`;
|
|
619
|
+
if (existsSync(bak)) { copyFileSync(bak, hj); unlinkSync(bak); log(" 되돌림 hooks.json"); return "reverted"; }
|
|
620
|
+
// 이 도구의 백업이 없을 때 PostToolUse 가 그대로면 되돌릴 것이 없다. 옆의 수동 백업으로 덮어쓰면
|
|
621
|
+
// 지금 내용을 잃는다.
|
|
622
|
+
if (hasPostToolUse(hj)) return "no-backup";
|
|
623
|
+
// 손으로 만든 백업이 남아 있을 수 있다(실측: 4.15.7 의 hooks.json.bak-20260906-postoolremove).
|
|
624
|
+
// PostToolUse 를 담고 있는 유효한 JSON 이 딱 하나면 그것이 제거 직전 상태다 — 무엇을 썼는지 밝히고 되돌린다.
|
|
625
|
+
const dir = dirname(hj);
|
|
626
|
+
const { usable, unsafe } = manualBackups(root, dir);
|
|
627
|
+
if (unsafe.length) bad(`hooks.json 수동 백업 후보가 링크이거나 플러그인 폴더 밖이라 쓰지 않음: ${unsafe.map(shown).join(", ")}`);
|
|
628
|
+
if (usable.length === 1) {
|
|
629
|
+
copyFileSync(join(dir, usable[0]), hj);
|
|
630
|
+
log(` 되돌림 hooks.json (수동 백업 사용: ${shown(usable[0])})`);
|
|
631
|
+
return "reverted";
|
|
632
|
+
}
|
|
633
|
+
if (usable.length > 1) { bad(`hooks.json 수동 백업이 여러 개라 자동 복구하지 않음: ${usable.map(shown).join(", ")}`); return "ambiguous"; }
|
|
634
|
+
return unsafe.length ? "linked" : "no-backup";
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// 파일에서 읽은 값(파일 이름, PINNED 의 host)을 출력에 넣을 때 쓴다. 줄바꿈과 제어 문자를 ? 로 바꾸고 길이를 자른다.
|
|
638
|
+
// SKILL 은 [!] 줄을 그대로 보고하라고 하므로, 값이 제 줄을 새로 시작하면 도구가 낸 줄처럼 읽힌다.
|
|
639
|
+
// 줄을 바꾸거나 글자 순서를 바꾸는 문자: C0 과 C1 제어 문자, 폭 없는 공백과 방향 표시, 줄과 문단 구분자,
|
|
640
|
+
// 방향 덮어쓰기와 격리, BOM. oneLine 은 길이를 두고(URL), shown 은 120자로 자른다(이름, host).
|
|
641
|
+
const UNSAFE_CH = /[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\u2066-\u2069\ufeff]/g;
|
|
642
|
+
const oneLine = (s) => String(s).replace(UNSAFE_CH, "?");
|
|
643
|
+
const shown = (s) => oneLine(s).slice(0, 120);
|
|
644
|
+
|
|
645
|
+
// hooks.json 옆의 수동 백업 후보. usable 은 PostToolUse 를 담은 유효한 JSON 이다. 링크로 심은 후보는
|
|
646
|
+
// unsafe 로 따로 두고 읽지도 않는다. 폴더 밖 파일(PostToolUse 키가 있는 settings.json 등)이 hooks.json 이 된다.
|
|
647
|
+
function manualBackups(root, dir) {
|
|
648
|
+
const usable = [], unsafe = [];
|
|
649
|
+
for (const f of readdirSync(dir)) {
|
|
650
|
+
if (!f.startsWith("hooks.json.") || f === "hooks.json.omcbak") continue;
|
|
651
|
+
const p = join(dir, f);
|
|
652
|
+
if (isLink(p) || !insideRoot(root, p)) unsafe.push(f);
|
|
653
|
+
else if (hasPostToolUse(p)) usable.push(f);
|
|
457
654
|
}
|
|
655
|
+
return { usable, unsafe };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const hasPostToolUse = (p) => {
|
|
659
|
+
try { return !!JSON.parse(readFileSync(p, "utf8"))?.hooks?.PostToolUse; }
|
|
660
|
+
catch { return false; }
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
function removePostToolUse(hj) {
|
|
664
|
+
const bak = `${hj}.omcbak`;
|
|
458
665
|
const j = JSON.parse(readFileSync(hj, "utf8"));
|
|
459
666
|
if (!j.hooks?.PostToolUse) return "already";
|
|
460
667
|
const list = j.hooks.PostToolUse.flatMap((g) => (g.hooks || []).map((h) => (h.command || "").match(/([\w-]+)\.mjs/)?.[1] || "?"));
|
|
@@ -472,38 +679,20 @@ function patchHooksJson(root) {
|
|
|
472
679
|
// 올릴 수 있다. 그러면 위 패치가 통째로 덮어써진다. 로컬에서 막을 수 있는
|
|
473
680
|
// 지점은 "마켓플레이스 저장소가 새 커밋을 가져오지 못하게" 하는 것뿐이다.
|
|
474
681
|
// origin remote 의 URL 을 무효 주소로 바꿔 fetch 를 실패시킨다(원래 URL 은
|
|
475
|
-
//
|
|
476
|
-
// 이미 받은 태그로 재설치·검증이 가능하다.
|
|
682
|
+
// `omc-pinned://` 뒤에 그대로 남기고 --revert 가 거기서 복구한다. PINNED 파일은 기록이다).
|
|
683
|
+
// 저장소 자체는 그대로라 이미 받은 태그로 재설치·검증이 가능하다.
|
|
477
684
|
function pinMarketplace() {
|
|
478
685
|
if (!existsSync(MARKET)) return "no-marketplace";
|
|
479
686
|
// 마켓플레이스 폴더가 제 git 저장소가 아니면 git 이 위쪽 저장소(예: dotfiles 로 관리하는
|
|
480
|
-
// ~/.claude)를 찾아 그 origin 을 바꾼다. 제 .git 이 있을 때만 고정하고 되돌린다.
|
|
687
|
+
// ~/.claude)를 찾아 그 origin 을 바꾼다. 제 .git 이 있을 때만 고정하고 되돌린다. .git 이 있어도
|
|
688
|
+
// 깨졌으면 git 이 위로 올라가는데, 그것은 gitEnv 의 천장이 막는다(origin 을 못 읽어 no-remote).
|
|
481
689
|
if (!existsSync(join(MARKET, ".git"))) return "no-git";
|
|
482
690
|
let url;
|
|
483
691
|
try { url = git(["remote", "get-url", "origin"]); } catch { return "no-remote"; }
|
|
484
692
|
|
|
485
|
-
if (MODE === "revert")
|
|
486
|
-
// 원본 URL 은 현재 origin 안에 그대로 들어 있다(`omc-pinned://` + 원본). 파일이 아니라
|
|
487
|
-
// 거기서 복원한다. PINNED 는 배포 폴더에 같이 딸려 오므로 다른 PC 의 값일 수 있고,
|
|
488
|
-
// PINFILE 은 스크립트 위치가 아니라 homedir() 기준이라 그 사실이 눈에 띄지도 않는다.
|
|
489
|
-
const PIN_PREFIX = "omc-pinned://";
|
|
490
|
-
let restored = null;
|
|
491
|
-
if (url.startsWith(PIN_PREFIX)) restored = url.slice(PIN_PREFIX.length);
|
|
492
|
-
if (existsSync(PINFILE)) {
|
|
493
|
-
let saved = {};
|
|
494
|
-
try { saved = JSON.parse(readFileSync(PINFILE, "utf8")); } catch { /* 깨졌으면 무시 */ }
|
|
495
|
-
if (saved.host && saved.host !== hostname()) {
|
|
496
|
-
bad(`PINNED 는 다른 기기(${saved.host})의 기록입니다 — 그 값은 쓰지 않고 현재 origin 에서 복원합니다.`);
|
|
497
|
-
}
|
|
498
|
-
if (!restored && saved.originUrl) restored = saved.originUrl;
|
|
499
|
-
unlinkSync(PINFILE);
|
|
500
|
-
}
|
|
501
|
-
if (!restored) return "not-pinned";
|
|
502
|
-
try { git(["remote", "set-url", "origin", restored]); } catch { /* noop */ }
|
|
503
|
-
return "unpinned";
|
|
504
|
-
}
|
|
693
|
+
if (MODE === "revert") return unpinMarketplace(url);
|
|
505
694
|
if (url.startsWith("omc-pinned://")) return "already";
|
|
506
|
-
if (MODE === "check") { bad(`마켓플레이스 미고정 (origin=${url})`); return "needs"; }
|
|
695
|
+
if (MODE === "check") { bad(`마켓플레이스 미고정 (origin=${oneLine(url)})`); return "needs"; }
|
|
507
696
|
|
|
508
697
|
mkdirSync(dirname(PINFILE), { recursive: true });
|
|
509
698
|
// host 를 남긴다. 이 폴더는 통째로 복사해 배포하므로 PINNED 도 같이 딸려 가는데,
|
|
@@ -513,6 +702,44 @@ function pinMarketplace() {
|
|
|
513
702
|
return "pinned";
|
|
514
703
|
}
|
|
515
704
|
|
|
705
|
+
// 원본 URL 은 현재 origin 안에 그대로 들어 있다(`omc-pinned://` + 원본). 거기서만 복원한다.
|
|
706
|
+
// PINNED 의 값으로 origin 을 쓰지 않는다. PINNED 는 배포 폴더에 같이 딸려 오므로 다른 PC 의 값일 수 있고,
|
|
707
|
+
// host 가 없는 기록은 어느 기기의 것인지 모른다. origin 이 고정 상태가 아니면 되돌릴 것도 없다.
|
|
708
|
+
function unpinMarketplace(url) {
|
|
709
|
+
const PIN_PREFIX = "omc-pinned://";
|
|
710
|
+
if (existsSync(PINFILE)) dropPinRecord();
|
|
711
|
+
const restored = url.startsWith(PIN_PREFIX) ? url.slice(PIN_PREFIX.length) : "";
|
|
712
|
+
if (!restored) return "not-pinned";
|
|
713
|
+
try { git(["remote", "set-url", "origin", restored]); } catch { /* noop */ }
|
|
714
|
+
return "unpinned";
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// PINNED 를 지운다. 다른 기기의 기록이면 알린다. 깨진 기록(JSON 이 아니거나, null 처럼 객체가 아닌 값)은 말없이 지운다.
|
|
718
|
+
function dropPinRecord() {
|
|
719
|
+
let saved = null;
|
|
720
|
+
try { saved = JSON.parse(readFileSync(PINFILE, "utf8")); } catch { /* 깨졌으면 무시 */ }
|
|
721
|
+
unlinkSync(PINFILE);
|
|
722
|
+
if (saved && typeof saved === "object" && saved.host && saved.host !== hostname()) {
|
|
723
|
+
bad(`PINNED 는 다른 기기(${shown(saved.host)})의 기록입니다 — 그 값은 쓰지 않습니다.`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// --root 폴더의 이름표. .claude-plugin/plugin.json 의 name 이 있으면 그것으로, 없으면 package.json 의 name
|
|
728
|
+
// 으로 정한다. OMC 가 아니면 그 설명을, 아니면 null. 둘 다 없거나 읽을 수 없으면 확인할 길이 없어 null 이다
|
|
729
|
+
// (번들 사본에는 없을 수 있다). 다른 폴더를 잘못 준 실수를 막는 확인이다. 폴더 밖 쓰기를 막는 것은 whyNotWrite.
|
|
730
|
+
const OMC_NAMES = [[".claude-plugin/plugin.json", "oh-my-claudecode"], ["package.json", "oh-my-claude-sisyphus"]];
|
|
731
|
+
function otherPluginName(root) {
|
|
732
|
+
for (const [rel, want] of OMC_NAMES) {
|
|
733
|
+
let name;
|
|
734
|
+
try { name = JSON.parse(readFileSync(join(root, rel), "utf8")).name; } catch { continue; }
|
|
735
|
+
if (name === undefined) continue;
|
|
736
|
+
if (name === want) return null;
|
|
737
|
+
const quoted = JSON.stringify(name);
|
|
738
|
+
return `${rel} 의 name 이 ${quoted.length > 80 ? `${quoted.slice(0, 80)}...` : quoted}`;
|
|
739
|
+
}
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
|
|
516
743
|
// ── --scan: 사실만 내고 끝낸다 ───────────────────────────────────────────────
|
|
517
744
|
// 아래 main 보다 먼저 끝나야 한다. 이 모드는 installed_plugins.json 도 플러그인 캐시도 읽지 않는다.
|
|
518
745
|
// 결과는 입력 순서대로 JSON 배열 한 줄이다. 동기 쓰기로 내보내야 process.exit 가 잘라먹지 않는다.
|
|
@@ -520,7 +747,7 @@ if (SCAN_AT >= 0) {
|
|
|
520
747
|
const files = args.slice(SCAN_AT + 1);
|
|
521
748
|
if (!files.length) { console.error("--scan 뒤에 파일 경로가 필요합니다."); process.exit(1); }
|
|
522
749
|
const out = files.map((file) => {
|
|
523
|
-
try { return { file, ...scanFacts(readFileSync(file, "utf8")) }; }
|
|
750
|
+
try { return { file, ...publicFacts(scanFacts(readFileSync(file, "utf8"))) }; }
|
|
524
751
|
catch (e) { return { file, error: String(e.code || e.message) }; }
|
|
525
752
|
});
|
|
526
753
|
writeSync(1, JSON.stringify(out) + "\n");
|
|
@@ -529,28 +756,36 @@ if (SCAN_AT >= 0) {
|
|
|
529
756
|
|
|
530
757
|
// ── main ─────────────────────────────────────────────────────────────────────
|
|
531
758
|
if (ROOT_AT >= 0 && !ROOT) { bad("--root 뒤에 플러그인 폴더 경로가 필요합니다."); process.exit(1); }
|
|
759
|
+
// --check 는 읽기 전용이다. --revert 와 함께 받으면 예전에는 되돌리기가 이겨 파일을 썼다. 무엇도 하지 않고 멈춘다.
|
|
760
|
+
if (argv.has("--check") && argv.has("--revert")) {
|
|
761
|
+
bad("--check 와 --revert 는 함께 쓸 수 없습니다. 진단만 하려면 --check 만 주십시오.");
|
|
762
|
+
process.exit(1);
|
|
763
|
+
}
|
|
532
764
|
log(ROOT
|
|
533
765
|
? `OMC 패치 (대상 폴더 ${ROOT}) 모드: ${MODE}`
|
|
534
766
|
: `OMC 패치 (대상 ${TARGET_VERSION}) 모드: ${MODE}${NO_UPDATE ? " --no-update" : ""}`);
|
|
535
767
|
log(`플랫폼: ${process.platform} node ${process.version}`);
|
|
536
768
|
log();
|
|
537
769
|
|
|
538
|
-
// 헬퍼 자리 결함은 이 판의 apply 로 고칠 수 없다(이미 감싼 파일). 원본으로 되돌린 뒤 다시 적용한다.
|
|
539
|
-
const MISPLACED_HINT = ROOT
|
|
540
|
-
? "--revert --root 로 되돌린 뒤 같은 --root 로 다시 적용하십시오."
|
|
541
|
-
: "--revert 로 되돌린 뒤 다시 적용하십시오.";
|
|
542
|
-
|
|
543
770
|
// targets 는 «고칠 플러그인 폴더» 의 이름표 목록이다. 기본은 캐시의 판 번호이고, --root 는
|
|
544
771
|
// 그 폴더 경로 하나다. rootOf 가 이름표를 폴더로 바꾼다.
|
|
545
772
|
let active, targets;
|
|
546
773
|
const rootOf = (v) => (ROOT ? v : join(CACHE, v));
|
|
774
|
+
// 헬퍼 자리 결함은 이 판의 apply 로 고칠 수 없다(이미 감싼 파일). 원본으로 되돌린 뒤 다시 적용한다.
|
|
775
|
+
// 되돌리기는 그 폴더 하나만 한다. 인자 없는 --revert 는 설치된 모든 판을 되돌리고 고정까지 푼다.
|
|
776
|
+
// active 는 아래에서 정해지므로 쓰는 자리에서 부른다.
|
|
777
|
+
const misplacedHint = () => (ROOT
|
|
778
|
+
? "--revert --root 로 되돌린 뒤 같은 --root 로 다시 적용하십시오."
|
|
779
|
+
: `--revert --root "${join(CACHE, active)}" 로 활성 폴더만 되돌린 뒤 다시 적용하십시오.`);
|
|
547
780
|
if (ROOT) {
|
|
548
|
-
// 판 폴더도 installed_plugins.json 도 없는
|
|
549
|
-
// 마켓플레이스 고정([4/4])은 하지 않는다 — 그 둘은 이 PC 의 설정이고 이 폴더의 것이 아니다.
|
|
781
|
+
// 그 폴더 하나만 고친다(판 폴더도 installed_plugins.json 도 없는 번들 사본, 또는 활성 캐시 폴더).
|
|
782
|
+
// 판 바꾸기([1/4])와 마켓플레이스 고정([4/4])은 하지 않는다 — 그 둘은 이 PC 의 설정이고 이 폴더의 것이 아니다.
|
|
550
783
|
if (!existsSync(join(ROOT, "hooks", "hooks.json")) || !existsSync(join(ROOT, "scripts"))) {
|
|
551
784
|
bad(`플러그인 폴더가 아님 (hooks/hooks.json 과 scripts/ 가 있어야 함): ${ROOT}`);
|
|
552
785
|
process.exit(1);
|
|
553
786
|
}
|
|
787
|
+
const other = otherPluginName(ROOT);
|
|
788
|
+
if (other) { bad(`OMC 플러그인 폴더가 아님 (${other}): ${ROOT}`); process.exit(1); }
|
|
554
789
|
log("[1/4] 버전");
|
|
555
790
|
log(" 건너뜀 (--root 는 판을 바꾸지 않음)");
|
|
556
791
|
active = ROOT;
|
|
@@ -560,7 +795,7 @@ if (ROOT) {
|
|
|
560
795
|
|
|
561
796
|
log("[1/4] 버전");
|
|
562
797
|
if (NO_UPDATE) log(" 건너뜀 (--no-update)");
|
|
563
|
-
else if (MODE === "revert") log(" 건너뜀 (revert 는 버전을 되돌리지 않음 — README 의 롤백 절차 참고)");
|
|
798
|
+
else if (MODE === "revert") log(" 건너뜀 (revert 는 버전을 되돌리지 않음 — 킷 배포 폴더 README 의 롤백 절차 참고)");
|
|
564
799
|
else if (!ensureVersion() && MODE !== "check") { bad("버전 확보 실패 — 중단"); process.exit(1); }
|
|
565
800
|
|
|
566
801
|
const versions = installedVersions();
|
|
@@ -586,6 +821,11 @@ const r = { done: 0, skip: 0, fail: 0, need: 0, misplaced: 0 };
|
|
|
586
821
|
const needBy = {};
|
|
587
822
|
const misplacedBy = {};
|
|
588
823
|
const foreignBy = {};
|
|
824
|
+
const orphanBy = {};
|
|
825
|
+
// 되돌리기 끝 줄이 세는 파일. 쓰지 못한 것(failed)과 백업이 없어 남은 것(left)이다. 링크로 이어진 판 폴더는
|
|
826
|
+
// 같은 파일을 여러 이름으로 보이므로 fileKey 로 한 번만 센다. 위 [2/4] 줄의 건수는 판 폴더마다 센다.
|
|
827
|
+
const failed = new Set();
|
|
828
|
+
const left = new Set();
|
|
589
829
|
for (const v of targets) {
|
|
590
830
|
const root = rootOf(v);
|
|
591
831
|
if (!existsSync(root)) continue;
|
|
@@ -595,16 +835,21 @@ for (const v of targets) {
|
|
|
595
835
|
misplacedBy[v] = x.misplaced;
|
|
596
836
|
for (const k of ["done", "skip", "fail", "need", "misplaced"]) r[k] += x[k];
|
|
597
837
|
if (x.foreign?.length) foreignBy[v] = x.foreign;
|
|
838
|
+
if (x.orphan?.length) orphanBy[v] = x.orphan;
|
|
839
|
+
for (const n of [...x.foreign, ...x.orphan]) left.add(fileKey(join(root, "scripts", `${n}.mjs`)));
|
|
840
|
+
for (const p of x.failed) failed.add(fileKey(p));
|
|
598
841
|
}
|
|
599
|
-
log(` ${MODE === "check" ? `패치 필요 ${r.need}건` : MODE === "revert" ? `되돌림 ${r.done}건` : `패치 ${r.done}건 / 실패 ${r.fail}건`}${r.misplaced ? ` / 헬퍼 자리 결함 ${r.misplaced}건` : ""} / 대상 아님 ${r.skip}건`);
|
|
600
|
-
//
|
|
842
|
+
log(` ${MODE === "check" ? `패치 필요 ${r.need}건` : MODE === "revert" ? `되돌림 ${r.done}건${r.fail ? ` / 실패 ${r.fail}건` : ""}` : `패치 ${r.done}건 / 실패 ${r.fail}건`}${r.misplaced ? ` / 헬퍼 자리 결함 ${r.misplaced}건` : ""} / 대상 아님 ${r.skip}건`);
|
|
843
|
+
// 백업이 없어 되돌리지 못한 파일은 되돌릴 원본이 디스크에 없다. 조용히 넘기면 «완전히
|
|
601
844
|
// 되돌렸다» 는 잘못된 인상을 준다 — 무엇이 남았고 어떻게 되찾는지 정확히 말한다.
|
|
602
|
-
|
|
603
|
-
bad(`${v}:
|
|
845
|
+
function reportNoBackup(v, list, what) {
|
|
846
|
+
bad(`${v}: ${what} — ${list.map(shown).join(", ")}`);
|
|
604
847
|
log(` 백업(.omcbak)이 없어 되돌릴 원본이 디스크에 없습니다. 원본이 필요하면:`);
|
|
605
848
|
if (ROOT) log(` 같은 판의 상류 원본에서 scripts/<이름>.mjs 를 다시 가져오십시오.`);
|
|
606
849
|
else log(` git -C "${MARKET}" archive v${v} scripts/<이름>.mjs | tar -x -C "${join(CACHE, v)}"`);
|
|
607
850
|
}
|
|
851
|
+
for (const [v, list] of Object.entries(foreignBy)) reportNoBackup(v, list, "이 도구가 만들지 않은 수정이 남아 있습니다");
|
|
852
|
+
for (const [v, list] of Object.entries(orphanBy)) reportNoBackup(v, list, "패치는 있는데 백업이 없어 되돌리지 못했습니다");
|
|
608
853
|
|
|
609
854
|
log();
|
|
610
855
|
log("[3/4] PostToolUse 훅 제거");
|
|
@@ -617,9 +862,11 @@ for (const v of targets) {
|
|
|
617
862
|
if (Array.isArray(hj)) ok(`${tag}제거: ${hj.join(", ")}`);
|
|
618
863
|
else if (hj === "already") ok(`${tag}이미 제거됨`);
|
|
619
864
|
else if (hj === "needs") hjNeedBy[v] = true;
|
|
620
|
-
else if (hj === "reverted" || hj === "ambiguous") { /* patchHooksJson 이 출력함 */ }
|
|
865
|
+
else if (hj === "reverted" || hj === "ambiguous" || hj === "linked") { /* patchHooksJson 이 출력함 */ }
|
|
621
866
|
else if (hj === "no-backup") ok(`${tag}되돌릴 백업 없음 (이 도구가 지운 적 없음)`);
|
|
622
867
|
else if (hj === "no-file") bad(`${tag}hooks.json 파일 자체가 없음`);
|
|
868
|
+
else if (hj === "unsafe") { r.fail++; failed.add(fileKey(join(root, "hooks", "hooks.json"))); }
|
|
869
|
+
if (hj === "ambiguous" || hj === "linked") left.add(fileKey(join(root, "hooks", "hooks.json")));
|
|
623
870
|
}
|
|
624
871
|
|
|
625
872
|
log();
|
|
@@ -661,19 +908,24 @@ if (MODE === "check") {
|
|
|
661
908
|
const clean = (needBy[active] || 0) === 0 && !hjNeedBy[active] && pin !== "needs" && misplacedActive === 0;
|
|
662
909
|
const others = targets.filter((v) => v !== active && ((needBy[v] || 0) > 0 || hjNeedBy[v] || (misplacedBy[v] || 0) > 0));
|
|
663
910
|
log(clean ? "결과: 모두 적용된 상태입니다."
|
|
664
|
-
: misplacedActive ? `결과: 헬퍼 자리 결함 ${misplacedActive}건이 있습니다. ${
|
|
911
|
+
: misplacedActive ? `결과: 헬퍼 자리 결함 ${misplacedActive}건이 있습니다. ${misplacedHint()}`
|
|
665
912
|
: ROOT ? "결과: 적용되지 않은 항목이 있습니다. --check 없이 같은 --root 로 다시 실행하십시오."
|
|
913
|
+
: NO_UPDATE ? "결과: 적용되지 않은 항목이 있습니다. --check 없이 --no-update 로 다시 실행하십시오."
|
|
666
914
|
: "결과: 적용되지 않은 항목이 있습니다. 인자 없이 다시 실행하십시오.");
|
|
667
915
|
if (others.length) {
|
|
668
916
|
log(` 참고: 비활성 버전 ${others.join(", ")} 에 미적용분이 있습니다.`);
|
|
669
917
|
log(" 지금 쓰이지 않는 버전이라 판정에 넣지 않았습니다. 그 버전으로 되돌릴 계획이면 함께 적용하십시오.");
|
|
670
918
|
}
|
|
671
919
|
} else if (MODE === "revert") {
|
|
672
|
-
|
|
920
|
+
// 백업이 없어 남은 파일과 hooks.json 도 «끝내지 못함» 이다. 완료라고 하면 남은 것을 보지 못하고 넘어간다.
|
|
921
|
+
const why = [failed.size && `실패 ${failed.size}건`, left.size && `되돌리지 못한 것 ${left.size}건`].filter(Boolean);
|
|
922
|
+
log(why.length
|
|
923
|
+
? `되돌리기를 끝내지 못했습니다. ${why.join(", ")}. 위의 [!] 줄을 확인하십시오.`
|
|
924
|
+
: "되돌리기 완료. Claude Code 를 재시작해야 반영됩니다.");
|
|
673
925
|
} else {
|
|
674
926
|
log(`완료. 실패 ${r.fail}건.`);
|
|
675
|
-
if (r.misplaced) log(`[!] 헬퍼 자리 결함 ${r.misplaced}건은 고치지 않았습니다. ${
|
|
927
|
+
if (r.misplaced) log(`[!] 헬퍼 자리 결함 ${r.misplaced}건은 고치지 않았습니다. ${misplacedHint()}`);
|
|
676
928
|
log(" 훅 스크립트 패치는 즉시 적용됩니다 (훅이 호출될 때마다 파일을 새로 읽습니다).");
|
|
677
929
|
log("[!] PostToolUse 훅 제거는 hooks.json 변경이라 각 세션이 다시 시작될 때부터 적용됩니다.");
|
|
678
930
|
}
|
|
679
|
-
process.exit(r.fail > 0 ? 1 : 0);
|
|
931
|
+
process.exit(r.fail > 0 || left.size > 0 ? 1 : 0);
|