@kaydash9999/banker-plugins 0.19.0 → 0.19.2
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 +63 -0
- package/bin/lib/telemetry-config.mjs +25 -0
- package/hooks/obsidize-hook.mjs +23 -4
- package/hooks/progress.mjs +24 -12
- package/hooks/register.mjs +1 -0
- package/hooks/telemetry-count-skill.mjs +4 -6
- package/hooks/telemetry-count.mjs +4 -6
- package/hooks/update-checkin.mjs +18 -17
- package/hooks/update-fetch.mjs +8 -7
- package/package.json +1 -1
- package/skills/anti-drm/SKILL.md +14 -7
- package/skills/anti-drm/scripts/anti-drm.mjs +92 -31
- package/skills/anti-drm/scripts/instruction-toggle.mjs +45 -14
- package/skills/lineage/SKILL.md +4 -1
- package/skills/lineage/lineage.py +230 -82
- package/skills/omc-patch/SKILL.md +30 -16
- package/skills/omc-patch/scripts/omc-patch.mjs +291 -72
- package/skills/tone-compact/SKILL.md +7 -2
- package/skills/tone-compact/scripts/instruction-toggle.mjs +45 -14
- package/skills/tone-compact/scripts/tone-compact.mjs +2 -1
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import {
|
|
25
25
|
readFileSync, writeFileSync, writeSync, existsSync, copyFileSync, readdirSync,
|
|
26
26
|
unlinkSync, mkdirSync, rmSync, symlinkSync, lstatSync, realpathSync,
|
|
27
|
-
chmodSync, renameSync, statSync, constants as FS_CONST,
|
|
27
|
+
chmodSync, renameSync, statSync, accessSync, openSync, fstatSync, closeSync, fsyncSync, constants as FS_CONST,
|
|
28
28
|
} from "node:fs";
|
|
29
29
|
import { join, dirname, basename, resolve, sep as PATH_SEP, delimiter as PATH_LIST_SEP } from "node:path";
|
|
30
30
|
import { homedir, hostname } from "node:os";
|
|
@@ -124,7 +124,7 @@ function activeVersion() {
|
|
|
124
124
|
|
|
125
125
|
// installed_plugins.json 의 omc 항목. 읽기·파싱 실패는 null(옛 규칙으로).
|
|
126
126
|
function installedEntry() {
|
|
127
|
-
try { return JSON.parse(
|
|
127
|
+
try { return JSON.parse(readRegular(INSTALLED, "utf8")).plugins?.[PLUGIN_KEY]?.[0] || null; }
|
|
128
128
|
catch { return null; }
|
|
129
129
|
}
|
|
130
130
|
|
|
@@ -196,7 +196,13 @@ function git(args, cwd = MARKET, opts = {}) {
|
|
|
196
196
|
// ── 1. 업데이트 ──────────────────────────────────────────────────────────────
|
|
197
197
|
function ensureVersion() {
|
|
198
198
|
const have = installedVersions();
|
|
199
|
-
if (have.includes(TARGET_VERSION)) {
|
|
199
|
+
if (have.includes(TARGET_VERSION)) {
|
|
200
|
+
const now = activeVersion().v;
|
|
201
|
+
if (MODE === "check" || now === TARGET_VERSION) { ok(`${TARGET_VERSION} 설치되어 있음`); return true; }
|
|
202
|
+
log(` ${TARGET_VERSION} 은 캐시에 있으나 활성 판은 ${oneLine(now)} 입니다. 전환합니다`);
|
|
203
|
+
const sha = targetSha();
|
|
204
|
+
return sha === null ? false : switchActive(sha, have);
|
|
205
|
+
}
|
|
200
206
|
if (MODE === "check") { bad(`${TARGET_VERSION} 미설치 (설치된 버전: ${have.join(", ") || "없음"})`); return false; }
|
|
201
207
|
if (!canInstallTarget()) return false;
|
|
202
208
|
|
|
@@ -253,10 +259,17 @@ function linkNodeModules(dest, have) {
|
|
|
253
259
|
}
|
|
254
260
|
}
|
|
255
261
|
|
|
262
|
+
// installed_plugins.json 을 읽는다. 읽지 못하거나 JSON 이 아니면 이름과 이유를 대고 null(파일 글은 내지 않는다).
|
|
263
|
+
function readInstalled() {
|
|
264
|
+
try { return JSON.parse(readRegular(INSTALLED, "utf8")); }
|
|
265
|
+
catch (err) { bad(`읽지 못함: installed_plugins.json (${err instanceof SyntaxError ? "JSON 이 아님" : failWhy(err)})`); return null; }
|
|
266
|
+
}
|
|
267
|
+
|
|
256
268
|
// 활성 버전 전환. installed_plugins.json 을 읽거나 쓰지 못하면 이름과 이유를 대고 false(호출부가 중단한다).
|
|
257
269
|
function switchActive(sha, have) {
|
|
270
|
+
const j = readInstalled();
|
|
271
|
+
if (!j) return false;
|
|
258
272
|
try {
|
|
259
|
-
const j = JSON.parse(readFileSync(INSTALLED, "utf8"));
|
|
260
273
|
const e = j.plugins?.[PLUGIN_KEY]?.[0];
|
|
261
274
|
if (!e) { bad(`installed_plugins.json 에 ${PLUGIN_KEY} 항목이 없음 — 수동 확인 필요`); return false; }
|
|
262
275
|
backup(INSTALLED, `${INSTALLED}.omcbak`);
|
|
@@ -277,14 +290,15 @@ function switchActive(sha, have) {
|
|
|
277
290
|
// ── 쓰기 ─────────────────────────────────────────────────────────────────────
|
|
278
291
|
// 같은 폴더의 새 임시 파일(있으면 실패하는 열기)에 쓴 뒤 rename 으로 바꾼다. 하드 링크로 이어진 다른 이름(폴더 밖
|
|
279
292
|
// 파일)의 내용은 그대로 둔다. 쓸 파일이 링크면 쓰는 때의 실제 경로에서 바꿔 링크를 남긴다. 그래서 확인 뒤 그 자리가
|
|
280
|
-
// 폴더 밖 링크로 바뀌면 그 실제 파일이 바뀐다(확인과 쓰기 사이의 틈은 줄었을 뿐 없지 않다).
|
|
281
|
-
// 그
|
|
282
|
-
|
|
283
|
-
|
|
293
|
+
// 폴더 밖 링크로 바뀌면 그 실제 파일이 바뀐다(확인과 쓰기 사이의 틈은 줄었을 뿐 없지 않다). follow 가 false 면
|
|
294
|
+
// 링크를 따라가지 않고 그 이름 자리를 새 파일로 바꾼다(링크는 사라지고 링크가 가리키던 파일은 그대로다. 고정 기록).
|
|
295
|
+
// 권한은 mode 를 주면 그 값, 아니면 원래 파일의 것(없으면 0644)이다.
|
|
296
|
+
function writeAtomic(file, data, mode, { follow = true } = {}) {
|
|
297
|
+
const real = follow ? realOr(file) : file;
|
|
284
298
|
const tmp = join(dirname(real), `.${basename(real)}.omc-tmp-${process.pid}`);
|
|
285
299
|
rmSync(tmp, { force: true });
|
|
286
300
|
try {
|
|
287
|
-
|
|
301
|
+
writeSynced(tmp, data);
|
|
288
302
|
chmodSync(tmp, mode ?? modeOf(real));
|
|
289
303
|
renameRetry(tmp, real);
|
|
290
304
|
} catch (e) {
|
|
@@ -293,6 +307,18 @@ function writeAtomic(file, data, mode) {
|
|
|
293
307
|
}
|
|
294
308
|
}
|
|
295
309
|
|
|
310
|
+
// 새 임시 파일(있으면 실패하는 열기)에 쓰고 디스크까지 내린다. 내리지 않고 rename 하면 전원이 나가거나 NFS 에서
|
|
311
|
+
// 새 이름 아래 짧거나 빈 파일이 남을 수 있다(OMC 의 atomicWriteFileSync, obsidizer 의 writeTemp 와 같은 꼴).
|
|
312
|
+
function writeSynced(tmp, data) {
|
|
313
|
+
const fd = openSync(tmp, "wx");
|
|
314
|
+
try {
|
|
315
|
+
writeFileSync(fd, data);
|
|
316
|
+
fsyncSync(fd);
|
|
317
|
+
} finally {
|
|
318
|
+
closeSync(fd);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
296
322
|
// 파일의 권한 비트. 읽을 수 없으면 0644.
|
|
297
323
|
function modeOf(p) {
|
|
298
324
|
try { return statSync(p).mode & 0o777; } catch { return 0o644; }
|
|
@@ -316,13 +342,37 @@ function backup(file, bak) {
|
|
|
316
342
|
catch (e) { if (e.code !== "EEXIST") throw e; }
|
|
317
343
|
}
|
|
318
344
|
|
|
345
|
+
// 일반 파일만 읽는다. FIFO 는 열기만 하고 읽지 않으며, 폴더와 소켓, 장치는 던진다(형제 도구의 읽기와 같은 꼴).
|
|
346
|
+
function readRegular(p, enc) {
|
|
347
|
+
const fd = openSync(p, FS_CONST.O_RDONLY | (FS_CONST.O_NONBLOCK ?? 0));
|
|
348
|
+
try {
|
|
349
|
+
if (!fstatSync(fd).isFile()) throw Object.assign(new Error("일반 파일이 아님"), { code: "ENOTFILE" });
|
|
350
|
+
return readFileSync(fd, enc);
|
|
351
|
+
} finally {
|
|
352
|
+
closeSync(fd);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
319
356
|
// 쓰기 실패의 이유 한 줄.
|
|
320
357
|
const failWhy = (e) => oneLine(e.code || e.message);
|
|
321
358
|
|
|
322
359
|
// ── 2. 훅 패치 ───────────────────────────────────────────────────────────────
|
|
360
|
+
// 훅 글을 읽는다. 읽지 못하면(권한, 일반 파일이 아님) 이름과 이유를 대고 null. check 는 확인하지 못한 것이라 모두 적용으로 보지 않는다.
|
|
361
|
+
function readHook(n, f, r) {
|
|
362
|
+
try { return readRegular(f, "utf8"); }
|
|
363
|
+
catch (e) {
|
|
364
|
+
bad(`읽지 못함: ${shown(n)} (${failWhy(e)})`);
|
|
365
|
+
if (MODE === "check") r.need++;
|
|
366
|
+
else { r.fail++; r.failed.push(f); }
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// hooks.json 과 그 백업이 부르는 훅 이름. 읽지 못하는 파일은 건너뛴다. hooks.json 은 [3/4] 가 이름과 이유를 대고 센다.
|
|
323
372
|
function hookNames(root) {
|
|
324
373
|
const files = [join(root, "hooks", "hooks.json"), `${join(root, "hooks", "hooks.json")}.omcbak`];
|
|
325
|
-
const
|
|
374
|
+
const readOrEmpty = (f) => { try { return readRegular(f, "utf8"); } catch { return ""; } };
|
|
375
|
+
const src = files.filter(existsSync).map(readOrEmpty).join("");
|
|
326
376
|
return [...new Set([...src.matchAll(/scripts\/([\w-]+)\.mjs/g)].map((m) => m[1]))].sort();
|
|
327
377
|
}
|
|
328
378
|
|
|
@@ -602,6 +652,12 @@ function syntaxError(file) {
|
|
|
602
652
|
}
|
|
603
653
|
}
|
|
604
654
|
|
|
655
|
+
// 백업의 글. 없거나 읽으려는 순간 사라졌으면 null(다른 실행이 먼저 되돌렸을 수 있다).
|
|
656
|
+
function readBackup(bak) {
|
|
657
|
+
try { return readRegular(bak); }
|
|
658
|
+
catch (e) { if (e.code === "ENOENT") return null; throw e; }
|
|
659
|
+
}
|
|
660
|
+
|
|
605
661
|
// 이 도구가 아닌 «손» 이 import 를 감싼 흔적. 4.15.7 의 손 수정은 최상위의 `await import(` 를
|
|
606
662
|
// `await Promise.race([import(` 로 바꿔치기했다. 그 모양이 최상위(함수 밖) 코드 자리에 있을 때만 손 수정으로 본다.
|
|
607
663
|
// 문자열, 주석, 함수 몸통 안의 같은 모양이나 import 가 첫 원소가 아닌 race 는 상류 코드일 수 있어 세지 않는다.
|
|
@@ -635,36 +691,74 @@ function patchHooks(root) {
|
|
|
635
691
|
|
|
636
692
|
function revertScript(root, n, f, r) {
|
|
637
693
|
const bak = `${f}.omcbak`;
|
|
638
|
-
if (existsSync(bak)
|
|
694
|
+
if (existsSync(bak) && restoreScript(root, n, f, bak, r)) return;
|
|
639
695
|
// 백업이 없는데 이미 감싸여 있으면 되돌릴 원본이 디스크에 없다. 조용히 넘기지 않고 이름을 댄다.
|
|
640
696
|
// - 우리 헬퍼가 있으면: 이 도구나 옛 킷이 고쳤는데 백업이 사라진 파일(복사해 온 폴더 등)
|
|
641
697
|
// - 손 패치: `await import(` 를 `await Promise.race([import(` 로 «바꿔치기» 해서 await import
|
|
642
698
|
// 패턴 자체가 사라진다. siteInfo 로는 잡히지 않아 원문에서 직접 찾는다(실측: 4.15.7 의 두 파일)
|
|
643
|
-
const src =
|
|
699
|
+
const src = readHook(n, f, r);
|
|
700
|
+
if (src === null) return;
|
|
644
701
|
if (src.includes("__omcRaceImport")) r.orphan.push(n);
|
|
645
702
|
else if (looksHandPatched(src)) r.foreign.push(n);
|
|
646
703
|
}
|
|
647
704
|
|
|
648
|
-
// 백업으로 되돌린다. 쓸 수 없는 자리거나 쓰지 못하면 이유를 대고 실패로 센다.
|
|
705
|
+
// 백업으로 되돌린다. 쓸 수 없는 자리거나 쓰지 못하면 이유를 대고 실패로 센다. 백업이 읽으려는 순간 사라졌으면 false
|
|
706
|
+
// (다른 실행이 먼저 되돌렸을 수 있다. 호출부가 백업 없음으로 다시 본다).
|
|
649
707
|
function restoreScript(root, n, f, bak, r) {
|
|
650
708
|
const why = whyNotWrite(root, f, bak) ?? restoreFrom(f, bak);
|
|
651
|
-
if (
|
|
709
|
+
if (why === GONE) return false;
|
|
710
|
+
if (!why) {
|
|
711
|
+
log(` 되돌림 ${shown(n)}`); r.done++;
|
|
712
|
+
const left = dropBackup(bak);
|
|
713
|
+
if (left) { bad(`되돌렸으나 백업을 지우지 못함: ${shown(n)} (${left}): ${oneLine(bak)}`); r.fail++; r.failed.push(f); }
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
652
716
|
bad(`되돌리지 않음: ${shown(n)} (${why})`);
|
|
653
717
|
// 옛 도구가 폴더 밖 링크 너머에서 고친 파일이다. 이 도구는 폴더 밖에 쓰지 않으므로 손 복구 방법을 알린다.
|
|
654
|
-
if (why === OUTSIDE) {
|
|
655
|
-
log(` 손으로 되돌리려면 ${oneLine(realOr(bak))} 을 확인한 뒤 그 내용으로 ${oneLine(realOr(f))} 을 덮어쓰고 백업을 지우십시오.`);
|
|
656
|
-
}
|
|
718
|
+
if (why === OUTSIDE) log(` ${handRestoreHint(bak, f)}`);
|
|
657
719
|
r.fail++; r.failed.push(f);
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// 링크 너머의 일반 파일(1MiB 이하)에 헬퍼가 있는가. 옛 도구가 링크를 따라가 거기서 고친 파일이면 참이다.
|
|
724
|
+
const PEEK_MAX = 1 << 20;
|
|
725
|
+
function patchedByOldTool(p) {
|
|
726
|
+
try {
|
|
727
|
+
const st = statSync(p);
|
|
728
|
+
return st.isFile() && st.size <= PEEK_MAX && readRegular(p, "utf8").includes("__omcRaceImport");
|
|
729
|
+
} catch {
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// 손 복구 안내. 링크 너머의 파일에 이 도구 계열의 패치가 있을 때만 백업으로 덮어쓰라고 하고, 아니면 링크부터 확인하게 한다.
|
|
735
|
+
function handRestoreHint(bak, f) {
|
|
736
|
+
const target = realOr(f);
|
|
737
|
+
return patchedByOldTool(target)
|
|
738
|
+
? `손으로 되돌리려면 ${oneLine(realOr(bak))} 의 내용을 확인한 뒤 ${oneLine(target)} 에 덮어쓰고 백업을 지우십시오.`
|
|
739
|
+
: `링크가 가리키는 ${oneLine(target)} 에 이 도구의 패치가 보이지 않습니다. 덮어쓰지 말고 링크가 가리키는 곳이 맞는지 먼저 확인하십시오.`;
|
|
658
740
|
}
|
|
659
741
|
|
|
660
|
-
// 백업의 내용으로 파일을
|
|
742
|
+
// 백업의 내용으로 파일을 바꾼다. 쓰지 못하면 그 이유, 백업이 읽으려는 순간 사라졌으면 GONE, 아니면 null.
|
|
743
|
+
const GONE = "백업이 먼저 사라짐";
|
|
661
744
|
function restoreFrom(f, bak) {
|
|
662
|
-
|
|
745
|
+
let data;
|
|
746
|
+
try { data = readRegular(bak); }
|
|
747
|
+
catch (e) { return e.code === "ENOENT" ? GONE : `읽지 못함: ${failWhy(e)}`; }
|
|
748
|
+
try { writeAtomic(f, data); return null; }
|
|
663
749
|
catch (e) { return `쓰지 못함: ${failWhy(e)}`; }
|
|
664
750
|
}
|
|
665
751
|
|
|
752
|
+
// 되돌린 뒤 백업을 지운다. 다른 실행이 먼저 지웠으면(ENOENT) 된 것이다. 못 지우면 그 이유, 아니면 null.
|
|
753
|
+
function dropBackup(bak) {
|
|
754
|
+
try { unlinkSync(bak); return null; }
|
|
755
|
+
catch (e) { return e.code === "ENOENT" ? null : failWhy(e); }
|
|
756
|
+
}
|
|
757
|
+
|
|
666
758
|
function checkOrPatchScript(root, n, f, r) {
|
|
667
|
-
const
|
|
759
|
+
const text = readHook(n, f, r);
|
|
760
|
+
if (text === null) return;
|
|
761
|
+
const info = siteInfo(text);
|
|
668
762
|
// 헬퍼 자리 결함은 check 와 apply 모두 이름을 댄다. 이미 감싼 파일이라 transform 으로는
|
|
669
763
|
// 고칠 수 없으므로 apply 도 건드리지 않고, 되돌린 뒤 다시 적용하라고 안내한다(misplacedHint).
|
|
670
764
|
if (info.state === "misplaced") { bad(`헬퍼 자리 결함: ${n} (${info.why})`); r.misplaced++; return; }
|
|
@@ -674,7 +768,7 @@ function checkOrPatchScript(root, n, f, r) {
|
|
|
674
768
|
// --check 도 쓰기 조건을 본다. apply 에서야 «쓰지 않음» 이 되는 파일을 진단에서 미리 알린다.
|
|
675
769
|
if (MODE === "check") { bad(why ? `패치 필요, 쓸 수 없음: ${n} (${why})` : `패치 필요: ${n}`); r.need++; return; }
|
|
676
770
|
if (why) { bad(`쓰지 않음: ${n} (${why})`); r.fail++; return; }
|
|
677
|
-
const out = transform(
|
|
771
|
+
const out = transform(text);
|
|
678
772
|
if (!out) { bad(`변환 실패: ${n}`); r.fail++; return; }
|
|
679
773
|
if (writePatched(f, bak, out, n)) r.done++;
|
|
680
774
|
else r.fail++;
|
|
@@ -692,7 +786,12 @@ function writePatched(f, bak, out, n) {
|
|
|
692
786
|
}
|
|
693
787
|
const err = syntaxError(f);
|
|
694
788
|
if (!err) return true;
|
|
695
|
-
|
|
789
|
+
try {
|
|
790
|
+
writeAtomic(f, readRegular(bak));
|
|
791
|
+
} catch (e) {
|
|
792
|
+
bad(`롤백도 실패: ${n} (${failWhy(e)}). ${oneLine(realOr(bak))} 으로 손수 되돌리십시오\n ${oneLine(err)}`);
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
696
795
|
bad(`문법오류로 롤백: ${n}\n ${oneLine(err)}`);
|
|
697
796
|
return false;
|
|
698
797
|
}
|
|
@@ -705,6 +804,16 @@ const OUTSIDE = "파일이 플러그인 폴더 밖에 있음";
|
|
|
705
804
|
function whyNotWrite(root, file, bak) {
|
|
706
805
|
if (isLink(bak)) return "백업 자리가 링크";
|
|
707
806
|
if (!insideRoot(root, file)) return OUTSIDE;
|
|
807
|
+
return notWritable(file);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// 있는 자리가 일반 파일이 아니거나(FIFO, 폴더, 장치) 이 계정이 쓸 수 없으면 그 이유. 새 파일로 바꿔 쓰는 방식이 파일 권한을
|
|
811
|
+
// 우회하지 않게 하고, 특수 파일을 일반 파일로 바꾸지 않게 한다. 없는 자리는 괜찮다.
|
|
812
|
+
function notWritable(file) {
|
|
813
|
+
let st;
|
|
814
|
+
try { st = statSync(file); } catch { return null; }
|
|
815
|
+
if (!st.isFile()) return "일반 파일이 아님";
|
|
816
|
+
try { accessSync(file, FS_CONST.W_OK); } catch { return "쓸 권한이 없음"; }
|
|
708
817
|
return null;
|
|
709
818
|
}
|
|
710
819
|
|
|
@@ -730,16 +839,21 @@ const realOr = (p) => {
|
|
|
730
839
|
const fileKey = (p) => (process.platform === "win32" ? realOr(p).toLowerCase() : realOr(p));
|
|
731
840
|
|
|
732
841
|
// ── 3. PostToolUse 훅 제거 ───────────────────────────────────────────────────
|
|
842
|
+
// [3/4] 에서 지금 다루는 판의 이름표(`5.6.1: `, 판이 하나면 빈 글). hooks.json 의 [!] 줄도 어느 판의 것인지 알려야 한다.
|
|
843
|
+
let hjTag = "";
|
|
844
|
+
const hjBad = (s) => bad(`${hjTag}${s}`);
|
|
845
|
+
|
|
733
846
|
function patchHooksJson(root) {
|
|
734
847
|
const hj = join(root, "hooks", "hooks.json");
|
|
735
848
|
if (!existsSync(hj)) return "no-file";
|
|
736
849
|
const why = whyNotWrite(root, hj, `${hj}.omcbak`);
|
|
737
|
-
if (why && MODE !== "check") {
|
|
850
|
+
if (why && MODE !== "check") { hjBad(`hooks.json 을 건드리지 않음 (${why})`); return "unsafe"; }
|
|
738
851
|
try {
|
|
739
852
|
return MODE === "revert" ? revertHooksJson(root, hj) : removePostToolUse(hj, why);
|
|
740
853
|
} catch (e) {
|
|
741
|
-
// JSON 오류 문구는 파일의 글을 담으므로 내지 않는다.
|
|
742
|
-
|
|
854
|
+
// JSON 오류 문구는 파일의 글을 담으므로 내지 않는다. check 는 읽기만 하므로 실패도 읽기다.
|
|
855
|
+
const why = e instanceof SyntaxError ? "JSON 이 아님" : failWhy(e);
|
|
856
|
+
hjBad(e instanceof SyntaxError || MODE === "check" ? `hooks.json 을 읽지 못함 (${why})` : `hooks.json 을 쓰지 못함 (${why})`);
|
|
743
857
|
return "unsafe";
|
|
744
858
|
}
|
|
745
859
|
}
|
|
@@ -747,7 +861,14 @@ function patchHooksJson(root) {
|
|
|
747
861
|
// 되돌린 결과. ambiguous 와 linked 는 hooks.json 을 되돌리지 못하고 남긴 것이라 끝 줄이 센다.
|
|
748
862
|
function revertHooksJson(root, hj) {
|
|
749
863
|
const bak = `${hj}.omcbak`;
|
|
750
|
-
|
|
864
|
+
const saved = readBackup(bak);
|
|
865
|
+
if (saved !== null) {
|
|
866
|
+
writeAtomic(hj, saved);
|
|
867
|
+
log(` 되돌림 ${hjTag}hooks.json`);
|
|
868
|
+
const left = dropBackup(bak);
|
|
869
|
+
if (left) { hjBad(`되돌렸으나 hooks.json 의 백업을 지우지 못함 (${left}): ${oneLine(bak)}`); return "unsafe"; }
|
|
870
|
+
return "reverted";
|
|
871
|
+
}
|
|
751
872
|
// 이 도구의 백업이 없을 때 PostToolUse 가 그대로면 되돌릴 것이 없다. 옆의 수동 백업으로 덮어쓰면
|
|
752
873
|
// 지금 내용을 잃는다.
|
|
753
874
|
if (hasPostToolUse(hj)) return "no-backup";
|
|
@@ -755,13 +876,13 @@ function revertHooksJson(root, hj) {
|
|
|
755
876
|
// PostToolUse 를 담고 있는 유효한 JSON 이 딱 하나면 그것이 제거 직전 상태다 — 무엇을 썼는지 밝히고 되돌린다.
|
|
756
877
|
const dir = dirname(hj);
|
|
757
878
|
const { usable, unsafe } = manualBackups(root, dir);
|
|
758
|
-
if (unsafe.length)
|
|
879
|
+
if (unsafe.length) hjBad(`hooks.json 수동 백업 후보가 링크이거나 플러그인 폴더 밖이라 쓰지 않음: ${unsafe.map(shown).join(", ")}`);
|
|
759
880
|
if (usable.length === 1) {
|
|
760
|
-
writeAtomic(hj,
|
|
761
|
-
log(` 되돌림 hooks.json (수동 백업 사용: ${shown(usable[0])})`);
|
|
881
|
+
writeAtomic(hj, readRegular(join(dir, usable[0])));
|
|
882
|
+
log(` 되돌림 ${hjTag}hooks.json (수동 백업 사용: ${shown(usable[0])})`);
|
|
762
883
|
return "reverted";
|
|
763
884
|
}
|
|
764
|
-
if (usable.length > 1) {
|
|
885
|
+
if (usable.length > 1) { hjBad(`hooks.json 수동 백업이 여러 개라 자동 복구하지 않음: ${usable.map(shown).join(", ")}`); return "ambiguous"; }
|
|
765
886
|
return unsafe.length ? "linked" : "no-backup";
|
|
766
887
|
}
|
|
767
888
|
|
|
@@ -774,10 +895,36 @@ function revertHooksJson(root, hj) {
|
|
|
774
895
|
const UNSAFE_CH = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Co}\p{Cn}\p{Variation_Selector}\p{Default_Ignorable_Code_Point}\u{2800}\u{a0}\u{1680}\u{2000}-\u{200a}\u{202f}\u{205f}\u{3000}]/gu;
|
|
775
896
|
const oneLine = (s) => String(s).replace(UNSAFE_CH, "?");
|
|
776
897
|
const shown = (s) => oneLine(s).slice(0, 120);
|
|
777
|
-
//
|
|
778
|
-
//
|
|
898
|
+
// 처리하지 못한 오류도 스택 트레이스 대신 정화한 한 줄과 종료 1 로 끝낸다. 오류 문구는 경로를 담아 줄바꿈이 들 수 있다.
|
|
899
|
+
// 시스템 오류(code 가 있는 것)가 아닌 도구 자신의 결함이면 어디서 났는지 앞 세 곳만 정화해 보인다.
|
|
900
|
+
process.on("uncaughtException", (e) => {
|
|
901
|
+
bad(`처리하지 못한 오류: ${oneLine(e?.message ?? e)}`);
|
|
902
|
+
if (!e?.code) for (const l of String(e?.stack ?? "").split("\n").filter((x) => /^\s+at /.test(x)).slice(0, 3)) log(` ${oneLine(l.trim())}`);
|
|
903
|
+
process.exit(1);
|
|
904
|
+
});
|
|
905
|
+
// URL 의 사용자 정보(`//` 뒤 첫 `/` 앞의 마지막 `@` 까지. 공백, 탭, 비밀번호 안의 @ 도)와 쿼리의 값(이름과 상관없이)을
|
|
906
|
+
// 가린다. 출력과 기록에 비밀이 남지 않는다. ssh 꼴(`git@host:`)과 경로 안의 @ 는 그대로다. 쿼리의 이름 칸은 `?` 를 받지
|
|
907
|
+
// 않는다. 받으면 `=` 없는 `?` 가 많은 origin 에서 시작 자리마다 끝까지 읽어 길이의 제곱만큼 걸린다.
|
|
779
908
|
const redactUrl = (url) => String(url).replace(/(\/\/)[^/]*@/g, "$1***@")
|
|
780
|
-
.replace(/([?&]
|
|
909
|
+
.replace(/([?&][^=&#\s?]*=)[^&#\s]*/g, "$1***");
|
|
910
|
+
|
|
911
|
+
// URL 의 scheme 과 host 까지만. 비밀은 사용자 정보와 쿼리 밖(경로, 조각, `;` 매개변수)에도 들 수 있어 나머지는 버린다.
|
|
912
|
+
// scp 꼴(`git@host:org/repo.git`)은 host 만이다. 파서가 host 로 읽어도 host 모양(이름이나 [IPv6], 선택 포트)이 아니면
|
|
913
|
+
// 보이지 않는다(ssh 같은 scheme 은 host 뒤의 `;k=...` 까지 host 로 읽는다).
|
|
914
|
+
const HOST_SHAPE = /^(?:[A-Za-z0-9._-]{1,253}|\[[0-9A-Fa-f:.]{2,45}\])(?::\d{1,5})?$/;
|
|
915
|
+
function hostOnly(url) {
|
|
916
|
+
let scheme = "", host;
|
|
917
|
+
try {
|
|
918
|
+
const u = new URL(String(url));
|
|
919
|
+
if (!u.host) return "(호스트가 없는 URL 또는 로컬 경로)";
|
|
920
|
+
[scheme, host] = [`${u.protocol}//`, u.host];
|
|
921
|
+
} catch {
|
|
922
|
+
const scp = /^(?:[^@/:]+@)?([^:/]+):(?!\/\/)/.exec(String(url));
|
|
923
|
+
if (!scp) return "(URL 꼴이 아님)";
|
|
924
|
+
host = scp[1];
|
|
925
|
+
}
|
|
926
|
+
return HOST_SHAPE.test(host) ? `${scheme}${host}` : "(호스트를 알아볼 수 없음)";
|
|
927
|
+
}
|
|
781
928
|
|
|
782
929
|
// hooks.json 옆의 수동 백업 후보. usable 은 PostToolUse 를 담은 유효한 JSON 이다. 링크로 심은 후보는
|
|
783
930
|
// unsafe 로 따로 두고 읽지도 않는다. 폴더 밖 파일(PostToolUse 키가 있는 settings.json 등)이 hooks.json 이 된다.
|
|
@@ -793,21 +940,21 @@ function manualBackups(root, dir) {
|
|
|
793
940
|
}
|
|
794
941
|
|
|
795
942
|
const hasPostToolUse = (p) => {
|
|
796
|
-
try { return !!JSON.parse(
|
|
943
|
+
try { return !!JSON.parse(readRegular(p, "utf8"))?.hooks?.PostToolUse; }
|
|
797
944
|
catch { return false; }
|
|
798
945
|
};
|
|
799
946
|
|
|
800
947
|
function removePostToolUse(hj, why) {
|
|
801
948
|
const bak = `${hj}.omcbak`;
|
|
802
|
-
const j = JSON.parse(
|
|
949
|
+
const j = JSON.parse(readRegular(hj, "utf8"));
|
|
803
950
|
if (!j.hooks?.PostToolUse) return "already";
|
|
804
951
|
const list = j.hooks.PostToolUse.flatMap((g) => (g.hooks || []).map((h) => (h.command || "").match(/([\w-]+)\.mjs/)?.[1] || "?"));
|
|
805
|
-
if (MODE === "check") {
|
|
952
|
+
if (MODE === "check") { hjBad(`hooks.json PostToolUse 남아있음${why ? `, 쓸 수 없음 (${why})` : ""}: ${list.join(", ")}`); return "needs"; }
|
|
806
953
|
backup(hj, bak);
|
|
807
954
|
delete j.hooks.PostToolUse;
|
|
808
955
|
writeAtomic(hj, JSON.stringify(j, null, 2) + "\n");
|
|
809
|
-
try { JSON.parse(
|
|
810
|
-
catch { writeAtomic(hj,
|
|
956
|
+
try { JSON.parse(readRegular(hj, "utf8")); return list; }
|
|
957
|
+
catch { writeAtomic(hj, readRegular(bak)); hjBad("hooks.json 손상으로 롤백"); return "failed"; }
|
|
811
958
|
}
|
|
812
959
|
|
|
813
960
|
// ── 4. 자동 업데이트 차단 ────────────────────────────────────────────────────
|
|
@@ -821,15 +968,22 @@ function removePostToolUse(hj, why) {
|
|
|
821
968
|
|
|
822
969
|
// origin 의 원문. `git remote get-url` 은 전역 insteadOf 를 펼친 값을 내서, 그 값이 고정되고 되돌린 뒤에도 원래
|
|
823
970
|
// 값 대신 남았다. 설정 값을 그대로 읽는다. 없으면(git 이 1 로 끝남) null.
|
|
971
|
+
// git 이 그 밖의 까닭으로 읽지 못하면(git 없음, 다른 계정 저장소 거부, 깨진 설정) { unreadable } 이다. 없는 것으로 보면
|
|
972
|
+
// 고정과 되돌리기가 하지 않은 일을 했다고 끝난다.
|
|
824
973
|
function originUrl() {
|
|
825
|
-
try { return git(["config", "--get", "remote.origin.url"]); }
|
|
974
|
+
try { return git(["config", "--get", "remote.origin.url"]); }
|
|
975
|
+
catch (e) { return e.status === 1 ? null : { unreadable: gitError(e) }; }
|
|
826
976
|
}
|
|
827
977
|
|
|
828
978
|
// git 오류의 원인 줄: `fatal:` 이나 `error:` 로 시작하는 첫 줄, 없으면 첫 줄(`warning:` 이 먼저 올 수 있다).
|
|
829
|
-
// 정화하고 URL 의 인증 정보를 가린다.
|
|
979
|
+
// 정화하고 URL 의 인증 정보를 가린다. git 이 오류 문구 없이 끝나면 Node 의 문구(명령 줄 전체, 곧 URL 전체가 든다)를
|
|
980
|
+
// 쓰지 않고 어떻게 끝났는지만 댄다.
|
|
830
981
|
const gitError = (e) => {
|
|
831
|
-
const lines = String(e.stderr ||
|
|
832
|
-
|
|
982
|
+
const lines = String(e.stderr || "").split("\n").filter((l) => l.trim());
|
|
983
|
+
if (!lines.length) {
|
|
984
|
+
return e.status == null && !e.signal && e.code ? `git 을 실행하지 못함 (${oneLine(e.code)})` : `git 이 오류 문구 없이 끝남 (${oneLine(e.signal || e.status)})`;
|
|
985
|
+
}
|
|
986
|
+
const line = lines.find((l) => /^(fatal|error):/.test(l)) || lines[0];
|
|
833
987
|
return oneLine(redactUrl(line)).slice(0, 200);
|
|
834
988
|
};
|
|
835
989
|
|
|
@@ -841,6 +995,7 @@ function pinMarketplace() {
|
|
|
841
995
|
if (!existsSync(join(MARKET, ".git"))) return "no-git";
|
|
842
996
|
const url = originUrl();
|
|
843
997
|
if (url === null) return "no-remote";
|
|
998
|
+
if (typeof url !== "string") { bad(`origin 을 읽지 못함 (${url.unreadable}): ${oneLine(MARKET)}`); return "origin-unreadable"; }
|
|
844
999
|
|
|
845
1000
|
if (MODE === "revert") return unpinMarketplace(url);
|
|
846
1001
|
if (url.startsWith("omc-pinned://")) return alreadyPinned();
|
|
@@ -855,15 +1010,26 @@ function pinMarketplace() {
|
|
|
855
1010
|
// PINFILE 은 스크립트 위치가 아니라 homedir() 기준이라 남의 기록인 줄 모르고 쓰기 쉽다.
|
|
856
1011
|
// URL 은 남기지 않는다. 원래 URL 은 origin 의 `omc-pinned://` 뒤에 있고, 인증 정보가 든 URL 이 기록에 남는다.
|
|
857
1012
|
// 고정이 된 뒤에만 쓴다. 실패한 고정의 기록은 남기지 않는다. 기록을 쓰지 못하면 고정은 둔 채 실패로 센다.
|
|
1013
|
+
// 덮어쓴 옛 기록에 URL 칸이 있었고 같은 파일의 다른 이름이 있으면, 그쪽에 남은 칸을 알리고 실패로 센다.
|
|
858
1014
|
function writePinRecord() {
|
|
1015
|
+
let ino = null;
|
|
859
1016
|
try {
|
|
860
1017
|
mkdirSync(dirname(PINFILE), { recursive: true });
|
|
861
|
-
|
|
862
|
-
|
|
1018
|
+
if (isLink(PINFILE)) throw new Error("링크라 따라가 쓰지 않음");
|
|
1019
|
+
ino = oldCellIno();
|
|
1020
|
+
writeAtomic(PINFILE, JSON.stringify({ pinnedAt: new Date().toISOString(), version: TARGET_VERSION, host: hostname() }, null, 2) + "\n", 0o600, { follow: false });
|
|
863
1021
|
} catch (e) {
|
|
864
1022
|
bad(`고정 기록(PINNED)을 쓰지 못함 (${failWhy(e)}). origin 은 고정됨: ${oneLine(PINFILE)}`);
|
|
865
1023
|
return false;
|
|
866
1024
|
}
|
|
1025
|
+
return ino === null ? true : sharedLeft(SHARED_WHY, ino);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// 덮어쓸 옛 기록에 URL 칸이 있고 같은 파일의 다른 이름이 있으면 그 inode 다. 칸이 없거나, 다른 이름이 없거나,
|
|
1029
|
+
// 기록을 읽지 못하면 null 이다.
|
|
1030
|
+
function oldCellIno() {
|
|
1031
|
+
try { return hasUrlKey(parseRecord(pinRecordText()).rec) ? sharedIno() : null; }
|
|
1032
|
+
catch { return null; }
|
|
867
1033
|
}
|
|
868
1034
|
|
|
869
1035
|
// 이미 고정된 origin. 옛 기록의 URL 칸을 지우지 못했으면 고정은 그대로 두고 실패로 센다.
|
|
@@ -881,23 +1047,48 @@ function scrubPinRecord() {
|
|
|
881
1047
|
if (!left) return true;
|
|
882
1048
|
if (left.why) return pinUrlLeft(left.why);
|
|
883
1049
|
delete left.rec.originUrl;
|
|
884
|
-
|
|
1050
|
+
const ino = sharedIno();
|
|
1051
|
+
try { writeAtomic(PINFILE, JSON.stringify(left.rec, null, 2) + "\n", 0o600, { follow: false }); }
|
|
885
1052
|
catch (e) { return pinUrlLeft(`지우지 못함 (쓰지 못함: ${failWhy(e)})`); }
|
|
1053
|
+
return ino === null ? true : sharedLeft(SHARED_WHY, ino);
|
|
1054
|
+
}
|
|
1055
|
+
const SHARED_WHY = "이 이름에서만 지움 (같은 파일의 다른 이름이 있어 그쪽에는 남음)";
|
|
1056
|
+
const SHARED_CHECK_WHY = "이 이름에서만 지울 수 있음 (같은 파일의 다른 이름이 있어 그쪽에는 남음)";
|
|
1057
|
+
|
|
1058
|
+
// 기록에 같은 파일의 다른 이름(하드 링크)이 있으면 그 파일의 inode(Windows 는 파일 ID, 정밀도를 잃지 않게 BigInt)다.
|
|
1059
|
+
// 없거나 확인하지 못하면 null. 이 이름을 rename 이나 unlink 로 바꾼 뒤에는 그 번호가 다른 이름들에만 남는다.
|
|
1060
|
+
function sharedIno() {
|
|
1061
|
+
try {
|
|
1062
|
+
const st = lstatSync(PINFILE, { bigint: true });
|
|
1063
|
+
return st.nlink > 1n ? st.ino : null;
|
|
1064
|
+
} catch {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
886
1067
|
}
|
|
1068
|
+
const sharedLeft = (what, ino) => pinUrlLeft(what, `\n 다른 이름은 같은 파일 시스템에서 inode(Windows 는 파일 ID) ${ino} 로 찾습니다.`);
|
|
887
1069
|
|
|
888
1070
|
// --check 의 판정. 남은 칸을 적용이 지울 수 있으면 scrub-needs, 이 도구로 지울 수 없으면 그 까닭을 알리고
|
|
889
1071
|
// scrub-blocked 다. 남은 칸이 없으면 already 다.
|
|
890
1072
|
function pinUrlCheck() {
|
|
891
1073
|
const left = pinUrlLeftover();
|
|
892
1074
|
if (!left) return "already";
|
|
893
|
-
|
|
894
|
-
|
|
1075
|
+
const why = left.why || pinDirWhy();
|
|
1076
|
+
const ino = why ? null : sharedIno();
|
|
1077
|
+
if (why || ino !== null) {
|
|
1078
|
+
if (why) pinUrlLeft(why);
|
|
1079
|
+
else sharedLeft(SHARED_CHECK_WHY, ino);
|
|
895
1080
|
return "scrub-blocked";
|
|
896
1081
|
}
|
|
897
1082
|
bad(`고정 기록(PINNED)에 옛 URL 칸이 남아 있음 (적용하면 지움): ${oneLine(PINFILE)}`);
|
|
898
1083
|
return "scrub-needs";
|
|
899
1084
|
}
|
|
900
1085
|
|
|
1086
|
+
// 적용이 새 임시 파일을 만들 폴더에 쓸 수 없으면 그 까닭이다. --check 는 쓰지 않고 권한만 본다.
|
|
1087
|
+
function pinDirWhy() {
|
|
1088
|
+
try { accessSync(dirname(realOr(PINFILE)), FS_CONST.W_OK); return null; }
|
|
1089
|
+
catch (e) { return `지울 수 없음 (쓰지 못함: ${failWhy(e)})`; }
|
|
1090
|
+
}
|
|
1091
|
+
|
|
901
1092
|
// 고정 기록에 남은 옛 URL 칸. 없으면 null 이다. 있으면 그 칸을 뺄 기록(rec)이나 이 도구가 지우지 못하는 까닭(why)이다.
|
|
902
1093
|
// 글에 그 이름이 있어도 최상위 칸이 아니면 없는 것이다. 링크인 기록은 따라가 쓰지 않으므로(사용자가 고친다) 링크에는
|
|
903
1094
|
// rec 를 내지 않는다.
|
|
@@ -905,30 +1096,36 @@ function pinUrlLeftover() {
|
|
|
905
1096
|
let text;
|
|
906
1097
|
try { text = pinRecordText(); }
|
|
907
1098
|
catch (e) { return { why: `확인하지 못함 (읽지 못함: ${failWhy(e)})` }; }
|
|
908
|
-
if (text === null
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
return rec && typeof rec === "object" && "originUrl" in rec ? { rec } : null;
|
|
1099
|
+
if (text === null) return null;
|
|
1100
|
+
const { ok, rec } = parseRecord(text);
|
|
1101
|
+
if (ok ? !hasUrlKey(rec) : !text.includes("originUrl")) return null;
|
|
1102
|
+
if (isLink(PINFILE)) return { why: "지울 수 없음 (링크라 따라가 쓰지 않음)" };
|
|
1103
|
+
return ok ? { rec } : { why: "지울 수 없음 (JSON 이 아님)" };
|
|
914
1104
|
}
|
|
915
1105
|
|
|
916
|
-
//
|
|
917
|
-
|
|
1106
|
+
// 기록의 글을 JSON 으로 읽는다. 읽히면 ok 가 true 다. 최상위 칸만 본다: 글에 그 이름이 있어도 칸이 아니면 없는 것이다.
|
|
1107
|
+
function parseRecord(text) {
|
|
1108
|
+
try { return { ok: true, rec: JSON.parse(text) }; }
|
|
1109
|
+
catch { return { ok: false }; }
|
|
1110
|
+
}
|
|
1111
|
+
const hasUrlKey = (rec) => !!rec && typeof rec === "object" && "originUrl" in rec;
|
|
1112
|
+
|
|
1113
|
+
// 고정 기록의 글. 없거나 일반 파일로 이어지지 않으면(폴더, FIFO, 소켓, 끊어진 링크, 링크 고리) null 이다. 그런 자리에는
|
|
1114
|
+
// URL 이 들 수 없다. 읽지 못하면 던진다. stat 으로 먼저 걸러야 소켓에서 열기 오류(ENXIO)가 나지 않고, 연 뒤 다시 보아야
|
|
1115
|
+
// stat 과 읽기 사이에 FIFO 로 바뀐 자리에서 멈추지 않는다.
|
|
918
1116
|
function pinRecordText() {
|
|
919
1117
|
try {
|
|
920
|
-
|
|
1118
|
+
return statSync(PINFILE).isFile() ? readRegular(PINFILE, "utf8") : null;
|
|
921
1119
|
} catch (e) {
|
|
922
|
-
if (["ENOENT", "ENOTDIR", "ELOOP"].includes(e.code)) return null;
|
|
1120
|
+
if (["ENOENT", "ENOTDIR", "ELOOP", "ENOTFILE"].includes(e.code)) return null;
|
|
923
1121
|
throw e;
|
|
924
1122
|
}
|
|
925
|
-
return readFileSync(PINFILE, "utf8");
|
|
926
1123
|
}
|
|
927
1124
|
|
|
928
1125
|
// 옛 URL 칸이 남았을 수 있음을 알린다. 인증 정보가 든 URL 이 남을 수 있어 적용과 되돌리기는 실패로 센다.
|
|
929
|
-
// 기록의 글은 내지 않는다.
|
|
930
|
-
function pinUrlLeft(what) {
|
|
931
|
-
bad(`고정 기록(PINNED)의 옛 URL 칸을 ${what}.\n 인증 정보가 든 URL 이 남았을 수 있습니다: ${oneLine(PINFILE)}`);
|
|
1126
|
+
// 기록의 글은 내지 않는다. more 는 경로 줄 뒤에 붙일 줄이다.
|
|
1127
|
+
function pinUrlLeft(what, more = "") {
|
|
1128
|
+
bad(`고정 기록(PINNED)의 옛 URL 칸을 ${what}.\n 인증 정보가 든 URL 이 남았을 수 있습니다: ${oneLine(PINFILE)}${more}`);
|
|
932
1129
|
return false;
|
|
933
1130
|
}
|
|
934
1131
|
|
|
@@ -943,7 +1140,7 @@ function unpinMarketplace(url) {
|
|
|
943
1140
|
try { git(["remote", "set-url", "--", "origin", restored]); }
|
|
944
1141
|
catch (e) {
|
|
945
1142
|
bad(`고정 해제 실패: origin 을 되돌리지 못했습니다 (${gitError(e)})`);
|
|
946
|
-
log(` 원래 URL:
|
|
1143
|
+
log(` 원래 URL(호스트까지만): ${oneLine(hostOnly(restored))}. 원문은 origin 의 omc-pinned:// 뒤에 그대로 있습니다.`);
|
|
947
1144
|
return "unpin-failed";
|
|
948
1145
|
}
|
|
949
1146
|
return "unpinned";
|
|
@@ -958,13 +1155,14 @@ function dropPinRecord() {
|
|
|
958
1155
|
if (left) return pinUrlLeft(left.why);
|
|
959
1156
|
let saved = null;
|
|
960
1157
|
try { saved = JSON.parse(pinRecordText()); } catch { /* 깨졌거나 읽지 못하면 무시 */ }
|
|
1158
|
+
const ino = hasUrlKey(saved) ? sharedIno() : null;
|
|
961
1159
|
try { unlinkSync(PINFILE); }
|
|
962
1160
|
catch (e) {
|
|
963
1161
|
bad(`고정 기록(PINNED)을 지우지 못함 (${failWhy(e)}): ${oneLine(PINFILE)}`);
|
|
964
1162
|
return false;
|
|
965
1163
|
}
|
|
966
1164
|
warnForeignPin(saved);
|
|
967
|
-
return true;
|
|
1165
|
+
return ino === null ? true : sharedLeft(SHARED_WHY, ino);
|
|
968
1166
|
}
|
|
969
1167
|
|
|
970
1168
|
// 고정 기록 자리에 무엇이 있는가(끊어진 링크 포함). 위 폴더를 열 수 없어 확인하지 못하면 있는 것으로 본다.
|
|
@@ -987,7 +1185,7 @@ const OMC_NAMES = [[".claude-plugin/plugin.json", "oh-my-claudecode"], ["package
|
|
|
987
1185
|
function otherPluginName(root) {
|
|
988
1186
|
for (const [rel, want] of OMC_NAMES) {
|
|
989
1187
|
let name;
|
|
990
|
-
try { name = JSON.parse(
|
|
1188
|
+
try { name = JSON.parse(readRegular(join(root, rel), "utf8")).name; } catch { continue; }
|
|
991
1189
|
if (name === undefined) continue;
|
|
992
1190
|
if (name === want) return null;
|
|
993
1191
|
// 파일에서 읽은 값이다. JSON.stringify 는 U+2028, U+202E 같은 글자를 그대로 두므로 oneLine 을 거친다.
|
|
@@ -1040,7 +1238,7 @@ if (SCAN_AT >= 0) {
|
|
|
1040
1238
|
const files = args.slice(SCAN_AT + 1);
|
|
1041
1239
|
if (!files.length) { console.error("--scan 뒤에 파일 경로가 필요합니다."); process.exit(1); }
|
|
1042
1240
|
const out = files.map((file) => {
|
|
1043
|
-
try { return { file, ...publicFacts(scanFacts(
|
|
1241
|
+
try { return { file, ...publicFacts(scanFacts(readRegular(file, "utf8"))) }; }
|
|
1044
1242
|
catch (e) { return { file, error: String(e.code || e.message) }; }
|
|
1045
1243
|
});
|
|
1046
1244
|
writeSync(1, JSON.stringify(out) + "\n");
|
|
@@ -1139,7 +1337,10 @@ function reportNoBackup(v, list, what) {
|
|
|
1139
1337
|
bad(`${oneLine(v)}: ${what} — ${list.map(shown).join(", ")}`);
|
|
1140
1338
|
log(` 백업(.omcbak)이 없어 되돌릴 원본이 디스크에 없습니다. 원본이 필요하면:`);
|
|
1141
1339
|
if (ROOT) log(` 같은 판의 상류 원본에서 scripts/<이름>.mjs 를 다시 가져오십시오.`);
|
|
1142
|
-
else
|
|
1340
|
+
else {
|
|
1341
|
+
log(` git -C "${oneLine(MARKET)}" archive -o "<임시 tar>" v${oneLine(v)} scripts/<이름>.mjs`);
|
|
1342
|
+
log(` tar -xf "<임시 tar>" -C "${oneLine(join(CACHE, v))}"`);
|
|
1343
|
+
}
|
|
1143
1344
|
}
|
|
1144
1345
|
for (const [v, list] of Object.entries(foreignBy)) reportNoBackup(v, list, "이 도구가 만들지 않은 수정이 남아 있습니다");
|
|
1145
1346
|
for (const [v, list] of Object.entries(orphanBy)) reportNoBackup(v, list, "패치는 있는데 백업이 없어 되돌리지 못했습니다");
|
|
@@ -1151,8 +1352,9 @@ const hjFailBy = {};
|
|
|
1151
1352
|
for (const v of targets) {
|
|
1152
1353
|
const root = rootOf(v);
|
|
1153
1354
|
if (!existsSync(root)) continue;
|
|
1154
|
-
const hj = patchHooksJson(root);
|
|
1155
1355
|
const tag = targets.length > 1 ? `${v}: ` : "";
|
|
1356
|
+
hjTag = tag;
|
|
1357
|
+
const hj = patchHooksJson(root);
|
|
1156
1358
|
if (Array.isArray(hj)) ok(`${tag}제거: ${hj.join(", ")}`);
|
|
1157
1359
|
else if (hj === "already") ok(`${tag}이미 제거됨`);
|
|
1158
1360
|
else if (hj === "needs") hjNeedBy[v] = true;
|
|
@@ -1175,7 +1377,7 @@ if (ROOT) {
|
|
|
1175
1377
|
// - DISABLE_AUTOUPDATER=1 : 확실하지만 전역(다른 마켓플레이스도 멈춤)
|
|
1176
1378
|
// - /plugin UI 의 마켓플레이스별 토글 : omc 만 끄지만 UI 상태라 파일로 확인 불가
|
|
1177
1379
|
try {
|
|
1178
|
-
const st = JSON.parse(
|
|
1380
|
+
const st = JSON.parse(readRegular(join(CFG, "settings.json"), "utf8"));
|
|
1179
1381
|
const off = st?.env?.DISABLE_AUTOUPDATER;
|
|
1180
1382
|
log(` DISABLE_AUTOUPDATER: ${off ? `${oneLine(off)} (전역 차단 중)` : "미설정 (다른 마켓플레이스는 계속 자동 업데이트)"}`);
|
|
1181
1383
|
} catch { /* settings.json 이 없거나 읽을 수 없으면 표시만 생략 */ }
|
|
@@ -1190,18 +1392,24 @@ if (ROOT) {
|
|
|
1190
1392
|
"no-git": () => bad("마켓플레이스 폴더가 git 저장소가 아님 — 고정하지 않음"),
|
|
1191
1393
|
"no-remote": () => ok("origin remote 없음 — 이미 fetch 불가"),
|
|
1192
1394
|
"pin-failed": () => {},
|
|
1395
|
+
"origin-unreadable": () => {},
|
|
1193
1396
|
"unpin-failed": () => {},
|
|
1194
1397
|
"record-failed": () => {},
|
|
1195
1398
|
"scrub-failed": () => ok("이미 고정됨"),
|
|
1196
1399
|
"scrub-needs": () => ok("이미 고정됨"),
|
|
1197
1400
|
"scrub-blocked": () => ok("이미 고정됨"),
|
|
1198
1401
|
}[pin] || (() => {}))();
|
|
1199
|
-
|
|
1402
|
+
// 마켓플레이스를 못 찾아 고정하지 않은 PC 에도 옛 판이 남긴 기록은 있을 수 있다. 같은 판정으로 다룬다.
|
|
1403
|
+
if (MODE !== "revert" && ["no-marketplace", "no-git", "no-remote"].includes(pin)) {
|
|
1404
|
+
const rec = alreadyPinned();
|
|
1405
|
+
if (rec !== "already") pin = rec;
|
|
1406
|
+
}
|
|
1407
|
+
if (["pin-failed", "origin-unreadable", "unpin-failed", "record-failed", "scrub-failed"].includes(pin)) {
|
|
1200
1408
|
r.fail++;
|
|
1201
1409
|
failed.add(["record-failed", "scrub-failed"].includes(pin) ? "PINNED" : "origin");
|
|
1202
1410
|
}
|
|
1203
1411
|
// 되돌리기는 origin 을 다룬 뒤 고정 기록을 지운다. 지우지 못하면 origin 과 따로 실패로 센다.
|
|
1204
|
-
if (
|
|
1412
|
+
if (MODE === "revert" && !dropPinRecord()) {
|
|
1205
1413
|
r.fail++;
|
|
1206
1414
|
failed.add("PINNED");
|
|
1207
1415
|
}
|
|
@@ -1216,15 +1424,26 @@ if (MODE === "check") {
|
|
|
1216
1424
|
// 그 안내를 먼저 낸다(되돌린 뒤 적용하면 «패치 필요» 항목도 함께 처리된다).
|
|
1217
1425
|
const misplacedActive = misplacedBy[active] || 0;
|
|
1218
1426
|
const pinNeeds = ["needs", "scrub-needs", "scrub-blocked"].includes(pin);
|
|
1219
|
-
const
|
|
1427
|
+
const pinUnread = pin === "origin-unreadable";
|
|
1428
|
+
const clean = (needBy[active] || 0) === 0 && !hjNeedBy[active] && !hjFailBy[active] && !pinNeeds && !pinUnread && misplacedActive === 0;
|
|
1429
|
+
// 막힘 줄이 결과가 되는 때는 적용으로 고칠 항목이 없고 막힘만 남은 때다. 고칠 항목이 함께 있으면 그 항목이 결과이고
|
|
1430
|
+
// 막힘은 참고로 알린다(적용은 훅을 패치하고 그 칸은 남겨 실패로 센다).
|
|
1431
|
+
const blockedResult = pin === "scrub-blocked" && (needBy[active] || 0) === 0 && !hjNeedBy[active] && !hjFailBy[active] && !misplacedActive;
|
|
1220
1432
|
const others = targets.filter((v) => v !== active && ((needBy[v] || 0) > 0 || hjNeedBy[v] || (misplacedBy[v] || 0) > 0));
|
|
1221
1433
|
log(clean ? "결과: 모두 적용된 상태입니다."
|
|
1222
1434
|
: hjFailBy[active] ? "결과: hooks.json 을 확인하지 못했습니다. 위의 [!] 줄을 확인하십시오."
|
|
1435
|
+
: pinUnread ? "결과: 마켓플레이스 origin 을 확인하지 못했습니다. 위의 [!] 줄을 확인하십시오."
|
|
1223
1436
|
: misplacedActive ? `결과: 헬퍼 자리 결함 ${misplacedActive}건이 있습니다. ${misplacedHint()}`
|
|
1224
|
-
:
|
|
1437
|
+
: blockedResult ? "결과: 고정 기록(PINNED)의 옛 URL 칸이 이 도구로 지울 수 없는 곳에 있습니다. 위의 [!] 줄을 확인하십시오."
|
|
1225
1438
|
: ROOT ? "결과: 적용되지 않은 항목이 있습니다. --check 없이 같은 --root 로 다시 실행하십시오."
|
|
1226
1439
|
: NO_UPDATE ? "결과: 적용되지 않은 항목이 있습니다. --check 없이 --no-update 로 다시 실행하십시오."
|
|
1227
1440
|
: "결과: 적용되지 않은 항목이 있습니다. 인자 없이 다시 실행하십시오.");
|
|
1441
|
+
if (pin === "scrub-blocked" && !blockedResult) {
|
|
1442
|
+
log(" 참고: 고정 기록(PINNED)의 옛 URL 칸이 이 도구로 지울 수 없는 곳에 있습니다. 위의 [!] 줄을 확인하십시오.");
|
|
1443
|
+
log(" 적용해도 그곳의 칸은 남고, 적용은 이 항목을 실패로 세어 종료 1 로 끝납니다.");
|
|
1444
|
+
}
|
|
1445
|
+
const unreadable = targets.filter((v) => v !== active && hjFailBy[v]);
|
|
1446
|
+
if (unreadable.length) log(` 참고: 비활성 버전 ${unreadable.map(oneLine).join(", ")} 의 hooks.json 을 확인하지 못했습니다. 판정에는 넣지 않았습니다.`);
|
|
1228
1447
|
if (others.length) {
|
|
1229
1448
|
log(` 참고: 비활성 버전 ${others.map(oneLine).join(", ")} 에 미적용분이 있습니다.`);
|
|
1230
1449
|
log(" 지금 쓰이지 않는 버전이라 판정에 넣지 않았습니다. 그 버전으로 되돌릴 계획이면 함께 적용하십시오.");
|