@kaydash9999/banker-plugins 0.16.2 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +44 -0
- package/README.md +15 -10
- package/bin/banker.js +2 -2
- package/codex/manifest.json +2 -1
- package/codex/transform-matrix.md +2 -1
- package/commands/setup.md +2 -2
- package/hooks/progress-plan.mjs +716 -0
- package/hooks/progress.mjs +378 -34
- package/hooks/register.mjs +24 -1
- package/package.json +1 -1
- package/skills/3d-intro-build/references/assemble.mjs +10 -2
- package/skills/3d-intro-build/references/curate.mjs +10 -2
- package/skills/3d-intro-build/references/preview-lib.mjs +2 -0
- package/skills/3d-intro-build/references/serve.mjs +10 -2
- package/skills/3d-intro-build/references/verify-intro.mjs +10 -2
- package/skills/motion-graphic-make/references/curate.mjs +10 -2
- package/skills/motion-graphic-make/references/preview-lib.mjs +2 -0
- package/skills/motion-graphic-make/references/serve.mjs +10 -2
- package/skills/obsidizer/obsidize.mjs +10 -2
- package/skills/omc-patch/SKILL.md +96 -21
- package/skills/omc-patch/scripts/omc-patch.mjs +164 -62
- package/skills/omc-patch/scripts/probe-pm.mjs +192 -0
- package/skills/ultra-chain/SKILL.md +475 -0
- package/skills/ultra-chain/references/capture.mjs +232 -0
- package/skills/ultra-chain/references/chain-ledger.mjs +491 -0
- package/skills/ultra-chain/references/chain-report.mjs +257 -0
- package/skills/ultra-chain/references/preview-lib.mjs +258 -0
- package/skills/ultra-chain/references/serve.mjs +139 -0
- package/skills/vertical-pptx/verify_a4p.mjs +10 -2
|
@@ -2,27 +2,33 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* OMC 패치 — 버전 고정 + 훅 결함 수정 + 자동 업데이트 차단.
|
|
4
4
|
*
|
|
5
|
-
* node omc-patch.mjs 업데이트(5.
|
|
5
|
+
* node omc-patch.mjs 업데이트(5.7.0) + 패치 + 고정
|
|
6
6
|
* node omc-patch.mjs --check 진단만 (아무것도 바꾸지 않음)
|
|
7
7
|
* node omc-patch.mjs --revert 패치·고정 되돌리기
|
|
8
8
|
* node omc-patch.mjs --no-update 버전은 그대로 두고 패치만
|
|
9
|
+
* node omc-patch.mjs --root <플러그인 폴더> [--check | --revert]
|
|
10
|
+
* 그 폴더의 hooks/hooks.json 과 scripts/ 만 다룬다(번들 사본용).
|
|
11
|
+
* installed_plugins.json, 마켓플레이스 origin, 판은 건드리지 않는다.
|
|
12
|
+
* node omc-patch.mjs --scan <파일>...
|
|
13
|
+
* 헬퍼 선언의 블록 깊이와 래핑 자리를 JSON 으로 낸다(읽기 전용).
|
|
9
14
|
*
|
|
10
15
|
* 활성 버전은 installed_plugins.json 의 oh-my-claudecode@omc 항목(version, installPath)에서
|
|
11
|
-
* 읽는다. 캐시에
|
|
16
|
+
* 읽는다. 캐시에 TARGET_VERSION 이 있다고 그것을 활성으로 보지 않는다(--no-update 는 활성 버전을 패치).
|
|
12
17
|
* OMC_PATCH_INSTALLED=<json 경로> 로 읽을 파일을 바꿀 수 있다(시험용, --check 와 함께).
|
|
13
18
|
*
|
|
14
19
|
* OS: Windows / macOS / Linux 공통. 런처는 apply.cmd(Windows), apply.sh(그 외).
|
|
15
20
|
* 배경과 근거는 같은 폴더의 README.md.
|
|
16
21
|
*/
|
|
17
22
|
import {
|
|
18
|
-
readFileSync, writeFileSync, existsSync, copyFileSync, readdirSync,
|
|
23
|
+
readFileSync, writeFileSync, writeSync, existsSync, copyFileSync, readdirSync,
|
|
19
24
|
unlinkSync, mkdirSync, rmSync, symlinkSync,
|
|
20
25
|
} from "node:fs";
|
|
21
|
-
import { join, dirname } from "node:path";
|
|
26
|
+
import { join, dirname, resolve } from "node:path";
|
|
22
27
|
import { homedir, hostname } from "node:os";
|
|
23
28
|
import { execFileSync } from "node:child_process";
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
// 인자 없는 apply 가 맞춰 놓는 판. banker 의 omc-patch 스킬은 늘 --no-update 로 돌려 이 값으로 판을 바꾸지 않는다.
|
|
31
|
+
const TARGET_VERSION = "5.7.0";
|
|
26
32
|
// 3초는 좁았다. 이 예산은 «정상이지만 느린» 로드까지 잘라내는데, 파일 스캔이 8.2배 느려진
|
|
27
33
|
// 이 PC 가 정확히 그 조건이다(실측: 4초 지연에서 session-end 가 조용히 건너뛰어졌다).
|
|
28
34
|
// 대안이 «무한 정지» 인 자리에만 걸리므로 넉넉히 잡는다.
|
|
@@ -36,9 +42,16 @@ const MARKETPLACE = "omc";
|
|
|
36
42
|
const PLUGIN_KEY = "oh-my-claudecode@omc";
|
|
37
43
|
const REPO_URL = "https://github.com/Yeachan-Heo/oh-my-claudecode.git";
|
|
38
44
|
|
|
39
|
-
const
|
|
45
|
+
const args = process.argv.slice(2);
|
|
46
|
+
const argv = new Set(args);
|
|
40
47
|
const MODE = argv.has("--revert") ? "revert" : argv.has("--check") ? "check" : "apply";
|
|
41
48
|
const NO_UPDATE = argv.has("--no-update");
|
|
49
|
+
// --root <플러그인 폴더>: 이 PC 의 설치 상태와 상관없이 그 폴더 하나만 고친다. 번들 사본처럼
|
|
50
|
+
// 판 폴더도 installed_plugins.json 도 없는 트리에 쓴다. 값이 없거나 다음 플래그면 멈춘다.
|
|
51
|
+
const ROOT_AT = args.indexOf("--root");
|
|
52
|
+
const ROOT_ARG = ROOT_AT >= 0 ? args[ROOT_AT + 1] : null;
|
|
53
|
+
const ROOT = ROOT_ARG && !ROOT_ARG.startsWith("--") ? resolve(ROOT_ARG) : null;
|
|
54
|
+
const SCAN_AT = args.indexOf("--scan");
|
|
42
55
|
|
|
43
56
|
const CFG = join(homedir(), ".claude");
|
|
44
57
|
const PLUGINS = join(CFG, "plugins");
|
|
@@ -194,7 +207,9 @@ const REGEX_PREV_CH = new Set("(,=:[!&|?{};+-*%~^".split(""));
|
|
|
194
207
|
const REGEX_PREV_KW = new Set(["return", "typeof", "instanceof", "in", "of", "new", "delete",
|
|
195
208
|
"void", "case", "do", "else", "yield", "await"]);
|
|
196
209
|
|
|
197
|
-
|
|
210
|
+
// probe(i, fnStack) 를 주면 코드 글자(문자열·주석·정규식·중괄호가 아닌 자리)마다 부른다.
|
|
211
|
+
// --scan 이 같은 토크나이저로 헬퍼 선언의 깊이를 재는 데 쓴다. 패치 경로는 probe 없이 부른다.
|
|
212
|
+
function scanTopLevelAwaitImports(src, probe) {
|
|
198
213
|
const n = src.length;
|
|
199
214
|
const fnStack = [];
|
|
200
215
|
const hits = [];
|
|
@@ -285,14 +300,20 @@ function scanTopLevelAwaitImports(src) {
|
|
|
285
300
|
if (c === "a" && !(i > 0 && ID_CH.test(src[i - 1])) && /^await\s+import\s*\(/.test(src.slice(i, i + 40))) {
|
|
286
301
|
if (!fnStack.some(Boolean)) hits.push(i);
|
|
287
302
|
}
|
|
303
|
+
if (probe) probe(i, fnStack);
|
|
288
304
|
prev = c; prevIdx = i; i++;
|
|
289
305
|
}
|
|
290
306
|
return hits;
|
|
291
307
|
}
|
|
292
308
|
|
|
293
309
|
// 자리별 상태. 이미 손으로 Promise.race 를 두른 자리는 건드리지 않는다(4.15.7 에 그런 것이 둘 있다).
|
|
310
|
+
// 이미 감싼 파일이라도 헬퍼 선언이 블록 안이면(옛 판의 자리) «패치됨» 이 아니라 «misplaced» 다.
|
|
311
|
+
// 그 상태를 «패치됨» 으로 넘기면 --check 가 결함 트리에 «모두 적용» 을 낸다(2026-10-12 실측).
|
|
294
312
|
function siteInfo(src) {
|
|
295
|
-
if (src.includes("__omcRaceImport"))
|
|
313
|
+
if (src.includes("__omcRaceImport")) {
|
|
314
|
+
const decl = scanFacts(src).helperDecls.find((d) => d.depth !== 0);
|
|
315
|
+
return decl ? { state: "misplaced", sites: [], decl } : { state: "patched", sites: [] };
|
|
316
|
+
}
|
|
296
317
|
const all = scanTopLevelAwaitImports(src);
|
|
297
318
|
if (!all.length) return { state: "n/a", sites: [] };
|
|
298
319
|
const open = all.filter((p) => !/Promise\.race/.test(src.slice(Math.max(0, p - 300), p)));
|
|
@@ -300,8 +321,6 @@ function siteInfo(src) {
|
|
|
300
321
|
return { state: "needs", sites: open };
|
|
301
322
|
}
|
|
302
323
|
|
|
303
|
-
const needsPatch = (src) => siteInfo(src).state;
|
|
304
|
-
|
|
305
324
|
function transform(src) {
|
|
306
325
|
const info = siteInfo(src);
|
|
307
326
|
if (info.state !== "needs") return null;
|
|
@@ -332,6 +351,28 @@ function transform(src) {
|
|
|
332
351
|
return out.slice(0, ls) + HELPER + out.slice(ls);
|
|
333
352
|
}
|
|
334
353
|
|
|
354
|
+
// ── 읽기 전용 진단 (--scan) ──────────────────────────────────────────────────
|
|
355
|
+
// 패치가 «제자리» 에 앉았는지를 위 토크나이저 하나로 잰다. 시험이 토크나이저를 따로 가지면
|
|
356
|
+
// 패치가 고르는 자리와 시험이 재는 자리가 서로 다른 규칙을 따르게 된다.
|
|
357
|
+
// helperDecls 헬퍼 선언마다 { line, depth }. depth 는 둘러싼 중괄호 수(함수 몸통과 블록).
|
|
358
|
+
// 0 이어야 최상위 문장이다. 블록 안 const 는 블록 범위라 다른 블록의 자리에서
|
|
359
|
+
// ReferenceError 가 나고, 훅의 catch 가 그것을 삼킨다(옛 자리 결함).
|
|
360
|
+
// wrapped `await __omcRaceImport(` 자리마다 { line, inFunction }. 패치는 최상위 자리만 감싼다.
|
|
361
|
+
// unwrapped 아직 감싸지지 않은 최상위 `await import(` 의 줄.
|
|
362
|
+
function scanFacts(src) {
|
|
363
|
+
const lineOf = (at) => src.slice(0, at).split("\n").length;
|
|
364
|
+
const helperDecls = [], wrapped = [];
|
|
365
|
+
const unwrapped = scanTopLevelAwaitImports(src, (i, fnStack) => {
|
|
366
|
+
const ch = src[i];
|
|
367
|
+
if (ch !== "c" && ch !== "l" && ch !== "v" && ch !== "f" && ch !== "a") return;
|
|
368
|
+
if (i > 0 && ID_CH.test(src[i - 1])) return;
|
|
369
|
+
const head = src.slice(i, i + 48);
|
|
370
|
+
if (/^(?:const|let|var|function)\s+__omcRaceImport\b/.test(head)) helperDecls.push({ line: lineOf(i), depth: fnStack.length });
|
|
371
|
+
else if (/^await\s+__omcRaceImport\s*\(/.test(head)) wrapped.push({ line: lineOf(i), inFunction: fnStack.some(Boolean) });
|
|
372
|
+
}).map(lineOf);
|
|
373
|
+
return { helperDecls, wrapped, unwrapped };
|
|
374
|
+
}
|
|
375
|
+
|
|
335
376
|
function syntaxError(file) {
|
|
336
377
|
try { execFileSync(process.execPath, ["--check", file], { stdio: ["ignore", "ignore", "pipe"] }); return null; }
|
|
337
378
|
catch (e) {
|
|
@@ -346,7 +387,7 @@ const looksHandPatched = (src) =>
|
|
|
346
387
|
!src.includes("__omcRaceImport") && /Promise\.race\s*\(\s*\[?[\s\S]{0,120}?\bimport\s*\(/.test(src);
|
|
347
388
|
|
|
348
389
|
function patchHooks(root) {
|
|
349
|
-
let done = 0, skip = 0, fail = 0, need = 0;
|
|
390
|
+
let done = 0, skip = 0, fail = 0, need = 0, misplaced = 0;
|
|
350
391
|
const foreign = [];
|
|
351
392
|
// revert 는 scripts/ 전체를 훑는다. hooks.json 기반 목록은 두 번 새는데, 실측으로
|
|
352
393
|
// 4.15.7 이 둘 다 겪었다 — PostToolUse 를 지우면 그 스크립트가 목록에서 빠지고(그래서
|
|
@@ -368,8 +409,14 @@ function patchHooks(root) {
|
|
|
368
409
|
if (looksHandPatched(readFileSync(f, "utf8"))) foreign.push(n);
|
|
369
410
|
continue;
|
|
370
411
|
}
|
|
371
|
-
const
|
|
372
|
-
|
|
412
|
+
const info = siteInfo(readFileSync(f, "utf8"));
|
|
413
|
+
// 헬퍼 자리 결함은 check 와 apply 모두 이름을 댄다. 이미 감싼 파일이라 transform 으로는
|
|
414
|
+
// 고칠 수 없으므로 apply 도 건드리지 않고, 되돌린 뒤 다시 적용하라고 안내한다(MISPLACED_HINT).
|
|
415
|
+
if (info.state === "misplaced") {
|
|
416
|
+
bad(`헬퍼 자리 결함: ${n} (헬퍼 선언 :${info.decl.line} 이 블록 안, 깊이 ${info.decl.depth})`);
|
|
417
|
+
misplaced++; continue;
|
|
418
|
+
}
|
|
419
|
+
if (info.state !== "needs") { skip++; continue; }
|
|
373
420
|
if (MODE === "check") { bad(`패치 필요: ${n}`); need++; continue; }
|
|
374
421
|
|
|
375
422
|
const out = transform(readFileSync(f, "utf8"));
|
|
@@ -380,7 +427,7 @@ function patchHooks(root) {
|
|
|
380
427
|
if (err) { copyFileSync(bak, f); bad(`문법오류로 롤백: ${n}\n ${err}`); fail++; continue; }
|
|
381
428
|
done++;
|
|
382
429
|
}
|
|
383
|
-
return { done, skip, fail, need, foreign };
|
|
430
|
+
return { done, skip, fail, need, misplaced, foreign };
|
|
384
431
|
}
|
|
385
432
|
|
|
386
433
|
// ── 3. PostToolUse 훅 제거 ───────────────────────────────────────────────────
|
|
@@ -466,61 +513,104 @@ function pinMarketplace() {
|
|
|
466
513
|
return "pinned";
|
|
467
514
|
}
|
|
468
515
|
|
|
516
|
+
// ── --scan: 사실만 내고 끝낸다 ───────────────────────────────────────────────
|
|
517
|
+
// 아래 main 보다 먼저 끝나야 한다. 이 모드는 installed_plugins.json 도 플러그인 캐시도 읽지 않는다.
|
|
518
|
+
// 결과는 입력 순서대로 JSON 배열 한 줄이다. 동기 쓰기로 내보내야 process.exit 가 잘라먹지 않는다.
|
|
519
|
+
if (SCAN_AT >= 0) {
|
|
520
|
+
const files = args.slice(SCAN_AT + 1);
|
|
521
|
+
if (!files.length) { console.error("--scan 뒤에 파일 경로가 필요합니다."); process.exit(1); }
|
|
522
|
+
const out = files.map((file) => {
|
|
523
|
+
try { return { file, ...scanFacts(readFileSync(file, "utf8")) }; }
|
|
524
|
+
catch (e) { return { file, error: String(e.code || e.message) }; }
|
|
525
|
+
});
|
|
526
|
+
writeSync(1, JSON.stringify(out) + "\n");
|
|
527
|
+
process.exit(out.some((x) => x.error) ? 1 : 0);
|
|
528
|
+
}
|
|
529
|
+
|
|
469
530
|
// ── main ─────────────────────────────────────────────────────────────────────
|
|
470
|
-
|
|
531
|
+
if (ROOT_AT >= 0 && !ROOT) { bad("--root 뒤에 플러그인 폴더 경로가 필요합니다."); process.exit(1); }
|
|
532
|
+
log(ROOT
|
|
533
|
+
? `OMC 패치 (대상 폴더 ${ROOT}) 모드: ${MODE}`
|
|
534
|
+
: `OMC 패치 (대상 ${TARGET_VERSION}) 모드: ${MODE}${NO_UPDATE ? " --no-update" : ""}`);
|
|
471
535
|
log(`플랫폼: ${process.platform} node ${process.version}`);
|
|
472
536
|
log();
|
|
473
537
|
|
|
474
|
-
|
|
538
|
+
// 헬퍼 자리 결함은 이 판의 apply 로 고칠 수 없다(이미 감싼 파일). 원본으로 되돌린 뒤 다시 적용한다.
|
|
539
|
+
const MISPLACED_HINT = ROOT
|
|
540
|
+
? "--revert --root 로 되돌린 뒤 같은 --root 로 다시 적용하십시오."
|
|
541
|
+
: "--revert 로 되돌린 뒤 다시 적용하십시오.";
|
|
542
|
+
|
|
543
|
+
// targets 는 «고칠 플러그인 폴더» 의 이름표 목록이다. 기본은 캐시의 판 번호이고, --root 는
|
|
544
|
+
// 그 폴더 경로 하나다. rootOf 가 이름표를 폴더로 바꾼다.
|
|
545
|
+
let active, targets;
|
|
546
|
+
const rootOf = (v) => (ROOT ? v : join(CACHE, v));
|
|
547
|
+
if (ROOT) {
|
|
548
|
+
// 판 폴더도 installed_plugins.json 도 없는 트리(번들 사본)를 고친다. 판 바꾸기([1/4])와
|
|
549
|
+
// 마켓플레이스 고정([4/4])은 하지 않는다 — 그 둘은 이 PC 의 설정이고 이 폴더의 것이 아니다.
|
|
550
|
+
if (!existsSync(join(ROOT, "hooks", "hooks.json")) || !existsSync(join(ROOT, "scripts"))) {
|
|
551
|
+
bad(`플러그인 폴더가 아님 (hooks/hooks.json 과 scripts/ 가 있어야 함): ${ROOT}`);
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
log("[1/4] 버전");
|
|
555
|
+
log(" 건너뜀 (--root 는 판을 바꾸지 않음)");
|
|
556
|
+
active = ROOT;
|
|
557
|
+
targets = [ROOT];
|
|
558
|
+
} else {
|
|
559
|
+
if (!existsSync(INSTALLED)) { bad(`Claude Code 플러그인 설정을 찾을 수 없음: ${INSTALLED}`); process.exit(1); }
|
|
475
560
|
|
|
476
|
-
log("[1/4] 버전");
|
|
477
|
-
if (NO_UPDATE) log(" 건너뜀 (--no-update)");
|
|
478
|
-
else if (MODE === "revert") log(" 건너뜀 (revert 는 버전을 되돌리지 않음 — README 의 롤백 절차 참고)");
|
|
479
|
-
else if (!ensureVersion() && MODE !== "check") { bad("버전 확보 실패 — 중단"); process.exit(1); }
|
|
561
|
+
log("[1/4] 버전");
|
|
562
|
+
if (NO_UPDATE) log(" 건너뜀 (--no-update)");
|
|
563
|
+
else if (MODE === "revert") log(" 건너뜀 (revert 는 버전을 되돌리지 않음 — README 의 롤백 절차 참고)");
|
|
564
|
+
else if (!ensureVersion() && MODE !== "check") { bad("버전 확보 실패 — 중단"); process.exit(1); }
|
|
480
565
|
|
|
481
|
-
const versions = installedVersions();
|
|
482
|
-
// [1/4] 뒤에 계산해야 일반 apply 가 TARGET_VERSION 으로 전환한 결과가 반영된다.
|
|
483
|
-
const { v
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
566
|
+
const versions = installedVersions();
|
|
567
|
+
// [1/4] 뒤에 계산해야 일반 apply 가 TARGET_VERSION 으로 전환한 결과가 반영된다.
|
|
568
|
+
const { v, src: activeSrc } = activeVersion();
|
|
569
|
+
active = v;
|
|
570
|
+
if (MODE !== "revert" && (MODE === "check" || NO_UPDATE)) {
|
|
571
|
+
log(` 활성 버전: ${active} (${activeSrc})`);
|
|
572
|
+
if (active !== TARGET_VERSION) {
|
|
573
|
+
log(` 활성 버전이 대상(${TARGET_VERSION})과 다름 — 기본 apply 는 ${TARGET_VERSION} 로 전환하고, --no-update 는 활성 버전(${active})을 그 자리에서 패치함`);
|
|
574
|
+
}
|
|
488
575
|
}
|
|
576
|
+
// apply 는 활성 버전만 손댄다. revert 와 check 는 설치된 모든 버전을 훑는다 —
|
|
577
|
+
// 여러 버전이 나란히 있을 때 이전 버전에 수정이 남으면, 롤백했을 때 예상과 다른
|
|
578
|
+
// 상태가 된다(실제로 4.15.7 의 hooks.json 수정이 그렇게 남았다).
|
|
579
|
+
targets = MODE === "apply" ? [active] : (versions.length ? versions : [active]);
|
|
580
|
+
if (!existsSync(join(CACHE, active))) { bad(`설치 경로 없음: ${join(CACHE, active)}`); process.exit(1); }
|
|
489
581
|
}
|
|
490
|
-
// apply 는 활성 버전만 손댄다. revert 와 check 는 설치된 모든 버전을 훑는다 —
|
|
491
|
-
// 여러 버전이 나란히 있을 때 이전 버전에 수정이 남으면, 롤백했을 때 예상과 다른
|
|
492
|
-
// 상태가 된다(실제로 4.15.7 의 hooks.json 수정이 그렇게 남았다).
|
|
493
|
-
const targets = MODE === "apply" ? [active] : (versions.length ? versions : [active]);
|
|
494
|
-
if (!existsSync(join(CACHE, active))) { bad(`설치 경로 없음: ${join(CACHE, active)}`); process.exit(1); }
|
|
495
582
|
|
|
496
583
|
log();
|
|
497
|
-
log(`[2/4] 훅 패치${MODE === "apply" ? ` (${active})` : ` (대상 ${targets.length}개 버전)`}`);
|
|
498
|
-
const r = { done: 0, skip: 0, fail: 0, need: 0 };
|
|
584
|
+
log(`[2/4] 훅 패치${MODE === "apply" || ROOT ? ` (${active})` : ` (대상 ${targets.length}개 버전)`}`);
|
|
585
|
+
const r = { done: 0, skip: 0, fail: 0, need: 0, misplaced: 0 };
|
|
499
586
|
const needBy = {};
|
|
587
|
+
const misplacedBy = {};
|
|
500
588
|
const foreignBy = {};
|
|
501
589
|
for (const v of targets) {
|
|
502
|
-
const root =
|
|
590
|
+
const root = rootOf(v);
|
|
503
591
|
if (!existsSync(root)) continue;
|
|
504
592
|
if (targets.length > 1) log(` — ${v}`);
|
|
505
593
|
const x = patchHooks(root);
|
|
506
594
|
needBy[v] = x.need;
|
|
507
|
-
|
|
595
|
+
misplacedBy[v] = x.misplaced;
|
|
596
|
+
for (const k of ["done", "skip", "fail", "need", "misplaced"]) r[k] += x[k];
|
|
508
597
|
if (x.foreign?.length) foreignBy[v] = x.foreign;
|
|
509
598
|
}
|
|
510
|
-
log(` ${MODE === "check" ? `패치 필요 ${r.need}건` : MODE === "revert" ? `되돌림 ${r.done}건` : `패치 ${r.done}건 / 실패 ${r.fail}건`} / 대상 아님 ${r.skip}건`);
|
|
599
|
+
log(` ${MODE === "check" ? `패치 필요 ${r.need}건` : MODE === "revert" ? `되돌림 ${r.done}건` : `패치 ${r.done}건 / 실패 ${r.fail}건`}${r.misplaced ? ` / 헬퍼 자리 결함 ${r.misplaced}건` : ""} / 대상 아님 ${r.skip}건`);
|
|
511
600
|
// 이 도구가 만들지 않은 수정은 되돌릴 원본이 디스크에 없다. 조용히 넘기면 «완전히
|
|
512
601
|
// 되돌렸다» 는 잘못된 인상을 준다 — 무엇이 남았고 어떻게 되찾는지 정확히 말한다.
|
|
513
602
|
for (const [v, list] of Object.entries(foreignBy)) {
|
|
514
603
|
bad(`${v}: 이 도구가 만들지 않은 수정이 남아 있습니다 — ${list.join(", ")}`);
|
|
515
604
|
log(` 백업(.omcbak)이 없어 되돌릴 원본이 디스크에 없습니다. 원본이 필요하면:`);
|
|
516
|
-
log(`
|
|
605
|
+
if (ROOT) log(` 같은 판의 상류 원본에서 scripts/<이름>.mjs 를 다시 가져오십시오.`);
|
|
606
|
+
else log(` git -C "${MARKET}" archive v${v} scripts/<이름>.mjs | tar -x -C "${join(CACHE, v)}"`);
|
|
517
607
|
}
|
|
518
608
|
|
|
519
609
|
log();
|
|
520
610
|
log("[3/4] PostToolUse 훅 제거");
|
|
521
611
|
const hjNeedBy = {};
|
|
522
612
|
for (const v of targets) {
|
|
523
|
-
const root =
|
|
613
|
+
const root = rootOf(v);
|
|
524
614
|
if (!existsSync(root)) continue;
|
|
525
615
|
const hj = patchHooksJson(root);
|
|
526
616
|
const tag = targets.length > 1 ? `${v}: ` : "";
|
|
@@ -534,35 +624,46 @@ for (const v of targets) {
|
|
|
534
624
|
|
|
535
625
|
log();
|
|
536
626
|
log("[4/4] 자동 업데이트 차단");
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
627
|
+
let pin = "skipped";
|
|
628
|
+
if (ROOT) {
|
|
629
|
+
log(" 건너뜀 (--root 는 마켓플레이스와 settings.json 을 건드리지 않음)");
|
|
630
|
+
} else {
|
|
631
|
+
// Claude Code 는 세션 시작 후 최대 10분 지연으로 마켓플레이스를 자동 fetch 한다.
|
|
632
|
+
// 공식 차단 수단은 두 가지뿐이고 각각 한계가 있어, 현재 상태를 같이 보여준다.
|
|
633
|
+
// - DISABLE_AUTOUPDATER=1 : 확실하지만 전역(다른 마켓플레이스도 멈춤)
|
|
634
|
+
// - /plugin UI 의 마켓플레이스별 토글 : omc 만 끄지만 UI 상태라 파일로 확인 불가
|
|
635
|
+
try {
|
|
636
|
+
const st = JSON.parse(readFileSync(join(CFG, "settings.json"), "utf8"));
|
|
637
|
+
const off = st?.env?.DISABLE_AUTOUPDATER;
|
|
638
|
+
log(` DISABLE_AUTOUPDATER: ${off ? `${off} (전역 차단 중)` : "미설정 (다른 마켓플레이스는 계속 자동 업데이트)"}`);
|
|
639
|
+
} catch { /* settings.json 이 없거나 읽을 수 없으면 표시만 생략 */ }
|
|
640
|
+
pin = pinMarketplace();
|
|
641
|
+
({
|
|
642
|
+
pinned: () => ok("마켓플레이스 고정 (origin fetch 차단)"),
|
|
643
|
+
already: () => ok("이미 고정됨"),
|
|
644
|
+
unpinned: () => ok("고정 해제 (origin 복구)"),
|
|
645
|
+
"not-pinned": () => ok("고정된 적 없음"),
|
|
646
|
+
needs: () => {},
|
|
647
|
+
"no-marketplace": () => bad("마켓플레이스 디렉터리 없음 — 고정 불필요"),
|
|
648
|
+
"no-git": () => bad("마켓플레이스 폴더가 git 저장소가 아님 — 고정하지 않음"),
|
|
649
|
+
"no-remote": () => ok("origin remote 없음 — 이미 fetch 불가"),
|
|
650
|
+
}[pin] || (() => {}))();
|
|
651
|
+
}
|
|
557
652
|
|
|
558
653
|
log();
|
|
559
654
|
if (MODE === "check") {
|
|
560
655
|
// 판정은 «활성 버전» 기준이다. apply 는 활성 버전만 손대므로, 판정만 모든 버전을 보면
|
|
561
656
|
// 캐시에 옛 버전이 남아 있는 한 apply 를 몇 번 돌려도 영원히 «미적용» 으로 나온다
|
|
562
657
|
// (3-OS 에서 동일하게 재현). 비활성 버전은 정보로만 알린다.
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
658
|
+
// 헬퍼 자리 결함이 하나라도 있으면 «모두 적용» 이 아니다. 다시 적용만으로는 안 고쳐지므로
|
|
659
|
+
// 그 안내를 먼저 낸다(되돌린 뒤 적용하면 «패치 필요» 항목도 함께 처리된다).
|
|
660
|
+
const misplacedActive = misplacedBy[active] || 0;
|
|
661
|
+
const clean = (needBy[active] || 0) === 0 && !hjNeedBy[active] && pin !== "needs" && misplacedActive === 0;
|
|
662
|
+
const others = targets.filter((v) => v !== active && ((needBy[v] || 0) > 0 || hjNeedBy[v] || (misplacedBy[v] || 0) > 0));
|
|
663
|
+
log(clean ? "결과: 모두 적용된 상태입니다."
|
|
664
|
+
: misplacedActive ? `결과: 헬퍼 자리 결함 ${misplacedActive}건이 있습니다. ${MISPLACED_HINT}`
|
|
665
|
+
: ROOT ? "결과: 적용되지 않은 항목이 있습니다. --check 없이 같은 --root 로 다시 실행하십시오."
|
|
666
|
+
: "결과: 적용되지 않은 항목이 있습니다. 인자 없이 다시 실행하십시오.");
|
|
566
667
|
if (others.length) {
|
|
567
668
|
log(` 참고: 비활성 버전 ${others.join(", ")} 에 미적용분이 있습니다.`);
|
|
568
669
|
log(" 지금 쓰이지 않는 버전이라 판정에 넣지 않았습니다. 그 버전으로 되돌릴 계획이면 함께 적용하십시오.");
|
|
@@ -571,6 +672,7 @@ if (MODE === "check") {
|
|
|
571
672
|
log("되돌리기 완료. Claude Code 를 재시작해야 반영됩니다.");
|
|
572
673
|
} else {
|
|
573
674
|
log(`완료. 실패 ${r.fail}건.`);
|
|
675
|
+
if (r.misplaced) log(`[!] 헬퍼 자리 결함 ${r.misplaced}건은 고치지 않았습니다. ${MISPLACED_HINT}`);
|
|
574
676
|
log(" 훅 스크립트 패치는 즉시 적용됩니다 (훅이 호출될 때마다 파일을 새로 읽습니다).");
|
|
575
677
|
log("[!] PostToolUse 훅 제거는 hooks.json 변경이라 각 세션이 다시 시작될 때부터 적용됩니다.");
|
|
576
678
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// project-memory probe for /banker:omc-patch. Runs one OMC plugin folder's project-memory-session.mjs the
|
|
3
|
+
// way Claude Code runs it at SessionStart, in a scratch project with its own home and state folder, then
|
|
4
|
+
// checks that the hook really wrote project-memory.json. A hook whose patched import died inside a block
|
|
5
|
+
// still exits 0 and prints {"continue":true}; only the file shows whether it did its work.
|
|
6
|
+
// The user's ~/.claude and the current project's .omc are not touched. Pure Node, no dependencies.
|
|
7
|
+
//
|
|
8
|
+
// node probe-pm.mjs --plugin <OMC plugin folder> [--scratch <new folder>] [--keep] [--timeout <seconds>]
|
|
9
|
+
//
|
|
10
|
+
// Without --scratch the probe works in a new temp folder and removes it at the end (--keep leaves it).
|
|
11
|
+
// A --scratch folder is never reused and is left for inspection.
|
|
12
|
+
// Prints PROBE GREEN or PROBE RED with the reason. Exit 0 green, 1 red, 2 usage (no hook script, the
|
|
13
|
+
// scratch folder already exists, a bad option).
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { dirname, join, resolve } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
const HOOK = join("scripts", "project-memory-session.mjs");
|
|
21
|
+
const PACKAGE = { name: "omc-pm-probe", version: "0.0.0", scripts: { build: "node build.js", test: "node test.js" } };
|
|
22
|
+
const MAX_OUTPUT = 16 * 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
export class UsageError extends Error {}
|
|
25
|
+
|
|
26
|
+
export function parseArgs(argv) {
|
|
27
|
+
const o = { plugin: null, scratch: null, keep: false, timeout: 30 };
|
|
28
|
+
for (let i = 0; i < argv.length; i++) {
|
|
29
|
+
const a = argv[i];
|
|
30
|
+
if (a === "--keep") o.keep = true;
|
|
31
|
+
else if (["--plugin", "--scratch", "--timeout"].includes(a)) {
|
|
32
|
+
const v = argv[++i];
|
|
33
|
+
if (v === undefined || v.startsWith("--")) throw new UsageError(`${a} 뒤에 값이 필요합니다`);
|
|
34
|
+
o[a.slice(2)] = a === "--timeout" ? Number(v) : v;
|
|
35
|
+
} else throw new UsageError(`모르는 인자 ${a}`);
|
|
36
|
+
}
|
|
37
|
+
if (!o.plugin) throw new UsageError("--plugin <OMC 플러그인 폴더> 가 필요합니다");
|
|
38
|
+
if (!(o.timeout > 0)) throw new UsageError("--timeout 은 0 보다 큰 초입니다");
|
|
39
|
+
return o;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Variables of this session the hook must not see. GIT_* goes too: GIT_DIR and its kin would point git
|
|
43
|
+
// past the ceiling below.
|
|
44
|
+
const dropped = (u) => u.startsWith("CLAUDE_") || u.startsWith("OMC_") || u.startsWith("GIT_") || u === "DISABLE_OMC";
|
|
45
|
+
|
|
46
|
+
// The hook sees only what Claude Code would give it, pointed at the scratch tree: no CLAUDE_*, OMC_* or GIT_*
|
|
47
|
+
// variable of this session leaks in, and git stops at the scratch folder. Keys compare in upper case
|
|
48
|
+
// because Windows treats them so.
|
|
49
|
+
export function probeEnv(base, scratch, plugin) {
|
|
50
|
+
const env = {};
|
|
51
|
+
for (const [k, v] of Object.entries(base)) if (!dropped(k.toUpperCase())) env[k] = v;
|
|
52
|
+
const home = join(scratch, "home");
|
|
53
|
+
Object.assign(env, {
|
|
54
|
+
HOME: home, USERPROFILE: home, CLAUDE_CONFIG_DIR: join(home, ".claude"), CLAUDE_PLUGIN_ROOT: plugin,
|
|
55
|
+
OMC_STATE_DIR: join(scratch, "state"), OMC_NOTIFY: "0", GIT_CEILING_DIRECTORIES: scratch,
|
|
56
|
+
});
|
|
57
|
+
return env;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function makeScratch(scratch) {
|
|
61
|
+
const project = join(scratch, "project");
|
|
62
|
+
for (const d of [project, join(scratch, "home", ".claude"), join(scratch, "state")]) mkdirSync(d, { recursive: true });
|
|
63
|
+
writeFileSync(join(project, "package.json"), JSON.stringify(PACKAGE));
|
|
64
|
+
return project;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Every project-memory.json under the scratch folder, wherever the hook chose to put it.
|
|
68
|
+
export function findMemoryFiles(dir, depth = 0) {
|
|
69
|
+
if (depth > 8) return [];
|
|
70
|
+
return readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
|
|
71
|
+
const p = join(dir, e.name);
|
|
72
|
+
if (e.isDirectory()) return findMemoryFiles(p, depth + 1);
|
|
73
|
+
return e.isFile() && e.name === "project-memory.json" ? [p] : [];
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const samePath = (a, b) => {
|
|
78
|
+
const norm = (p) => resolve(String(p)).replace(/[\\/]+$/, "");
|
|
79
|
+
return process.platform === "win32" ? norm(a).toLowerCase() === norm(b).toLowerCase() : norm(a) === norm(b);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const languagesOf = (mem) => (Array.isArray(mem.techStack?.languages) ? mem.techStack.languages : []);
|
|
83
|
+
|
|
84
|
+
// Each check: [passes(mem, project), what failed]. Checked in order; the first failure is the reason.
|
|
85
|
+
const MEMORY_CHECKS = [
|
|
86
|
+
[(mem, project) => samePath(mem.projectRoot, project), (mem) => `projectRoot 가 스크래치 프로젝트가 아님(${mem.projectRoot})`],
|
|
87
|
+
[(mem) => languagesOf(mem).length > 0, () => "techStack.languages 가 비어 있음"],
|
|
88
|
+
[(mem) => mem.build?.scripts?.build === PACKAGE.scripts.build, () => "build.scripts.build 가 package.json 과 다름"],
|
|
89
|
+
[(mem) => mem.lastScanned !== undefined && mem.lastScanned !== null, () => "lastScanned 가 없음"],
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
// The first check that fails, or null when the memory describes the scratch project.
|
|
93
|
+
export function memoryProblem(mem, project) {
|
|
94
|
+
if (!mem || typeof mem !== "object") return "project-memory.json 을 JSON 으로 읽지 못함";
|
|
95
|
+
const failed = MEMORY_CHECKS.find(([passes]) => !passes(mem, project));
|
|
96
|
+
return failed ? failed[1](mem) : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readJson(file) {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function verdict(run, files, project) {
|
|
108
|
+
if (run.error?.code === "ENOBUFS") return "PROBE RED: 훅 출력이 16MB 를 넘음";
|
|
109
|
+
if (run.error?.code === "ETIMEDOUT" || run.signal) return "PROBE RED: 훅이 시간 안에 끝나지 않음";
|
|
110
|
+
if (run.status !== 0) return `PROBE RED: 훅 종료 코드 ${run.status}`;
|
|
111
|
+
if (!files.length) return "PROBE RED: no project-memory.json (훅이 돌았지만 아무 일도 하지 않음)";
|
|
112
|
+
if (files.length > 1) return `PROBE RED: project-memory.json 이 ${files.length}개`;
|
|
113
|
+
const problem = memoryProblem(readJson(files[0]), project);
|
|
114
|
+
return problem ? `PROBE RED: ${problem}` : "PROBE GREEN: 스크래치 프로젝트의 project-memory.json 이 만들어짐";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function probe({ plugin, scratch, timeout }) {
|
|
118
|
+
const script = join(plugin, HOOK);
|
|
119
|
+
const project = makeScratch(scratch);
|
|
120
|
+
const payload = JSON.stringify({ session_id: "omc-patch-probe", transcript_path: join(scratch, "transcript.jsonl"),
|
|
121
|
+
cwd: project, hook_event_name: "SessionStart", source: "startup" });
|
|
122
|
+
const run = spawnSync(process.execPath, [script], { cwd: project, input: payload, env: probeEnv(process.env, scratch, plugin),
|
|
123
|
+
encoding: "utf8", timeout: Math.max(1, Math.ceil(timeout * 1000)), maxBuffer: MAX_OUTPUT, windowsHide: true });
|
|
124
|
+
const files = findMemoryFiles(scratch);
|
|
125
|
+
return { script, run, files, line: verdict(run, files, project) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const one = (s) => String(s || "").replace(/\s+/g, " ").trim().slice(0, 300);
|
|
129
|
+
|
|
130
|
+
// The parsed options with the plugin folder resolved, or a UsageError for what cannot run.
|
|
131
|
+
function prepare(argv) {
|
|
132
|
+
const o = parseArgs(argv);
|
|
133
|
+
const plugin = resolve(o.plugin);
|
|
134
|
+
if (!existsSync(join(plugin, HOOK))) throw new UsageError(`훅 스크립트가 없습니다: ${join(plugin, HOOK)}`);
|
|
135
|
+
return { ...o, plugin };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// A --scratch folder is made here and must not exist yet. A plain mkdir fails on anything already at that
|
|
139
|
+
// path, a symlink included, so the probe never writes through a folder it did not create.
|
|
140
|
+
function claimScratch(dir) {
|
|
141
|
+
mkdirSync(dirname(dir), { recursive: true });
|
|
142
|
+
try {
|
|
143
|
+
mkdirSync(dir, { mode: 0o700 });
|
|
144
|
+
} catch (e) {
|
|
145
|
+
if (e.code === "EEXIST") throw new UsageError(`스크래치 폴더가 이미 있습니다(다시 쓰지 않음): ${dir}`);
|
|
146
|
+
throw e;
|
|
147
|
+
}
|
|
148
|
+
return dir;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function removeTemp(scratch, out) {
|
|
152
|
+
try {
|
|
153
|
+
rmSync(scratch, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
|
|
154
|
+
} catch (e) {
|
|
155
|
+
out.write(`probe-pm: 임시 폴더를 지우지 못함(${e.code || e.message}): ${scratch}\n`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const reportLines = (r, scratch) => [`node ${process.execPath}`, `script ${r.script}`, `scratch ${scratch}`,
|
|
160
|
+
`exit ${r.run.status}${r.run.signal ? ` (signal ${r.run.signal})` : ""}`, `stdout ${one(r.run.stdout)}`,
|
|
161
|
+
`stderr ${one(r.run.stderr)}`, `memfiles ${r.files.length}`, r.line].join("\n");
|
|
162
|
+
|
|
163
|
+
export function main(argv, out = process.stdout) {
|
|
164
|
+
let o;
|
|
165
|
+
let scratch;
|
|
166
|
+
try {
|
|
167
|
+
o = prepare(argv);
|
|
168
|
+
scratch = o.scratch ? claimScratch(resolve(o.scratch)) : mkdtempSync(join(tmpdir(), "omc-pm-probe-"));
|
|
169
|
+
} catch (e) {
|
|
170
|
+
out.write(`probe-pm: ${e.message}\n`);
|
|
171
|
+
return 2;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const r = probe({ plugin: o.plugin, scratch, timeout: o.timeout });
|
|
175
|
+
out.write(`${reportLines(r, scratch)}\n`);
|
|
176
|
+
return r.line.startsWith("PROBE GREEN") ? 0 : 1;
|
|
177
|
+
} finally {
|
|
178
|
+
if (!o.scratch && !o.keep) removeTemp(scratch, out);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Run as a command, also when reached through a symlinked path: Node resolves
|
|
183
|
+
// symlinks for import.meta.url but not for process.argv[1].
|
|
184
|
+
const invokedDirectly = () => {
|
|
185
|
+
try {
|
|
186
|
+
return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
if (invokedDirectly()) process.exitCode = main(process.argv.slice(2));
|