@kaydash9999/banker-plugins 0.20.1 → 0.20.5
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 +195 -1
- package/PRIVACY.md +13 -3
- package/README.md +27 -4
- package/bin/banker.js +84 -40
- package/bin/lib/telemetry-config.mjs +31 -16
- package/codex/manifest.json +1 -1
- package/codex/transform-matrix.md +1 -1
- package/commands/setup.md +23 -0
- package/hooks/progress.mjs +143 -38
- package/hooks/register.mjs +13 -3
- package/hooks/update-checkin.mjs +15 -7
- package/hooks/update-fetch.mjs +15 -9
- package/hooks/update-notify.mjs +2 -9
- package/package.json +1 -1
- package/skills/3d-intro-build/SKILL.md +11 -5
- package/skills/3d-intro-build/references/azure-adapter.mjs +374 -81
- package/skills/3d-intro-build/references/curate.mjs +48 -14
- package/skills/3d-intro-build/references/preview-lib.mjs +51 -15
- package/skills/3d-intro-build/references/prompts.md +4 -4
- package/skills/3d-intro-build/references/serve.mjs +6 -1
- package/skills/3d-intro-build/references/verify-intro.mjs +16 -9
- package/skills/3d-intro-build/references/video-pool.mjs +250 -135
- package/skills/3d-intro-setup/SKILL.md +41 -10
- package/skills/3d-intro-setup/references/azure-adapter.mjs +374 -81
- package/skills/3d-intro-setup/references/video-pool.mjs +250 -135
- package/skills/anti-drm/SKILL.md +6 -3
- package/skills/anti-drm/scripts/anti-drm.mjs +49 -12
- package/skills/anti-drm/scripts/instruction-toggle.mjs +105 -20
- package/skills/audit-mock/SKILL.md +1 -1
- package/skills/audit-security/SKILL.md +64 -2
- package/skills/deep-research/SKILL.md +5 -0
- package/skills/docs-setup/SKILL.md +37 -7
- package/skills/lineage/SKILL.md +12 -12
- package/skills/lineage/lineage.py +399 -300
- package/skills/motion-graphic-make/SKILL.md +1 -1
- package/skills/motion-graphic-make/references/curate.mjs +48 -14
- package/skills/motion-graphic-make/references/preview-lib.mjs +51 -15
- package/skills/motion-graphic-make/references/serve.mjs +6 -1
- package/skills/motion-graphic-setup/SKILL.md +58 -13
- package/skills/obsidizer/SKILL.md +1 -1
- package/skills/obsidizer/obsidize.mjs +8 -1
- package/skills/omc-patch/SKILL.md +80 -23
- package/skills/omc-patch/scripts/omc-patch.mjs +460 -116
- package/skills/payload-mon/SKILL.md +5 -2
- package/skills/payload-mon/scripts/payload-mon.mjs +137 -30
- package/skills/payload-mon/scripts/payload-size.mjs +4 -1
- package/skills/pdf-vision-extract/SKILL.md +11 -6
- package/skills/ralph-qa/SKILL.md +125 -151
- package/skills/ralph-qa/references/gemini-seat.mjs +67 -19
- package/skills/ralph-qa/references/payload-scan.mjs +22 -7
- package/skills/ralph-qa/references/seat-payload.mjs +646 -0
- package/skills/ralph-qa/references/seat-run.mjs +956 -0
- package/skills/ralph-qa/references/seat-spawn.mjs +205 -0
- package/skills/ralph-qa/references/verifier-probe.mjs +57 -16
- package/skills/setup-harness-factory/SKILL.md +17 -1
- package/skills/setup-insane-search/SKILL.md +16 -0
- package/skills/setup-java/SKILL.md +20 -1
- package/skills/setup-lsp/SKILL.md +56 -13
- package/skills/setup-mcp/SKILL.md +73 -4
- package/skills/setup-node/SKILL.md +31 -6
- package/skills/setup-omc/SKILL.md +24 -0
- package/skills/setup-omc-hud/SKILL.md +13 -5
- package/skills/setup-omc-hud/assets/apply-hud.mjs +381 -27
- package/skills/setup-omc-hud/assets/omc-hud-custom.mjs +81 -14
- package/skills/setup-omc-hud/scripts/claude-update-last.mjs +5 -3
- package/skills/setup-playwright/SKILL.md +45 -2
- package/skills/setup-pwsh/SKILL.md +19 -0
- package/skills/setup-python/SKILL.md +45 -7
- package/skills/setup-sandbox/SKILL.md +30 -5
- package/skills/setup-stitch/SKILL.md +22 -0
- package/skills/setup-tmux/SKILL.md +24 -4
- package/skills/tone-compact/SKILL.md +2 -1
- package/skills/tone-compact/scripts/instruction-toggle.mjs +105 -20
- package/skills/ultra-chain/SKILL.md +4 -3
- package/skills/ultra-chain/references/preview-lib.mjs +51 -15
- package/skills/ultra-chain/references/serve.mjs +6 -1
- package/skills/ultra-interview/SKILL.md +1 -0
- package/skills/update-banker/SKILL.md +19 -3
- package/skills/vertical-pptx/SKILL.md +6 -3
- package/skills/vertical-pptx-setup/SKILL.md +76 -20
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* 활성 버전은 installed_plugins.json 의 oh-my-claudecode@omc 항목(version, installPath)에서
|
|
18
18
|
* 읽는다. 캐시에 TARGET_VERSION 이 있다고 그것을 활성으로 보지 않는다(--no-update 는 활성 버전을 패치).
|
|
19
|
-
* OMC_PATCH_INSTALLED=<json 경로> 로 읽을 파일을 바꿀 수 있다(시험용, --check
|
|
19
|
+
* OMC_PATCH_INSTALLED=<json 경로> 로 읽을 파일을 바꿀 수 있다(시험용, --check 에서만 받음).
|
|
20
20
|
*
|
|
21
21
|
* OS: Windows / macOS / Linux 공통. 런처(apply.cmd, apply.sh)와 배경 문서(README.md)는
|
|
22
22
|
* 킷 배포 폴더에만 있다. 이 파일만 옮겨 쓰는 곳(예: 스킬 폴더)에는 없다.
|
|
@@ -61,8 +61,9 @@ const CFG = join(homedir(), ".claude");
|
|
|
61
61
|
const PLUGINS = join(CFG, "plugins");
|
|
62
62
|
const CACHE = join(PLUGINS, "cache", MARKETPLACE, "oh-my-claudecode");
|
|
63
63
|
const MARKET = join(PLUGINS, "marketplaces", MARKETPLACE);
|
|
64
|
-
// OMC_PATCH_INSTALLED: 시험용. installed_plugins.json 대신 읽을 JSON 경로(읽기 전용
|
|
65
|
-
|
|
64
|
+
// OMC_PATCH_INSTALLED: 시험용. --check 일 때만 installed_plugins.json 대신 읽을 JSON 경로(읽기 전용). 적용과 되돌리기는
|
|
65
|
+
// 늘 Claude Code 가 읽는 파일을 본다 — 다른 파일이 정한 판을 패치하거나 그 파일에 쓰지 않게.
|
|
66
|
+
const INSTALLED = (MODE === "check" && process.env.OMC_PATCH_INSTALLED) || join(PLUGINS, "installed_plugins.json");
|
|
66
67
|
const PINFILE = join(CFG, "omc-local-patches", "PINNED");
|
|
67
68
|
|
|
68
69
|
const log = (s = "") => console.log(s);
|
|
@@ -78,7 +79,7 @@ const ok = (s) => console.log(` ok ${s}`);
|
|
|
78
79
|
// 심볼이 undefined 가 되고, 호출부에 이미 있는 null 검사/try-catch 가 no-op
|
|
79
80
|
// 경로를 태운다. 정상 로드 시에는 동작이 전혀 바뀌지 않는다.
|
|
80
81
|
const HELPER = `
|
|
81
|
-
// --- OMC local patch: bound top-level dynamic import (
|
|
82
|
+
// --- OMC local patch: bound top-level dynamic import (made by omc-patch; undo with omc-patch --revert)
|
|
82
83
|
const __omcRaceImport = (p) => {
|
|
83
84
|
let t;
|
|
84
85
|
const timed = new Promise((res) => {
|
|
@@ -87,6 +88,8 @@ const __omcRaceImport = (p) => {
|
|
|
87
88
|
// 뜨지도 못한다(실측: 원본과 패치본이 똑같이 150ms 안에 죽었다). import 가 정상적으로
|
|
88
89
|
// 끝나면 아래에서 clearTimeout 하므로 정상 경로는 한 톨도 느려지지 않는다.
|
|
89
90
|
t = setTimeout(() => {
|
|
91
|
+
// 훅이 일을 건너뛴 것을 알 수 있게 한 줄 남긴다. 종료 코드는 바꾸지 않는다(0 이 아니면 Claude Code 가 훅 오류로 본다). 닫힌 stderr 의 쓰기 오류는 뒤에 error 이벤트로 오므로 먼저 받아 둔다.
|
|
92
|
+
try { process.stderr.on('error', () => {}); process.stderr.write('[OMC local patch] import 가 ${IMPORT_TIMEOUT_MS / 1000}초 안에 끝나지 않아 빈 모듈로 진행합니다\\n'); } catch { /* 알림만 */ }
|
|
90
93
|
const k = setTimeout(() => process.exit(typeof process.exitCode === 'number' ? process.exitCode : 0), ${EXIT_GRACE_MS});
|
|
91
94
|
if (k && k.unref) k.unref();
|
|
92
95
|
// 빈 객체를 돌려주는 것이 중요하다. 호출부 다수가 \`const { a } = await import(...)\`
|
|
@@ -177,6 +180,11 @@ function fetchConfig(env) {
|
|
|
177
180
|
return out;
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
// 업데이트 fetch 만 받는 값. 받는 속도가 초당 1000바이트 밑으로 30초 이어지면 git(curl)이 스스로 끊고, 자격 증명을 터미널에서 묻지 않는다.
|
|
184
|
+
const NET_ENV = { GIT_HTTP_LOW_SPEED_LIMIT: "1000", GIT_HTTP_LOW_SPEED_TIME: "30", GIT_TERMINAL_PROMPT: "0" };
|
|
185
|
+
// fetch 전체의 시간 제한(ms). 응답하지 않는 망(연결을 붙잡는 방화벽, TLS 블랙홀)에서 멈추지 않게 한다. 시험이 줄일 수 있게
|
|
186
|
+
// OMC_PATCH_FETCH_TIMEOUT_MS 를 받되 줄이기만 한다. 0 이하나 숫자가 아니면 버린다. 문서에 적지 않는 시험용 장치다.
|
|
187
|
+
const FETCH_TIMEOUT_MS = Math.min(Number(process.env.OMC_PATCH_FETCH_TIMEOUT_MS) > 0 ? Number(process.env.OMC_PATCH_FETCH_TIMEOUT_MS) : 120000, 120000);
|
|
180
188
|
// 마켓플레이스의 .git 이 깨졌으면(중단된 clone) git 은 위로 올라가 다음 저장소(dotfiles 로 관리하는 ~/.claude 등)를
|
|
181
189
|
// 고른다. 바로 위 폴더를 천장(GIT_CEILING_DIRECTORIES)으로 두어 마켓플레이스 밖으로 나가지 못하게 한다.
|
|
182
190
|
// 마켓플레이스가 링크면 실제 경로 쪽 위 폴더도 막는다. 셸에 있던 천장 값은 대소문자와 상관없이 이 값으로 바꾼다.
|
|
@@ -184,25 +192,66 @@ const gitEnv = ({ network = false } = {}) => {
|
|
|
184
192
|
const vars = gitRepoVars();
|
|
185
193
|
const drop = (k) => isEnvConfig(k) || vars.has(k.toUpperCase()) || k.toUpperCase() === "GIT_CEILING_DIRECTORIES";
|
|
186
194
|
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !drop(k)));
|
|
187
|
-
if (network) Object.assign(env, fetchConfig(process.env));
|
|
195
|
+
if (network) Object.assign(env, fetchConfig(process.env), NET_ENV);
|
|
188
196
|
env.GIT_CEILING_DIRECTORIES = [...new Set([dirname(MARKET), dirname(realOr(MARKET))])].join(PATH_LIST_SEP);
|
|
189
197
|
return env;
|
|
190
198
|
};
|
|
191
199
|
|
|
192
|
-
|
|
193
|
-
|
|
200
|
+
// 저장소의 git 도 홈 폴더에서 돌리고 저장소는 -C 로 준다. Windows 는 이름만 준 프로그램을 일하는 폴더에서 PATH 보다 먼저
|
|
201
|
+
// 찾아, 마켓플레이스(남의 저장소 사본)에 든 git.exe 가 돌 수 있다.
|
|
202
|
+
function git(args, dir = MARKET, opts = {}) {
|
|
203
|
+
return execFileSync("git", ["-C", dir, ...args], { cwd: homedir(), env: gitEnv(opts), encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts.timeout }).trim();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 태그 받기를 띄우는 작은 node 프로세스. execFileSync 의 timeout 은 git 하나만 끝내, git 이 띄운 git remote-http 와
|
|
207
|
+
// git-remote-http 가 연결을 붙든 채 남는다(실측: http 는 curl 의 정체 감지 30초, TLS 는 curl 의 연결 제한 300초까지). 흐름은
|
|
208
|
+
// 동기 그대로 두고 이 프로세스가 git 을 띄워, 시간이 다 되거나 Ctrl+C(SIGINT), SIGTERM, SIGHUP 을 받으면 git 과 그 자식을 함께
|
|
209
|
+
// 끝낸다. POSIX 는 git 을 새 프로세스 묶음으로 띄워 묶음에 SIGTERM(git 이 ref 잠금 파일을 지움), git 이 끝나거나 2초가 지나면
|
|
210
|
+
// SIGKILL. Windows 는 git 이 살아 있을 때 taskkill /T /F 로 트리째 끝낸다. 시간이 다 되면 종료 124 다. git 은 홈 폴더에서
|
|
211
|
+
// -C 로 돈다(git 함수와 같은 까닭). 알려진 한계: 바깥에서 이 도구의 프로세스 묶음에 SIGKILL 을 보내면(`timeout -s KILL` 등)
|
|
212
|
+
// 이 프로세스가 먼저 끝나 새 묶음의 git 이 남는다. Ctrl+C, SIGTERM, Claude Code 의 작업 중지는 git 까지 끝낸다.
|
|
213
|
+
const FETCH_REAPER = `
|
|
214
|
+
const { spawn, spawnSync } = require("node:child_process");
|
|
215
|
+
const { constants } = require("node:os");
|
|
216
|
+
const [, ms, dir, ...args] = process.argv;
|
|
217
|
+
const win = process.platform === "win32";
|
|
218
|
+
const c = spawn("git", ["-C", dir, ...args], { cwd: require("node:os").homedir(), stdio: "inherit", detached: !win, windowsHide: true });
|
|
219
|
+
const group = (sig) => { try { process.kill(-c.pid, sig); } catch { /* the group is gone */ } };
|
|
220
|
+
const taskkill = \`\${process.env.SystemRoot || "C:\\\\Windows"}\\\\System32\\\\taskkill.exe\`;
|
|
221
|
+
const tree = () => spawnSync(taskkill, ["/T", "/F", "/PID", String(c.pid)], { stdio: "ignore", windowsHide: true });
|
|
222
|
+
let code = null;
|
|
223
|
+
const stop = (why) => {
|
|
224
|
+
if (code !== null) return;
|
|
225
|
+
code = why;
|
|
226
|
+
if (win) { tree(); process.exit(code); }
|
|
227
|
+
group("SIGTERM");
|
|
228
|
+
setTimeout(() => { group("SIGKILL"); process.exit(code); }, 2000);
|
|
229
|
+
};
|
|
230
|
+
const t = setTimeout(() => stop(124), Number(ms));
|
|
231
|
+
for (const s of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(s, () => stop(130));
|
|
232
|
+
c.on("error", (e) => { clearTimeout(t); process.stderr.write("error: cannot run git (" + e.code + ")\\n"); process.exit(127); });
|
|
233
|
+
c.on("exit", (status, sig) => {
|
|
234
|
+
clearTimeout(t);
|
|
235
|
+
if (!win) group("SIGKILL");
|
|
236
|
+
process.exit(code ?? status ?? 128 + (constants.signals[sig] || 0));
|
|
237
|
+
});
|
|
238
|
+
`;
|
|
239
|
+
// 바깥 시간 제한은 위 프로세스가 끝내지 못할 때만 쓴다(node 를 띄우는 시간, SIGTERM 뒤 2초, taskkill).
|
|
240
|
+
const REAPER_GRACE_MS = 10000;
|
|
241
|
+
function gitFetch(args) {
|
|
242
|
+
try {
|
|
243
|
+
execFileSync(process.execPath, ["--input-type=commonjs", "-e", FETCH_REAPER, String(FETCH_TIMEOUT_MS), MARKET, ...args],
|
|
244
|
+
{ cwd: homedir(), env: gitEnv({ network: true }), encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: FETCH_TIMEOUT_MS + REAPER_GRACE_MS });
|
|
245
|
+
} catch (e) {
|
|
246
|
+
if (e.status === 124) e.code = "ETIMEDOUT";
|
|
247
|
+
throw e;
|
|
248
|
+
}
|
|
194
249
|
}
|
|
195
250
|
|
|
196
251
|
// ── 1. 업데이트 ──────────────────────────────────────────────────────────────
|
|
197
252
|
function ensureVersion() {
|
|
198
253
|
const have = installedVersions();
|
|
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
|
-
}
|
|
254
|
+
if (have.includes(TARGET_VERSION)) return useCached();
|
|
206
255
|
if (MODE === "check") { bad(`${TARGET_VERSION} 미설치 (설치된 버전: ${have.join(", ") || "없음"})`); return false; }
|
|
207
256
|
if (!canInstallTarget()) return false;
|
|
208
257
|
|
|
@@ -212,29 +261,97 @@ function ensureVersion() {
|
|
|
212
261
|
const dest = join(CACHE, TARGET_VERSION);
|
|
213
262
|
unpackTarget(dest);
|
|
214
263
|
linkNodeModules(dest, have);
|
|
215
|
-
return switchActive(sha
|
|
264
|
+
return switchActive(sha);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// TARGET_VERSION 이 캐시에 있을 때. 이미 활성이면 끝, 아니면 마켓플레이스 태그의 커밋으로 전환한다.
|
|
268
|
+
// 전환도 마켓플레이스와 git 이 있어야 하므로 먼저 확인한다(풀지 않으므로 tar 는 없어도 된다).
|
|
269
|
+
function useCached() {
|
|
270
|
+
const now = activeVersion().v;
|
|
271
|
+
if (MODE === "check" || now === TARGET_VERSION) { ok(`${TARGET_VERSION} 설치되어 있음`); return true; }
|
|
272
|
+
log(` ${TARGET_VERSION} 은 캐시에 있으나 활성 판은 ${oneLine(now)} 입니다. 전환합니다`);
|
|
273
|
+
if (!canInstallTarget({ tar: false })) return false;
|
|
274
|
+
const sha = targetSha();
|
|
275
|
+
return sha === null ? false : switchActive(sha);
|
|
216
276
|
}
|
|
217
277
|
|
|
218
|
-
// 마켓플레이스가 제 git 저장소이고 git
|
|
219
|
-
function canInstallTarget() {
|
|
278
|
+
// 마켓플레이스가 제 git 저장소이고 git 이 있어야 TARGET_VERSION 의 커밋을 찾고, 풀려면 tar 도 있어야 한다.
|
|
279
|
+
function canInstallTarget({ tar = true } = {}) {
|
|
220
280
|
if (!existsSync(MARKET)) { bad(`마켓플레이스 없음: ${oneLine(MARKET)}\n Claude Code 에서 marketplace 를 먼저 추가하십시오: ${REPO_URL}`); return false; }
|
|
221
281
|
if (!existsSync(join(MARKET, ".git"))) { bad(`마켓플레이스 폴더가 git 저장소가 아님: ${oneLine(MARKET)}`); return false; }
|
|
282
|
+
// 판을 바꾸기 전에 installed_plugins.json 의 백업 자리를 본다. 링크나 일반 파일이 아닌 자리면 있으면 실패하는 복사가
|
|
283
|
+
// EEXIST 로 끝나 이미 있는 백업으로 보여, 백업 없이 바꾸게 된다. 풀기 전에 멈춘다.
|
|
284
|
+
const spot = backupSpotWhy(`${INSTALLED}.omcbak`);
|
|
285
|
+
if (spot) { bad(`installed_plugins.json 을 바꾸지 않음 (${spot}): ${oneLine(INSTALLED)}.omcbak`); return false; }
|
|
222
286
|
|
|
223
287
|
// git 과 tar 에 의존한다. Windows 10+ / macOS / 대부분의 Linux 배포판에는 둘 다 있지만,
|
|
224
288
|
// 최소 설치 컨테이너나 tar 없는 환경에서는 없을 수 있다. 여기서 못 찾으면 아래에서
|
|
225
289
|
// execFileSync 가 ENOENT 로 죽어 원인이 안 보이므로, 먼저 확인하고 무엇이 없는지 말한다.
|
|
226
|
-
|
|
290
|
+
const tools = [["git", "마켓플레이스에서 소스를 꺼내는 데"], ["tar", "그 소스를 푸는 데"]].slice(0, tar ? 2 : 1);
|
|
291
|
+
for (const [cmd, why] of tools) {
|
|
227
292
|
try { execFileSync(cmd, ["--version"], { cwd: homedir(), stdio: "ignore" }); }
|
|
228
293
|
catch { bad(`${cmd} 를 찾을 수 없습니다 (${why} 필요). 설치한 뒤 다시 실행하거나, --no-update 로 패치만 적용하십시오.`); return false; }
|
|
229
294
|
}
|
|
230
295
|
return true;
|
|
231
296
|
}
|
|
232
297
|
|
|
233
|
-
//
|
|
298
|
+
// origin 에서 태그를 받는다. 받았으면 null, 받지 못했으면 그 까닭 한 줄. 이 도구가 고정한 origin 은 받을 수 없으므로
|
|
299
|
+
// 부르지 않고 그렇다고 한다(git 은 "remote helper" 오류만 낸다). curl 은 비밀번호 안의 `/` 를 호스트의 끝으로 읽어 사용자
|
|
300
|
+
// 정보 조각을 문구에 싣는다("Port number ended with 'Q'", "Could not resolve host: alice"). 그래서 다음이면 문구는 싣지
|
|
301
|
+
// 않고 git 이 어떻게 끝났는지만 댄다: origin 에 사용자 정보가 있음, git 의 오류 줄에 사용자 정보가 든 주소가 남음(전역
|
|
302
|
+
// insteadOf 가 더한 것. 그 줄을 가린 자리 `//***@` 로 안다), 프록시 주소를 curl 이 잘못 읽을 수 있음(proxyMisread).
|
|
303
|
+
// 바른 꼴의 사용자 정보는 git 이 줄에서 떼고 curl 도 제대로 읽어, 그 줄은 그대로 싣는다.
|
|
304
|
+
function fetchTags() {
|
|
305
|
+
const url = originUrl();
|
|
306
|
+
if (typeof url === "string" && url.startsWith("omc-pinned://")) return "origin 이 고정되어 있음(omc-pinned://). 받으려면 먼저 --revert 로 고정을 푸십시오";
|
|
307
|
+
try { gitFetch(["fetch", "--tags", "--quiet", "origin"]); return null; }
|
|
308
|
+
catch (e) {
|
|
309
|
+
if (e.code === "ETIMEDOUT") return `${FETCH_TIMEOUT_MS / 1000}초 안에 끝나지 않아 끊음(응답하지 않는 망일 수 있음)`;
|
|
310
|
+
const line = gitLine(e);
|
|
311
|
+
const why = redactUrl(line);
|
|
312
|
+
const ended = `git 이 종료 ${oneLine(e.status ?? e.signal)} 로 끝남`;
|
|
313
|
+
if (hasUserinfo(url) || why.includes("//***@")) return `${ended}(주소에 사용자 정보가 있어 git 의 문구는 싣지 않음)`;
|
|
314
|
+
if (proxyMisread()) return `${ended}(프록시 주소의 사용자 정보를 curl 이 잘못 읽을 수 있어 git 의 문구는 싣지 않음)`;
|
|
315
|
+
return shownGit(line);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 주소의 사용자 정보(처음 `//` 뒤부터, 없으면 처음부터 마지막 `@` 까지)에 날 `/` `?` `#` `\` 가 있는지. curl 은 그 앞에서
|
|
320
|
+
// authority 를 끊는다. 그러면 사용자 이름이 호스트가 되고 비밀번호 앞부분이 포트가 된다.
|
|
321
|
+
const brokenUserinfo = (v) => {
|
|
322
|
+
const s = String(v);
|
|
323
|
+
const at = s.lastIndexOf("@");
|
|
324
|
+
const i = s.indexOf("//");
|
|
325
|
+
return at !== -1 && /[/?#\\]/.test(s.slice(i !== -1 && i < at ? i + 2 : 0, at));
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// 태그 받기가 쓸 수 있는 프록시 주소 가운데 curl 이 잘못 읽을 것이 있는지. 프록시 주소는 git 의 줄에 실리지 않아
|
|
329
|
+
// ("Could not resolve proxy: alice" 처럼 호스트만) 설정에서 본다: curl 이 읽는 환경 변수와 git 이 넘기는 `*.proxy` 설정
|
|
330
|
+
// (태그 받기와 같은 환경, `http.proxy`, `http.<url>.proxy`, `remote.origin.proxy`).
|
|
331
|
+
function proxyMisread() {
|
|
332
|
+
const env = Object.entries(process.env).filter(([k]) => /^(https?|all)_proxy$/i.test(k)).map(([, v]) => v);
|
|
333
|
+
let cfg = [];
|
|
334
|
+
try { cfg = git(["config", "--get-regexp", "\\.proxy$"], MARKET, { network: true }).split("\n").map((l) => l.slice(l.indexOf(" ") + 1)); }
|
|
335
|
+
catch { /* 그런 설정이 없으면 git 은 종료 1 */ }
|
|
336
|
+
return [...env, ...cfg].some(brokenUserinfo);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// URL 에 사용자 정보가 있는지(redactUrl 이 가리는 자리: 처음 `//` 뒤부터 마지막 `@` 까지).
|
|
340
|
+
const hasUserinfo = (url) => {
|
|
341
|
+
const s = String(url);
|
|
342
|
+
const i = s.indexOf("//");
|
|
343
|
+
return i !== -1 && s.lastIndexOf("@") > i + 1;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// 태그 v<TARGET_VERSION> 의 커밋. 못 찾으면 null. 받지 못해도 로컬 태그가 있으면 진행한다(오프라인). 없으면 받지 못한 까닭도 댄다.
|
|
234
347
|
function targetSha() {
|
|
235
|
-
|
|
348
|
+
const why = fetchTags();
|
|
236
349
|
try { return git(["rev-parse", `v${TARGET_VERSION}^{}`]); }
|
|
237
|
-
catch {
|
|
350
|
+
catch {
|
|
351
|
+
bad(`태그 v${TARGET_VERSION} 를 마켓플레이스에서 찾을 수 없음`);
|
|
352
|
+
if (why) log(` origin 에서 태그를 받지 못함: ${why}`);
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
238
355
|
}
|
|
239
356
|
|
|
240
357
|
// git archive | tar 로 소스를 편다. 저장소에 dist/ 가 포함되어 빌드는 불필요하다.
|
|
@@ -259,22 +376,31 @@ function linkNodeModules(dest, have) {
|
|
|
259
376
|
}
|
|
260
377
|
}
|
|
261
378
|
|
|
262
|
-
// installed_plugins.json 을 읽는다. 읽지 못하거나 JSON 이 아니면 이름과 이유를 대고
|
|
379
|
+
// installed_plugins.json 을 읽는다. 읽지 못하거나 JSON 이 아니면 이름과 이유를 대고 undefined(파일 글은 내지 않는다).
|
|
380
|
+
// 읽은 값이 0, false, "", null 이어도 그 값을 돌려준다 — 실패와 섞이면 이유 없이 멈춘다.
|
|
263
381
|
function readInstalled() {
|
|
264
382
|
try { return JSON.parse(readRegular(INSTALLED, "utf8")); }
|
|
265
|
-
catch (err) { bad(`읽지 못함: installed_plugins.json (${err instanceof SyntaxError ? "JSON 이 아님" : failWhy(err)})`); return
|
|
383
|
+
catch (err) { bad(`읽지 못함: installed_plugins.json (${err instanceof SyntaxError ? "JSON 이 아님" : failWhy(err)})`); return undefined; }
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// installPath 의 마지막 칸(판)을 v 로 바꾼다. 구분자는 그 경로가 쓰는 것을 따른다.
|
|
387
|
+
function withVersion(installPath, v) {
|
|
388
|
+
const sep = installPath.includes("//") ? "//" : installPath.includes("\\") ? "\\" : "/";
|
|
389
|
+
return installPath.split(sep).slice(0, -1).concat(v).join(sep);
|
|
266
390
|
}
|
|
267
391
|
|
|
268
392
|
// 활성 버전 전환. installed_plugins.json 을 읽거나 쓰지 못하면 이름과 이유를 대고 false(호출부가 중단한다).
|
|
269
|
-
|
|
393
|
+
// 완료 줄의 앞 판은 바꾸기 전 항목의 version 이다(캐시 목록의 첫 판이 아니라).
|
|
394
|
+
function switchActive(sha) {
|
|
270
395
|
const j = readInstalled();
|
|
271
|
-
if (
|
|
396
|
+
if (j === undefined) return false;
|
|
397
|
+
let from;
|
|
272
398
|
try {
|
|
273
|
-
const e = j
|
|
399
|
+
const e = j?.plugins?.[PLUGIN_KEY]?.[0];
|
|
274
400
|
if (!e) { bad(`installed_plugins.json 에 ${PLUGIN_KEY} 항목이 없음 — 수동 확인 필요`); return false; }
|
|
401
|
+
from = typeof e.version === "string" ? e.version : "?";
|
|
275
402
|
backup(INSTALLED, `${INSTALLED}.omcbak`);
|
|
276
|
-
|
|
277
|
-
e.installPath = e.installPath.split(sep).slice(0, -1).concat(TARGET_VERSION).join(sep);
|
|
403
|
+
e.installPath = withVersion(e.installPath, TARGET_VERSION);
|
|
278
404
|
e.version = TARGET_VERSION;
|
|
279
405
|
e.gitCommitSha = sha;
|
|
280
406
|
e.lastUpdated = new Date().toISOString();
|
|
@@ -283,7 +409,7 @@ function switchActive(sha, have) {
|
|
|
283
409
|
bad(`쓰지 못함: installed_plugins.json (${failWhy(err)})`);
|
|
284
410
|
return false;
|
|
285
411
|
}
|
|
286
|
-
ok(`${
|
|
412
|
+
ok(`${oneLine(from)} → ${TARGET_VERSION} (sha ${sha.slice(0, 12)})`);
|
|
287
413
|
return true;
|
|
288
414
|
}
|
|
289
415
|
|
|
@@ -292,14 +418,16 @@ function switchActive(sha, have) {
|
|
|
292
418
|
// 파일)의 내용은 그대로 둔다. 쓸 파일이 링크면 쓰는 때의 실제 경로에서 바꿔 링크를 남긴다. 그래서 확인 뒤 그 자리가
|
|
293
419
|
// 폴더 밖 링크로 바뀌면 그 실제 파일이 바뀐다(확인과 쓰기 사이의 틈은 줄었을 뿐 없지 않다). follow 가 false 면
|
|
294
420
|
// 링크를 따라가지 않고 그 이름 자리를 새 파일로 바꾼다(링크는 사라지고 링크가 가리키던 파일은 그대로다. 고정 기록).
|
|
295
|
-
// 권한은 mode 를 주면 그 값, 아니면 원래 파일의 것(없으면 0644)이다.
|
|
421
|
+
// 권한은 mode 를 주면 그 값, 아니면 원래 파일의 것(없으면 0644)이다. 임시 파일을 만들 때부터 그 권한으로 연다(umask 가
|
|
422
|
+
// 줄인 비트는 chmod 가 맞춘다). 만든 뒤에 맞추면 그 사이 기본 권한(0664 등)으로 남이 읽을 수 있다.
|
|
296
423
|
function writeAtomic(file, data, mode, { follow = true } = {}) {
|
|
297
424
|
const real = follow ? realOr(file) : file;
|
|
298
425
|
const tmp = join(dirname(real), `.${basename(real)}.omc-tmp-${process.pid}`);
|
|
426
|
+
const m = mode ?? modeOf(real);
|
|
299
427
|
rmSync(tmp, { force: true });
|
|
300
428
|
try {
|
|
301
|
-
writeSynced(tmp, data);
|
|
302
|
-
chmodSync(tmp,
|
|
429
|
+
writeSynced(tmp, data, m);
|
|
430
|
+
chmodSync(tmp, m);
|
|
303
431
|
renameRetry(tmp, real);
|
|
304
432
|
} catch (e) {
|
|
305
433
|
rmSync(tmp, { force: true });
|
|
@@ -309,16 +437,23 @@ function writeAtomic(file, data, mode, { follow = true } = {}) {
|
|
|
309
437
|
|
|
310
438
|
// 새 임시 파일(있으면 실패하는 열기)에 쓰고 디스크까지 내린다. 내리지 않고 rename 하면 전원이 나가거나 NFS 에서
|
|
311
439
|
// 새 이름 아래 짧거나 빈 파일이 남을 수 있다(OMC 의 atomicWriteFileSync, obsidizer 의 writeTemp 와 같은 꼴).
|
|
312
|
-
function writeSynced(tmp, data) {
|
|
313
|
-
const fd = openSync(tmp, "wx");
|
|
440
|
+
function writeSynced(tmp, data, mode) {
|
|
441
|
+
const fd = openSync(tmp, "wx", mode);
|
|
314
442
|
try {
|
|
315
443
|
writeFileSync(fd, data);
|
|
316
|
-
|
|
444
|
+
syncIfAble(fd);
|
|
317
445
|
} finally {
|
|
318
446
|
closeSync(fd);
|
|
319
447
|
}
|
|
320
448
|
}
|
|
321
449
|
|
|
450
|
+
// fsync 를 하지 않는 파일 시스템(EINVAL, ENOTSUP)에서도 새 파일은 rename 으로 통째로 바뀐다. 잃는 것은 정전 때의 안전뿐이다.
|
|
451
|
+
// 그 밖의 오류는 그대로 던진다(쓰지 못함으로 알림). azure-adapter 의 syncIfAble 과 같은 꼴이다.
|
|
452
|
+
const NO_FSYNC = new Set(["EINVAL", "ENOTSUP"]);
|
|
453
|
+
function syncIfAble(fd) {
|
|
454
|
+
try { fsyncSync(fd); } catch (e) { if (!NO_FSYNC.has(e.code)) throw e; }
|
|
455
|
+
}
|
|
456
|
+
|
|
322
457
|
// 파일의 권한 비트. 읽을 수 없으면 0644.
|
|
323
458
|
function modeOf(p) {
|
|
324
459
|
try { return statSync(p).mode & 0o777; } catch { return 0o644; }
|
|
@@ -336,12 +471,26 @@ function renameRetry(from, to) {
|
|
|
336
471
|
}
|
|
337
472
|
}
|
|
338
473
|
|
|
339
|
-
// 백업을 만든다. 이미 있으면
|
|
474
|
+
// 백업을 만든다. 이미 있으면 그대로 둔다(적용은 그 전에 freshBackup 으로 지금 원본과 맞춘다). 있으면 실패하는 복사라 확인과
|
|
475
|
+
// 복사 사이에 틈이 없다.
|
|
340
476
|
function backup(file, bak) {
|
|
341
477
|
try { copyFileSync(file, bak, FS_CONST.COPYFILE_EXCL); }
|
|
342
478
|
catch (e) { if (e.code !== "EEXIST") throw e; }
|
|
343
479
|
}
|
|
344
480
|
|
|
481
|
+
// 적용 직전에 본다. 이미 있는 백업이 지금 원본(패치 전)과 다르면 그 백업은 옛 원본이다(같은 판 폴더에 새 원본이 깔림). 지금
|
|
482
|
+
// 원본으로 바꿔야 나중의 되돌리기가 새 원본을 돌려준다. 정상 흐름에서는 둘이 같아 바꾸지 않는다. 백업이 없으면 backup 이 만든다.
|
|
483
|
+
// 다시 읽은 글이 원본이 아니면(isOriginal 이 거짓) 바꾸지 않는다. 같이 돈 다른 실행이 이 실행의 두 읽기 사이에 먼저 패치했으면 그
|
|
484
|
+
// 글은 패치본이고, 그것을 백업에 쓰면 되돌리기가 패치본을 돌려준다. 확인한 글을 그대로 쓰므로 확인과 쓰기 사이에 틈이 없다.
|
|
485
|
+
function freshBackup(file, bak, name, isOriginal) {
|
|
486
|
+
const saved = readBackup(bak);
|
|
487
|
+
if (saved === null) return;
|
|
488
|
+
const now = readRegular(file);
|
|
489
|
+
if (saved.equals(now) || !isOriginal(now.toString("utf8"))) return;
|
|
490
|
+
writeAtomic(bak, now, modeOf(bak), { follow: false });
|
|
491
|
+
ok(`옛 백업을 지금 원본으로 바꿈: ${shown(name)}`);
|
|
492
|
+
}
|
|
493
|
+
|
|
345
494
|
// 일반 파일만 읽는다. FIFO 는 열기만 하고 읽지 않으며, 폴더와 소켓, 장치는 던진다(형제 도구의 읽기와 같은 꼴).
|
|
346
495
|
function readRegular(p, enc) {
|
|
347
496
|
const fd = openSync(p, FS_CONST.O_RDONLY | (FS_CONST.O_NONBLOCK ?? 0));
|
|
@@ -361,9 +510,11 @@ const failWhy = (e) => oneLine(e.code || e.message);
|
|
|
361
510
|
function readHook(n, f, r) {
|
|
362
511
|
try { return readRegular(f, "utf8"); }
|
|
363
512
|
catch (e) {
|
|
513
|
+
// 읽지 못한 훅은 --check 에서도 실패다. "패치 필요" 로 세면 결과 줄이 적용을 권하는데, 그 적용도 같은 읽기에서 실패한다.
|
|
364
514
|
bad(`읽지 못함: ${shown(n)} (${failWhy(e)})`);
|
|
365
|
-
|
|
366
|
-
|
|
515
|
+
r.fail++;
|
|
516
|
+
r.failed.push(f);
|
|
517
|
+
if (MODE === "check") r.unread++;
|
|
367
518
|
return null;
|
|
368
519
|
}
|
|
369
520
|
}
|
|
@@ -652,10 +803,11 @@ function syntaxError(file) {
|
|
|
652
803
|
}
|
|
653
804
|
}
|
|
654
805
|
|
|
655
|
-
// 백업의 글. 없거나 읽으려는 순간 사라졌으면 null(다른 실행이 먼저 되돌렸을 수 있다).
|
|
806
|
+
// 백업의 글. 없거나 읽으려는 순간 사라졌으면 null(다른 실행이 먼저 되돌렸을 수 있다). 그 밖에 읽지 못하면 backupRead 표시를 달아
|
|
807
|
+
// 던진다 — 쓰기 실패와 같은 catch 가 "읽지 못함" 으로 알리게.
|
|
656
808
|
function readBackup(bak) {
|
|
657
809
|
try { return readRegular(bak); }
|
|
658
|
-
catch (e) { if (e.code === "ENOENT") return null; throw e; }
|
|
810
|
+
catch (e) { if (e.code === "ENOENT") return null; throw Object.assign(e, { backupRead: true }); }
|
|
659
811
|
}
|
|
660
812
|
|
|
661
813
|
// 이 도구가 아닌 «손» 이 import 를 감싼 흔적. 4.15.7 의 손 수정은 최상위의 `await import(` 를
|
|
@@ -672,14 +824,15 @@ function looksHandPatched(src) {
|
|
|
672
824
|
return found;
|
|
673
825
|
}
|
|
674
826
|
|
|
827
|
+
// 패치(이 도구의 헬퍼, 손 패치)가 없는 글인가. 적용이 옛 백업을 바꾸기 전에 지금 글이 원본인지 본다.
|
|
828
|
+
const unpatched = (src) => !src.includes("__omcRaceImport") && !looksHandPatched(src);
|
|
829
|
+
|
|
675
830
|
function patchHooks(root) {
|
|
676
|
-
const r = { done: 0, skip: 0, fail: 0, need: 0, misplaced: 0, foreign: [], orphan: [], failed: [] };
|
|
831
|
+
const r = { done: 0, skip: 0, fail: 0, need: 0, misplaced: 0, unread: 0, foreign: [], orphan: [], failed: [] };
|
|
677
832
|
// revert 는 scripts/ 전체를 훑는다. hooks.json 기반 목록은 두 번 새는데, 실측으로
|
|
678
833
|
// 4.15.7 이 둘 다 겪었다 — PostToolUse 를 지우면 그 스크립트가 목록에서 빠지고(그래서
|
|
679
834
|
// .omcbak 이 있어도 안 돌아온다), 손으로 고친 파일은 .omcbak 조차 없어 아예 안 보인다.
|
|
680
|
-
const names = MODE === "revert"
|
|
681
|
-
? readdirSync(join(root, "scripts")).filter((f) => f.endsWith(".mjs")).map((f) => f.slice(0, -4)).sort()
|
|
682
|
-
: hookNames(root);
|
|
835
|
+
const names = MODE === "revert" ? scriptNames(root, r) : hookNames(root);
|
|
683
836
|
for (const n of names) {
|
|
684
837
|
const f = join(root, "scripts", `${n}.mjs`);
|
|
685
838
|
if (!existsSync(f)) continue;
|
|
@@ -689,6 +842,19 @@ function patchHooks(root) {
|
|
|
689
842
|
return r;
|
|
690
843
|
}
|
|
691
844
|
|
|
845
|
+
// revert 가 훑는 이름: scripts/ 의 .mjs 전부. scripts/ 가 없는 판 폴더는 되돌릴 훅이 없어 한 줄로 알리고 넘어간다.
|
|
846
|
+
// 그 밖에 목록을 읽지 못하면(파일, 권한) 이름과 이유를 대고 실패로 센다 — 다음 판과 [3/4], [4/4] 는 그대로 간다.
|
|
847
|
+
function scriptNames(root, r) {
|
|
848
|
+
const dir = join(root, "scripts");
|
|
849
|
+
try {
|
|
850
|
+
return readdirSync(dir).filter((f) => f.endsWith(".mjs")).map((f) => f.slice(0, -4)).sort();
|
|
851
|
+
} catch (e) {
|
|
852
|
+
if (e.code === "ENOENT") log(" 건너뜀: scripts 폴더 없음");
|
|
853
|
+
else { bad(`읽지 못함: scripts (${failWhy(e)}): ${oneLine(dir)}`); r.fail++; r.failed.push(dir); }
|
|
854
|
+
return [];
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
692
858
|
function revertScript(root, n, f, r) {
|
|
693
859
|
const bak = `${f}.omcbak`;
|
|
694
860
|
if (existsSync(bak) && restoreScript(root, n, f, bak, r)) return;
|
|
@@ -702,10 +868,25 @@ function revertScript(root, n, f, r) {
|
|
|
702
868
|
else if (looksHandPatched(src)) r.foreign.push(n);
|
|
703
869
|
}
|
|
704
870
|
|
|
871
|
+
// 되돌리기 전에 본다(쓰기 조건 뒤). 지금 파일에 패치(헬퍼, 손 패치)가 없고 글이 백업과 다르면 그 사이 같은 판 폴더에 새 원본이
|
|
872
|
+
// 깔린 것이다. 옛 백업을 덮으면 새 원본을 잃는다. 그 이유를, 아니면 null. 어느 쪽이든 읽지 못하면 판단하지 않는다(전처럼 복구).
|
|
873
|
+
const STALE = "지금 파일에 패치가 없고 백업과 글이 다름. 백업이 옛 원본일 수 있음";
|
|
874
|
+
function staleWhy(f, bak) {
|
|
875
|
+
let now, saved;
|
|
876
|
+
try {
|
|
877
|
+
now = readRegular(f, "utf8");
|
|
878
|
+
saved = readRegular(bak, "utf8");
|
|
879
|
+
} catch {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
if (now.includes("__omcRaceImport") || looksHandPatched(now)) return null;
|
|
883
|
+
return now === saved ? null : STALE;
|
|
884
|
+
}
|
|
885
|
+
|
|
705
886
|
// 백업으로 되돌린다. 쓸 수 없는 자리거나 쓰지 못하면 이유를 대고 실패로 센다. 백업이 읽으려는 순간 사라졌으면 false
|
|
706
887
|
// (다른 실행이 먼저 되돌렸을 수 있다. 호출부가 백업 없음으로 다시 본다).
|
|
707
888
|
function restoreScript(root, n, f, bak, r) {
|
|
708
|
-
const why = whyNotWrite(root, f, bak) ?? restoreFrom(f, bak);
|
|
889
|
+
const why = whyNotWrite(root, f, bak) ?? staleWhy(f, bak) ?? restoreFrom(f, bak);
|
|
709
890
|
if (why === GONE) return false;
|
|
710
891
|
if (!why) {
|
|
711
892
|
log(` 되돌림 ${shown(n)}`); r.done++;
|
|
@@ -720,23 +901,27 @@ function restoreScript(root, n, f, bak, r) {
|
|
|
720
901
|
return true;
|
|
721
902
|
}
|
|
722
903
|
|
|
723
|
-
// 링크 너머의 일반 파일(1MiB 이하)
|
|
904
|
+
// 링크 너머의 일반 파일(1MiB 이하)이 백업을 이 도구가 패치한 글과 같은가. 옛 도구가 링크를 따라가 거기서 고친 파일이면 참이다.
|
|
905
|
+
// 헬퍼 이름만 든 다른 파일(메모, 손으로 고친 파일)은 거짓이다 — 거기에 백업을 덮어쓰라고 하면 그 파일의 글을 잃는다.
|
|
906
|
+
// 다른 자리에 헬퍼를 넣던 옛 킷의 패치본과 옛 헬퍼로 패치한 파일도 거짓이라 "먼저 확인" 쪽으로 간다(보수 쪽).
|
|
724
907
|
const PEEK_MAX = 1 << 20;
|
|
725
|
-
function patchedByOldTool(p) {
|
|
908
|
+
function patchedByOldTool(p, bak) {
|
|
726
909
|
try {
|
|
727
910
|
const st = statSync(p);
|
|
728
|
-
|
|
911
|
+
if (!st.isFile() || st.size > PEEK_MAX) return false;
|
|
912
|
+
const patched = transform(readRegular(bak, "utf8"));
|
|
913
|
+
return patched !== null && patched === readRegular(p, "utf8");
|
|
729
914
|
} catch {
|
|
730
915
|
return false;
|
|
731
916
|
}
|
|
732
917
|
}
|
|
733
918
|
|
|
734
|
-
// 손 복구 안내. 링크 너머의
|
|
919
|
+
// 손 복구 안내. 링크 너머의 파일이 이 도구의 패치본일 때만 백업으로 덮어쓰라고 하고, 아니면 링크부터 확인하게 한다.
|
|
735
920
|
function handRestoreHint(bak, f) {
|
|
736
921
|
const target = realOr(f);
|
|
737
|
-
return patchedByOldTool(target)
|
|
922
|
+
return patchedByOldTool(target, bak)
|
|
738
923
|
? `손으로 되돌리려면 ${oneLine(realOr(bak))} 의 내용을 확인한 뒤 ${oneLine(target)} 에 덮어쓰고 백업을 지우십시오.`
|
|
739
|
-
: `링크가 가리키는 ${oneLine(target)}
|
|
924
|
+
: `링크가 가리키는 ${oneLine(target)} 의 글은 지금 이 도구가 만드는 패치본과 같지 않거나 읽어 확인할 수 없습니다. 덮어쓰지 말고 링크가 가리키는 곳이 맞는지 먼저 확인하십시오.`;
|
|
740
925
|
}
|
|
741
926
|
|
|
742
927
|
// 백업의 내용으로 파일을 바꾼다. 쓰지 못하면 그 이유, 백업이 읽으려는 순간 사라졌으면 GONE, 아니면 null.
|
|
@@ -778,10 +963,11 @@ function checkOrPatchScript(root, n, f, r) {
|
|
|
778
963
|
// 쓰지 못하면 이유를 대고 false 다(예전에는 예외로 멈췄다).
|
|
779
964
|
function writePatched(f, bak, out, n) {
|
|
780
965
|
try {
|
|
966
|
+
freshBackup(f, bak, n, unpatched);
|
|
781
967
|
backup(f, bak);
|
|
782
968
|
writeAtomic(f, out);
|
|
783
969
|
} catch (e) {
|
|
784
|
-
bad(`쓰지 못함: ${n} (${failWhy(e)})`);
|
|
970
|
+
bad(e.backupRead ? `읽지 못함: ${n} 의 백업 (${failWhy(e)})` : `쓰지 못함: ${n} (${failWhy(e)})`);
|
|
785
971
|
return false;
|
|
786
972
|
}
|
|
787
973
|
const err = syntaxError(f);
|
|
@@ -799,14 +985,32 @@ function writePatched(f, bak, out, n) {
|
|
|
799
985
|
// 폴더 밖 쓰기를 거부한 이유. 되돌리기는 이 이유일 때 손 복구 방법을 함께 알린다.
|
|
800
986
|
const OUTSIDE = "파일이 플러그인 폴더 밖에 있음";
|
|
801
987
|
|
|
802
|
-
// 쓰기 전에 본다. 쓸 파일이 (링크를 따라가서) 플러그인 폴더 밖이거나 백업 자리가
|
|
803
|
-
// 아니면 null. 백업 자리가 깨진 링크면 existsSync 는 false 라, 복사가 링크가 가리키는 곳에 쓴다.
|
|
988
|
+
// 쓰기 전에 본다. 쓸 파일이 (링크를 따라가서) 플러그인 폴더 밖이거나 백업 자리가 링크, 또는 일반 파일이 아닌 것(FIFO,
|
|
989
|
+
// 폴더)이면 그 이유를, 아니면 null. 백업 자리가 깨진 링크면 existsSync 는 false 라, 복사가 링크가 가리키는 곳에 쓴다.
|
|
990
|
+
// 일반 파일이 아닌 백업 자리는 있으면 실패하는 복사가 EEXIST 로 끝나 "처음 백업" 으로 보이므로, 백업 없이 패치하게 된다.
|
|
804
991
|
function whyNotWrite(root, file, bak) {
|
|
805
|
-
|
|
992
|
+
const spot = backupSpotWhy(bak);
|
|
993
|
+
if (spot) return spot;
|
|
806
994
|
if (!insideRoot(root, file)) return OUTSIDE;
|
|
807
995
|
return notWritable(file);
|
|
808
996
|
}
|
|
809
997
|
|
|
998
|
+
// 백업 자리가 링크이거나 일반 파일이 아닌 것(FIFO, 폴더)이면 그 이유, 아니면 null.
|
|
999
|
+
function backupSpotWhy(bak) {
|
|
1000
|
+
if (isLink(bak)) return "백업 자리가 링크";
|
|
1001
|
+
return notRegularAt(bak) ? "백업 자리가 일반 파일이 아님" : null;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// 그 자리에 일반 파일도 링크도 아닌 것(FIFO, 폴더, 장치)이 있으면 true. 없는 자리는 false.
|
|
1005
|
+
function notRegularAt(p) {
|
|
1006
|
+
try {
|
|
1007
|
+
const st = lstatSync(p);
|
|
1008
|
+
return !st.isFile() && !st.isSymbolicLink();
|
|
1009
|
+
} catch {
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
810
1014
|
// 있는 자리가 일반 파일이 아니거나(FIFO, 폴더, 장치) 이 계정이 쓸 수 없으면 그 이유. 새 파일로 바꿔 쓰는 방식이 파일 권한을
|
|
811
1015
|
// 우회하지 않게 하고, 특수 파일을 일반 파일로 바꾸지 않게 한다. 없는 자리는 괜찮다.
|
|
812
1016
|
function notWritable(file) {
|
|
@@ -851,18 +1055,30 @@ function patchHooksJson(root) {
|
|
|
851
1055
|
try {
|
|
852
1056
|
return MODE === "revert" ? revertHooksJson(root, hj) : removePostToolUse(hj, why);
|
|
853
1057
|
} catch (e) {
|
|
854
|
-
|
|
855
|
-
const why = e instanceof SyntaxError ? "JSON 이 아님" : failWhy(e);
|
|
856
|
-
hjBad(e instanceof SyntaxError || MODE === "check" ? `hooks.json 을 읽지 못함 (${why})` : `hooks.json 을 쓰지 못함 (${why})`);
|
|
1058
|
+
hjBad(hooksJsonFailure(e));
|
|
857
1059
|
return "unsafe";
|
|
858
1060
|
}
|
|
859
1061
|
}
|
|
860
1062
|
|
|
1063
|
+
// JSON 오류 문구는 파일의 글을 담으므로 내지 않는다. check 는 읽기만 하므로 실패도 읽기다.
|
|
1064
|
+
function hooksJsonFailure(e) {
|
|
1065
|
+
const why = e instanceof SyntaxError ? "JSON 이 아님" : failWhy(e);
|
|
1066
|
+
if (e.backupRead) return `hooks.json 의 백업을 읽지 못함 (${why})`;
|
|
1067
|
+
return e instanceof SyntaxError || e.reading || MODE === "check" ? `hooks.json 을 읽지 못함 (${why})` : `hooks.json 을 쓰지 못함 (${why})`;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// hooks.json 의 글. 읽기 실패에는 reading 표시를 달아 던진다 — 쓰기 실패와 같은 catch 가 맞는 문구를 고르게.
|
|
1071
|
+
function readHooksJson(hj) {
|
|
1072
|
+
try { return readRegular(hj, "utf8"); }
|
|
1073
|
+
catch (e) { throw Object.assign(e, { reading: true }); }
|
|
1074
|
+
}
|
|
1075
|
+
|
|
861
1076
|
// 되돌린 결과. ambiguous 와 linked 는 hooks.json 을 되돌리지 못하고 남긴 것이라 끝 줄이 센다.
|
|
862
1077
|
function revertHooksJson(root, hj) {
|
|
863
1078
|
const bak = `${hj}.omcbak`;
|
|
864
1079
|
const saved = readBackup(bak);
|
|
865
1080
|
if (saved !== null) {
|
|
1081
|
+
if (staleHooksJson(hj, saved)) { hjBad(`hooks.json 을 되돌리지 않음 (지금 파일에 PostToolUse 가 있고 백업과 글이 다름. 백업이 옛 원본일 수 있음)`); return "unsafe"; }
|
|
866
1082
|
writeAtomic(hj, saved);
|
|
867
1083
|
log(` 되돌림 ${hjTag}hooks.json`);
|
|
868
1084
|
const left = dropBackup(bak);
|
|
@@ -870,7 +1086,9 @@ function revertHooksJson(root, hj) {
|
|
|
870
1086
|
return "reverted";
|
|
871
1087
|
}
|
|
872
1088
|
// 이 도구의 백업이 없을 때 PostToolUse 가 그대로면 되돌릴 것이 없다. 옆의 수동 백업으로 덮어쓰면
|
|
873
|
-
// 지금 내용을 잃는다.
|
|
1089
|
+
// 지금 내용을 잃는다. 읽지 못하면 PostToolUse 가 있는지 모르므로 "되돌릴 것 없음" 이라 하지 않는다
|
|
1090
|
+
// (위 catch 가 읽기 실패로 알리고 센다). 깨진 JSON 은 아래 수동 백업 찾기로 넘긴다.
|
|
1091
|
+
readHooksJson(hj);
|
|
874
1092
|
if (hasPostToolUse(hj)) return "no-backup";
|
|
875
1093
|
// 손으로 만든 백업이 남아 있을 수 있다(실측: 4.15.7 의 hooks.json.bak-20260906-postoolremove).
|
|
876
1094
|
// PostToolUse 를 담고 있는 유효한 JSON 이 딱 하나면 그것이 제거 직전 상태다 — 무엇을 썼는지 밝히고 되돌린다.
|
|
@@ -902,18 +1120,83 @@ process.on("uncaughtException", (e) => {
|
|
|
902
1120
|
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
1121
|
process.exit(1);
|
|
904
1122
|
});
|
|
905
|
-
//
|
|
906
|
-
|
|
907
|
-
//
|
|
908
|
-
|
|
909
|
-
|
|
1123
|
+
// git 이 줄에서 주소를 닫는 글자(`'...':` 의 끝).
|
|
1124
|
+
const CLOSING = /'?:?$/;
|
|
1125
|
+
// URL 의 사용자 정보와 쿼리의 값(이름과 상관없이)을 가린다. 출력과 기록에 비밀이 남지 않는다. 사용자 정보는 처음 `//` 뒤부터
|
|
1126
|
+
// 마지막 `@` 까지다(공백, 탭, 비밀번호 안의 `/` `?` `#` `@` 도). 경로나 쿼리에 `@` 가 든 URL 은 호스트까지 가려진다 — 비밀번호를
|
|
1127
|
+
// 놓치는 것보다 낫다. `//` 가 없는 ssh 꼴(`git@host:`)은 그대로다. 위치로 찾아 길이에 비례해 끝난다(`//.*@` 꼴 정규식은 `@` 없이
|
|
1128
|
+
// `/` 가 많은 글에서 길이의 제곱만큼 걸린다). 쿼리의 이름 칸은 `?` 를 받지 않는다. 받으면 `=` 없는 `?` 가 많은 origin 에서
|
|
1129
|
+
// 시작 자리마다 끝까지 읽어 길이의 제곱만큼 걸린다. 값 끝에 git 이 주소를 닫는 `'` 와 `:` 가 붙으면 그 둘은 남긴다.
|
|
1130
|
+
function redactUrl(url) {
|
|
1131
|
+
let s = String(url);
|
|
1132
|
+
const i = s.indexOf("//");
|
|
1133
|
+
const at = s.lastIndexOf("@");
|
|
1134
|
+
if (i !== -1 && at > i + 1) s = `${s.slice(0, i + 2)}***${s.slice(at)}`;
|
|
1135
|
+
return s.replace(/([?&][^=&#\s?]*=)[^&#\s]*/g, (m, name) => `${name}***${CLOSING.exec(m)[0]}`);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// 진단 줄의 주소(사용자 결정 2026-10-21): 기본 상류(REPO_URL)는 가린 꼴 그대로, 그 밖은 scheme 과 host 까지만. 미러의
|
|
1139
|
+
// 경로, 조각, `;` 매개변수에는 토큰이 들 수 있다. 기본 상류인지는 scheme, 사용자 정보(가렸든 아니든), 쿼리를 빼고 본다.
|
|
1140
|
+
const PIN = "omc-pinned://";
|
|
1141
|
+
const SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
|
|
1142
|
+
function hostPath(url) {
|
|
1143
|
+
let s = String(url);
|
|
1144
|
+
if (s.startsWith(PIN)) s = s.slice(PIN.length);
|
|
1145
|
+
const scheme = SCHEME.exec(s);
|
|
1146
|
+
if (scheme) s = s.slice(scheme[0].length);
|
|
1147
|
+
// user information ends at an @ before the first / (and ? # \ in a URL): an @ further on is in the path
|
|
1148
|
+
const cut = s.search(scheme ? /[/?#\\]/ : /\//);
|
|
1149
|
+
const at = s.lastIndexOf("@", cut === -1 ? s.length : cut);
|
|
1150
|
+
if (at !== -1) s = s.slice(at + 1);
|
|
1151
|
+
// the query to the end, found by position (a pattern from each "?" took the square of the length with a "#" after
|
|
1152
|
+
// them); with a fragment after it the address is another one (a fragment is not masked)
|
|
1153
|
+
const q = s.indexOf("?", s.lastIndexOf("#") + 1);
|
|
1154
|
+
if (q !== -1) s = s.slice(0, q);
|
|
1155
|
+
// the closing slashes by a loop: `/\/+$/` takes the square of the length on an address of many slashes
|
|
1156
|
+
let end = s.length;
|
|
1157
|
+
while (end > 0 && s[end - 1] === "/") end--;
|
|
1158
|
+
return s.slice(0, end).replace(/\.git$/i, "").toLowerCase();
|
|
1159
|
+
}
|
|
1160
|
+
// 기본 상류는 scheme 이 있는 주소만이다. scheme 없는 `user:<토큰>@github.com/...` 은 git 이 scp 꼴(host 가 `user`)로 읽고,
|
|
1161
|
+
// 가림(처음 `//` 부터)도 그 사용자 정보에 닿지 않는다.
|
|
1162
|
+
const isUpstream = (url) => SCHEME.test(String(url).startsWith(PIN) ? String(url).slice(PIN.length) : String(url)) && hostPath(url) === hostPath(REPO_URL);
|
|
1163
|
+
// 기본 상류의 보이는 꼴: 쿼리는 이름 없는 토큰까지 통째로 `?***`(redactUrl 은 `이름=값` 만 가린다). 쿼리를 가림보다 먼저
|
|
1164
|
+
// 잘라, 쿼리 속 `@` 까지 가린 사용자 정보가 쿼리의 `?` 를 먹지 않게 한다. 기본 상류의 첫 `?` 는 쿼리다: host 앞에 날 `?` 가
|
|
1165
|
+
// 있으면 hostPath 가 거기서 잘라 기본 상류가 아니다.
|
|
1166
|
+
function upstreamShown(u) {
|
|
1167
|
+
const q = u.indexOf("?");
|
|
1168
|
+
return q === -1 ? redactUrl(u) : `${redactUrl(u.slice(0, q))}?***`;
|
|
1169
|
+
}
|
|
1170
|
+
// --check 가 보이는 origin(가리기 전의 글).
|
|
1171
|
+
const shownOrigin = (url) => (isUpstream(url) ? upstreamShown(String(url)) : hostOnly(url));
|
|
1172
|
+
// git 의 줄에 든 주소마다, git 이 찍은 그대로의 주소로 정한 꼴. 가린 뒤에 정하면 마지막 `@` 까지 먹은 가림 뒤의 글(경로나
|
|
1173
|
+
// 쿼리 속 `@` 뒤의 토큰)이 host 로 보였다. 고정한 주소는 앞의 `omc-pinned://` 를 남긴다. 주소는 공백 앞까지로 보고, git 이
|
|
1174
|
+
// 주소를 닫는 `'` 와 `:` 는 남긴다.
|
|
1175
|
+
function shownAddress(u) {
|
|
1176
|
+
if (isUpstream(u)) return upstreamShown(u);
|
|
1177
|
+
const pin = u.startsWith(PIN) ? PIN : "";
|
|
1178
|
+
const inner = u.slice(pin.length);
|
|
1179
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(inner)) return `${pin}${hostOnly(inner)}`;
|
|
1180
|
+
const host = hostPath(inner).split("/")[0];
|
|
1181
|
+
return `${pin}${HOST_SHAPE.test(host) ? host : "(호스트를 알아볼 수 없음)"}`;
|
|
1182
|
+
}
|
|
1183
|
+
const shownAddresses = (line) => line.replace(/[A-Za-z][A-Za-z0-9+.-]*:\/\/\S*/g, (u) => {
|
|
1184
|
+
const tail = CLOSING.exec(u)[0];
|
|
1185
|
+
return `${shownAddress(u.slice(0, u.length - tail.length))}${tail}`;
|
|
1186
|
+
});
|
|
910
1187
|
|
|
911
1188
|
// URL 의 scheme 과 host 까지만. 비밀은 사용자 정보와 쿼리 밖(경로, 조각, `;` 매개변수)에도 들 수 있어 나머지는 버린다.
|
|
912
1189
|
// scp 꼴(`git@host:org/repo.git`)은 host 만이다. 파서가 host 로 읽어도 host 모양(이름이나 [IPv6], 선택 포트)이 아니면
|
|
913
1190
|
// 보이지 않는다(ssh 같은 scheme 은 host 뒤의 `;k=...` 까지 host 로 읽는다).
|
|
914
1191
|
const HOST_SHAPE = /^(?:[A-Za-z0-9._-]{1,253}|\[[0-9A-Fa-f:.]{2,45}\])(?::\d{1,5})?$/;
|
|
1192
|
+
// `scheme://` 뒤 첫 `/` `?` `#` `\` 앞까지(파서가 읽는 authority). 비밀번호에 그 글자가 날로 들면 파서는 그 앞의 `이름:숫자` 를
|
|
1193
|
+
// host:port 로 읽고 `@` 를 경로로 보낸다. 사용자 이름에도 날 `@` 가 들면(이메일 주소) 그 뒤의 `도메인:숫자` 를 읽는다. 그래서
|
|
1194
|
+
// authority 뒤에 `@` 가 있으면 host 를 믿지 않는다(가림이 마지막 `@` 까지인 것과 같은 쪽. 경로에 `@` 가 든 URL 은 host 도 감춘다).
|
|
1195
|
+
const AUTHORITY = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/?#\\]*)/;
|
|
915
1196
|
function hostOnly(url) {
|
|
916
1197
|
let scheme = "", host;
|
|
1198
|
+
const auth = AUTHORITY.exec(String(url));
|
|
1199
|
+
if (auth && String(url).includes("@", auth[0].length)) return "(호스트를 알아볼 수 없음)";
|
|
917
1200
|
try {
|
|
918
1201
|
const u = new URL(String(url));
|
|
919
1202
|
if (!u.host) return "(호스트가 없는 URL 또는 로컬 경로)";
|
|
@@ -939,17 +1222,31 @@ function manualBackups(root, dir) {
|
|
|
939
1222
|
return { usable, unsafe };
|
|
940
1223
|
}
|
|
941
1224
|
|
|
1225
|
+
// 되돌리기 전에 본다. 이 도구가 지운 PostToolUse 가 지금 hooks.json 에 다시 있고 글이 백업과 다르면 그 사이 새 hooks.json 이
|
|
1226
|
+
// 깔린 것이다. 옛 백업을 덮으면 새 훅 등록을 잃는다. 읽지 못하면 판단하지 않는다(전처럼 복구).
|
|
1227
|
+
function staleHooksJson(hj, saved) {
|
|
1228
|
+
let now;
|
|
1229
|
+
try { now = readRegular(hj); } catch { return false; }
|
|
1230
|
+
return !now.equals(saved) && hasPostToolUse(hj);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// 글이 PostToolUse 가 든 JSON 인가. 적용이 옛 hooks.json 백업을 바꾸기 전에 지금 글이 원본(지우기 전)인지도 본다.
|
|
1234
|
+
const postToolUseIn = (text) => {
|
|
1235
|
+
try { return !!JSON.parse(text)?.hooks?.PostToolUse; }
|
|
1236
|
+
catch { return false; }
|
|
1237
|
+
};
|
|
942
1238
|
const hasPostToolUse = (p) => {
|
|
943
|
-
try { return
|
|
1239
|
+
try { return postToolUseIn(readRegular(p, "utf8")); }
|
|
944
1240
|
catch { return false; }
|
|
945
1241
|
};
|
|
946
1242
|
|
|
947
1243
|
function removePostToolUse(hj, why) {
|
|
948
1244
|
const bak = `${hj}.omcbak`;
|
|
949
|
-
const j = JSON.parse(
|
|
1245
|
+
const j = JSON.parse(readHooksJson(hj));
|
|
950
1246
|
if (!j.hooks?.PostToolUse) return "already";
|
|
951
1247
|
const list = j.hooks.PostToolUse.flatMap((g) => (g.hooks || []).map((h) => (h.command || "").match(/([\w-]+)\.mjs/)?.[1] || "?"));
|
|
952
1248
|
if (MODE === "check") { hjBad(`hooks.json PostToolUse 남아있음${why ? `, 쓸 수 없음 (${why})` : ""}: ${list.join(", ")}`); return "needs"; }
|
|
1249
|
+
freshBackup(hj, bak, `${hjTag}hooks.json`, postToolUseIn);
|
|
953
1250
|
backup(hj, bak);
|
|
954
1251
|
delete j.hooks.PostToolUse;
|
|
955
1252
|
writeAtomic(hj, JSON.stringify(j, null, 2) + "\n");
|
|
@@ -972,20 +1269,21 @@ function removePostToolUse(hj, why) {
|
|
|
972
1269
|
// 고정과 되돌리기가 하지 않은 일을 했다고 끝난다.
|
|
973
1270
|
function originUrl() {
|
|
974
1271
|
try { return git(["config", "--get", "remote.origin.url"]); }
|
|
975
|
-
catch (e) { return e.status === 1 ? null : { unreadable:
|
|
1272
|
+
catch (e) { return e.status === 1 ? null : { unreadable: gitLine(e) }; }
|
|
976
1273
|
}
|
|
977
1274
|
|
|
978
|
-
// git 오류의 원인 줄: `fatal:` 이나 `error:` 로 시작하는 첫 줄, 없으면 첫 줄(`warning:` 이 먼저 올 수 있다)
|
|
979
|
-
//
|
|
980
|
-
//
|
|
981
|
-
const
|
|
1275
|
+
// git 오류의 원인 줄: `fatal:` 이나 `error:` 로 시작하는 첫 줄, 없으면 첫 줄(`warning:` 이 먼저 올 수 있다), 정화하고
|
|
1276
|
+
// 200자까지, 아직 가리지 않은 글. git 이 오류 문구 없이 끝나면 Node 의 문구(명령 줄 전체, 곧 URL 전체가 든다)를 쓰지 않고
|
|
1277
|
+
// 어떻게 끝났는지만 댄다.
|
|
1278
|
+
const gitLine = (e) => {
|
|
982
1279
|
const lines = String(e.stderr || "").split("\n").filter((l) => l.trim());
|
|
983
1280
|
if (!lines.length) {
|
|
984
1281
|
return e.status == null && !e.signal && e.code ? `git 을 실행하지 못함 (${oneLine(e.code)})` : `git 이 오류 문구 없이 끝남 (${oneLine(e.signal || e.status)})`;
|
|
985
1282
|
}
|
|
986
|
-
|
|
987
|
-
return oneLine(redactUrl(line)).slice(0, 200);
|
|
1283
|
+
return oneLine(lines.find((l) => /^(fatal|error):/.test(l)) || lines[0]).slice(0, 200);
|
|
988
1284
|
};
|
|
1285
|
+
// 출력에 싣는 꼴: 주소마다 git 이 찍은 주소로 정한 꼴(shownAddress), 나머지는 가림.
|
|
1286
|
+
const shownGit = (line) => redactUrl(shownAddresses(line));
|
|
989
1287
|
|
|
990
1288
|
function pinMarketplace() {
|
|
991
1289
|
if (!existsSync(MARKET)) return "no-marketplace";
|
|
@@ -995,14 +1293,14 @@ function pinMarketplace() {
|
|
|
995
1293
|
if (!existsSync(join(MARKET, ".git"))) return "no-git";
|
|
996
1294
|
const url = originUrl();
|
|
997
1295
|
if (url === null) return "no-remote";
|
|
998
|
-
if (typeof url !== "string") { bad(`origin 을 읽지 못함 (${url.unreadable}): ${oneLine(MARKET)}`); return "origin-unreadable"; }
|
|
1296
|
+
if (typeof url !== "string") { bad(`origin 을 읽지 못함 (${shownGit(url.unreadable)}): ${oneLine(MARKET)}`); return "origin-unreadable"; }
|
|
999
1297
|
|
|
1000
1298
|
if (MODE === "revert") return unpinMarketplace(url);
|
|
1001
|
-
if (url.startsWith(
|
|
1002
|
-
if (MODE === "check") { bad(`마켓플레이스 미고정 (origin=${oneLine(
|
|
1299
|
+
if (url.startsWith(PIN)) return alreadyPinned();
|
|
1300
|
+
if (MODE === "check") { bad(`마켓플레이스 미고정 (origin=${oneLine(shownOrigin(url))})`); return "needs"; }
|
|
1003
1301
|
|
|
1004
1302
|
try { git(["remote", "set-url", "--", "origin", `omc-pinned://${url}`]); }
|
|
1005
|
-
catch (e) { bad(`고정 실패: origin 을 바꾸지 못했습니다 (${
|
|
1303
|
+
catch (e) { bad(`고정 실패: origin 을 바꾸지 못했습니다 (${shownGit(gitLine(e))})`); return "pin-failed"; }
|
|
1006
1304
|
return writePinRecord() ? "pinned" : "record-failed";
|
|
1007
1305
|
}
|
|
1008
1306
|
|
|
@@ -1011,25 +1309,33 @@ function pinMarketplace() {
|
|
|
1011
1309
|
// URL 은 남기지 않는다. 원래 URL 은 origin 의 `omc-pinned://` 뒤에 있고, 인증 정보가 든 URL 이 기록에 남는다.
|
|
1012
1310
|
// 고정이 된 뒤에만 쓴다. 실패한 고정의 기록은 남기지 않는다. 기록을 쓰지 못하면 고정은 둔 채 실패로 센다.
|
|
1013
1311
|
// 덮어쓴 옛 기록에 URL 칸이 있었고 같은 파일의 다른 이름이 있으면, 그쪽에 남은 칸을 알리고 실패로 센다.
|
|
1312
|
+
// 판은 패치한 활성 판이다(--no-update 면 그 판, 인자 없는 적용이면 전환한 판). 기록 폴더는 처음 만들 때 이 계정만 쓰게(0700) 한다.
|
|
1014
1313
|
function writePinRecord() {
|
|
1015
1314
|
let ino = null;
|
|
1315
|
+
let held = null;
|
|
1016
1316
|
try {
|
|
1017
|
-
mkdirSync(dirname(PINFILE), { recursive: true });
|
|
1317
|
+
mkdirSync(dirname(PINFILE), { recursive: true, mode: 0o700 });
|
|
1018
1318
|
if (isLink(PINFILE)) throw new Error("링크라 따라가 쓰지 않음");
|
|
1019
|
-
|
|
1020
|
-
|
|
1319
|
+
held = urlInRecord();
|
|
1320
|
+
ino = held ? sharedIno() : null;
|
|
1321
|
+
writeAtomic(PINFILE, JSON.stringify({ pinnedAt: new Date().toISOString(), version: active, host: hostname() }, null, 2) + "\n", 0o600, { follow: false });
|
|
1021
1322
|
} catch (e) {
|
|
1022
1323
|
bad(`고정 기록(PINNED)을 쓰지 못함 (${failWhy(e)}). origin 은 고정됨: ${oneLine(PINFILE)}`);
|
|
1023
1324
|
return false;
|
|
1024
1325
|
}
|
|
1025
|
-
return ino === null ? true : sharedLeft(
|
|
1326
|
+
return ino === null ? true : sharedLeft(sharedWhy(held), ino);
|
|
1026
1327
|
}
|
|
1027
1328
|
|
|
1028
|
-
//
|
|
1029
|
-
//
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1329
|
+
// 덮어쓰거나 지울 옛 기록이 URL 을 담는가. "yes" 는 JSON 의 최상위 칸을 봄, "maybe" 는 읽지 못했거나 JSON 이 아닌 글에
|
|
1330
|
+
// originUrl 이 있음(칸인지 모름), null 은 없음(일반 파일로 이어지지 않는 자리 포함). 같은 파일의 다른 이름을 알릴지와
|
|
1331
|
+
// 그 문구("남음", "남았을 수 있음")를 가른다.
|
|
1332
|
+
function urlInRecord() {
|
|
1333
|
+
let text;
|
|
1334
|
+
try { text = pinRecordText(); } catch { return "maybe"; }
|
|
1335
|
+
if (text === null) return null;
|
|
1336
|
+
const { ok, rec } = parseRecord(text);
|
|
1337
|
+
if (ok) return hasUrlKey(rec) ? "yes" : null;
|
|
1338
|
+
return text.includes("originUrl") ? "maybe" : null;
|
|
1033
1339
|
}
|
|
1034
1340
|
|
|
1035
1341
|
// 이미 고정된 origin. 옛 기록의 URL 칸을 지우지 못했으면 고정은 그대로 두고 실패로 센다.
|
|
@@ -1053,6 +1359,8 @@ function scrubPinRecord() {
|
|
|
1053
1359
|
return ino === null ? true : sharedLeft(SHARED_WHY, ino);
|
|
1054
1360
|
}
|
|
1055
1361
|
const SHARED_WHY = "이 이름에서만 지움 (같은 파일의 다른 이름이 있어 그쪽에는 남음)";
|
|
1362
|
+
const SHARED_MAYBE_WHY = "이 이름에서만 지움 (같은 파일의 다른 이름이 있어 그쪽에 남았을 수 있음)";
|
|
1363
|
+
const sharedWhy = (held) => (held === "yes" ? SHARED_WHY : SHARED_MAYBE_WHY);
|
|
1056
1364
|
const SHARED_CHECK_WHY = "이 이름에서만 지울 수 있음 (같은 파일의 다른 이름이 있어 그쪽에는 남음)";
|
|
1057
1365
|
|
|
1058
1366
|
// 기록에 같은 파일의 다른 이름(하드 링크)이 있으면 그 파일의 inode(Windows 는 파일 ID, 정밀도를 잃지 않게 BigInt)다.
|
|
@@ -1068,7 +1376,7 @@ function sharedIno() {
|
|
|
1068
1376
|
const sharedLeft = (what, ino) => pinUrlLeft(what, `\n 다른 이름은 같은 파일 시스템에서 inode(Windows 는 파일 ID) ${ino} 로 찾습니다.`);
|
|
1069
1377
|
|
|
1070
1378
|
// --check 의 판정. 남은 칸을 적용이 지울 수 있으면 scrub-needs, 이 도구로 지울 수 없으면 그 까닭을 알리고
|
|
1071
|
-
// scrub-blocked 다. 남은 칸이 없으면 already 다.
|
|
1379
|
+
// scrub-blocked 다. 기록을 읽지 못했거나 JSON 이 아니어서 칸이 있는지 모르면 scrub-unsure 다. 남은 칸이 없으면 already 다.
|
|
1072
1380
|
function pinUrlCheck() {
|
|
1073
1381
|
const left = pinUrlLeftover();
|
|
1074
1382
|
if (!left) return "already";
|
|
@@ -1077,7 +1385,7 @@ function pinUrlCheck() {
|
|
|
1077
1385
|
if (why || ino !== null) {
|
|
1078
1386
|
if (why) pinUrlLeft(why);
|
|
1079
1387
|
else sharedLeft(SHARED_CHECK_WHY, ino);
|
|
1080
|
-
return "scrub-blocked";
|
|
1388
|
+
return left.unsure ? "scrub-unsure" : "scrub-blocked";
|
|
1081
1389
|
}
|
|
1082
1390
|
bad(`고정 기록(PINNED)에 옛 URL 칸이 남아 있음 (적용하면 지움): ${oneLine(PINFILE)}`);
|
|
1083
1391
|
return "scrub-needs";
|
|
@@ -1095,12 +1403,13 @@ function pinDirWhy() {
|
|
|
1095
1403
|
function pinUrlLeftover() {
|
|
1096
1404
|
let text;
|
|
1097
1405
|
try { text = pinRecordText(); }
|
|
1098
|
-
catch (e) { return { why: `확인하지 못함 (읽지 못함: ${failWhy(e)})
|
|
1406
|
+
catch (e) { return { why: `확인하지 못함 (읽지 못함: ${failWhy(e)})`, unsure: true }; }
|
|
1099
1407
|
if (text === null) return null;
|
|
1100
1408
|
const { ok, rec } = parseRecord(text);
|
|
1101
1409
|
if (ok ? !hasUrlKey(rec) : !text.includes("originUrl")) return null;
|
|
1102
1410
|
if (isLink(PINFILE)) return { why: "지울 수 없음 (링크라 따라가 쓰지 않음)" };
|
|
1103
|
-
|
|
1411
|
+
// JSON 이 아닌 글은 originUrl 이 칸인지 알 수 없다. 읽지 못한 기록과 함께 "남았을 수 있음" 이다(unsure).
|
|
1412
|
+
return ok ? { rec } : { why: "지울 수 없음 (JSON 이 아님)", unsure: true };
|
|
1104
1413
|
}
|
|
1105
1414
|
|
|
1106
1415
|
// 기록의 글을 JSON 으로 읽는다. 읽히면 ok 가 true 다. 최상위 칸만 본다: 글에 그 이름이 있어도 칸이 아니면 없는 것이다.
|
|
@@ -1139,7 +1448,7 @@ function unpinMarketplace(url) {
|
|
|
1139
1448
|
if (!restored) return "not-pinned";
|
|
1140
1449
|
try { git(["remote", "set-url", "--", "origin", restored]); }
|
|
1141
1450
|
catch (e) {
|
|
1142
|
-
bad(`고정 해제 실패: origin 을 되돌리지 못했습니다 (${
|
|
1451
|
+
bad(`고정 해제 실패: origin 을 되돌리지 못했습니다 (${shownGit(gitLine(e))})`);
|
|
1143
1452
|
log(` 원래 URL(호스트까지만): ${oneLine(hostOnly(restored))}. 원문은 origin 의 omc-pinned:// 뒤에 그대로 있습니다.`);
|
|
1144
1453
|
return "unpin-failed";
|
|
1145
1454
|
}
|
|
@@ -1155,14 +1464,15 @@ function dropPinRecord() {
|
|
|
1155
1464
|
if (left) return pinUrlLeft(left.why);
|
|
1156
1465
|
let saved = null;
|
|
1157
1466
|
try { saved = JSON.parse(pinRecordText()); } catch { /* 깨졌거나 읽지 못하면 무시 */ }
|
|
1158
|
-
const
|
|
1467
|
+
const held = urlInRecord();
|
|
1468
|
+
const ino = held ? sharedIno() : null;
|
|
1159
1469
|
try { unlinkSync(PINFILE); }
|
|
1160
1470
|
catch (e) {
|
|
1161
1471
|
bad(`고정 기록(PINNED)을 지우지 못함 (${failWhy(e)}): ${oneLine(PINFILE)}`);
|
|
1162
1472
|
return false;
|
|
1163
1473
|
}
|
|
1164
1474
|
warnForeignPin(saved);
|
|
1165
|
-
return ino === null ? true : sharedLeft(
|
|
1475
|
+
return ino === null ? true : sharedLeft(sharedWhy(held), ino);
|
|
1166
1476
|
}
|
|
1167
1477
|
|
|
1168
1478
|
// 고정 기록 자리에 무엇이 있는가(끊어진 링크 포함). 위 폴더를 열 수 없어 확인하지 못하면 있는 것으로 본다.
|
|
@@ -1173,7 +1483,7 @@ function pinRecordThere() {
|
|
|
1173
1483
|
|
|
1174
1484
|
// 다른 기기의 기록이면 알린다. 그 값은 쓰지 않는다.
|
|
1175
1485
|
function warnForeignPin(saved) {
|
|
1176
|
-
if (saved && typeof saved === "object" && saved.host && saved.host !== hostname()) {
|
|
1486
|
+
if (saved && typeof saved === "object" && typeof saved.host === "string" && saved.host && saved.host !== hostname()) {
|
|
1177
1487
|
bad(`PINNED 는 다른 기기(${shown(saved.host)})의 기록입니다 — 그 값은 쓰지 않습니다.`);
|
|
1178
1488
|
}
|
|
1179
1489
|
}
|
|
@@ -1261,6 +1571,8 @@ log();
|
|
|
1261
1571
|
// targets 는 «고칠 플러그인 폴더» 의 이름표 목록이다. 기본은 캐시의 판 번호이고, --root 는
|
|
1262
1572
|
// 그 폴더 경로 하나다. rootOf 가 이름표를 폴더로 바꾼다.
|
|
1263
1573
|
let active, targets;
|
|
1574
|
+
// installed_plugins.json 에서 활성 판을 찾지 못해 추정했는가. 적용은 멈추고 --check 는 그렇다고 결과 줄에 낸다.
|
|
1575
|
+
let activeGuessed = false;
|
|
1264
1576
|
const rootOf = (v) => (ROOT ? v : join(CACHE, v));
|
|
1265
1577
|
// 헬퍼 자리 결함은 이 판의 apply 로 고칠 수 없다(이미 감싼 파일). 원본으로 되돌린 뒤 다시 적용한다.
|
|
1266
1578
|
// 되돌리기는 그 폴더 하나만 한다. 인자 없는 --revert 는 설치된 모든 판을 되돌리고 고정까지 푼다.
|
|
@@ -1293,12 +1605,18 @@ if (ROOT) {
|
|
|
1293
1605
|
// [1/4] 뒤에 계산해야 일반 apply 가 TARGET_VERSION 으로 전환한 결과가 반영된다.
|
|
1294
1606
|
const { v, src: activeSrc } = activeVersion();
|
|
1295
1607
|
active = v;
|
|
1608
|
+
activeGuessed = activeSrc.startsWith("추정");
|
|
1296
1609
|
if (MODE !== "revert" && (MODE === "check" || NO_UPDATE)) {
|
|
1297
1610
|
log(` 활성 버전: ${oneLine(active)} (${activeSrc})`);
|
|
1298
1611
|
if (active !== TARGET_VERSION) {
|
|
1299
1612
|
log(` 활성 버전이 대상(${TARGET_VERSION})과 다름 — 기본 apply 는 ${TARGET_VERSION} 로 전환하고, --no-update 는 활성 버전(${oneLine(active)})을 그 자리에서 패치함`);
|
|
1300
1613
|
}
|
|
1301
1614
|
}
|
|
1615
|
+
// 추정한 판에 적용하면 Claude Code 가 쓰는 판은 그대로 남고, 그 뒤 --check 는 추정한 판을 보고 적용됐다고 한다. 쓰기 전에 멈춘다.
|
|
1616
|
+
if (MODE === "apply" && activeGuessed) {
|
|
1617
|
+
bad(`활성 버전을 installed_plugins.json 에서 확인하지 못해 적용하지 않음 (추정한 판 ${oneLine(active)})`);
|
|
1618
|
+
process.exit(1);
|
|
1619
|
+
}
|
|
1302
1620
|
// apply 는 활성 버전만 손댄다. revert 와 check 는 설치된 모든 버전을 훑는다 —
|
|
1303
1621
|
// 여러 버전이 나란히 있을 때 이전 버전에 수정이 남으면, 롤백했을 때 예상과 다른
|
|
1304
1622
|
// 상태가 된다(실제로 4.15.7 의 hooks.json 수정이 그렇게 남았다).
|
|
@@ -1311,6 +1629,7 @@ log(`[2/4] 훅 패치${MODE === "apply" || ROOT ? ` (${oneLine(active)})` : `
|
|
|
1311
1629
|
const r = { done: 0, skip: 0, fail: 0, need: 0, misplaced: 0 };
|
|
1312
1630
|
const needBy = {};
|
|
1313
1631
|
const misplacedBy = {};
|
|
1632
|
+
const unreadBy = {};
|
|
1314
1633
|
const foreignBy = {};
|
|
1315
1634
|
const orphanBy = {};
|
|
1316
1635
|
// 되돌리기 끝 줄이 세는 파일. 쓰지 못한 것(failed)과 백업이 없어 남은 것(left)이다. 링크로 이어진 판 폴더는
|
|
@@ -1324,6 +1643,7 @@ for (const v of targets) {
|
|
|
1324
1643
|
const x = patchHooks(root);
|
|
1325
1644
|
needBy[v] = x.need;
|
|
1326
1645
|
misplacedBy[v] = x.misplaced;
|
|
1646
|
+
unreadBy[v] = x.unread;
|
|
1327
1647
|
for (const k of ["done", "skip", "fail", "need", "misplaced"]) r[k] += x[k];
|
|
1328
1648
|
if (x.foreign?.length) foreignBy[v] = x.foreign;
|
|
1329
1649
|
if (x.orphan?.length) orphanBy[v] = x.orphan;
|
|
@@ -1336,12 +1656,19 @@ log(` ${MODE === "check" ? `패치 필요 ${r.need}건` : MODE === "revert" ? `
|
|
|
1336
1656
|
function reportNoBackup(v, list, what) {
|
|
1337
1657
|
bad(`${oneLine(v)}: ${what} — ${list.map(shown).join(", ")}`);
|
|
1338
1658
|
log(` 백업(.omcbak)이 없어 되돌릴 원본이 디스크에 없습니다. 원본이 필요하면:`);
|
|
1339
|
-
if (ROOT) log(` 같은 판의 상류 원본에서 scripts/<이름>.mjs 를 다시 가져오십시오.`);
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
log(`
|
|
1659
|
+
if (ROOT) { log(` 같은 판의 상류 원본에서 scripts/<이름>.mjs 를 다시 가져오십시오.`); return; }
|
|
1660
|
+
const [market, dest] = [MARKET, join(CACHE, v)].map((p) => (oneLine(p) === p ? shellArg(p) : null));
|
|
1661
|
+
if (!market || !dest) {
|
|
1662
|
+
log(` 폴더 경로를 셸 명령에 그대로 넣을 수 없어 명령을 싣지 않습니다. 마켓플레이스 폴더 ${oneLine(MARKET)} 의 태그 v${oneLine(v)} 에서 scripts/<이름>.mjs 를 꺼내 ${oneLine(join(CACHE, v))} 에 푸십시오.`);
|
|
1663
|
+
return;
|
|
1343
1664
|
}
|
|
1665
|
+
log(` git -C ${market} archive -o "<임시 tar>" 'v${oneLine(v)}' 'scripts/<이름>.mjs'`);
|
|
1666
|
+
log(` tar -xf "<임시 tar>" -C ${dest}`);
|
|
1667
|
+
log(" <임시 tar> 는 새로 만든 임시 폴더(bash: mktemp -d) 안의 파일로 정하고, 다 쓴 뒤 폴더째 지우십시오.");
|
|
1344
1668
|
}
|
|
1669
|
+
// 손으로 칠 명령에 넣는 값. bash 와 PowerShell 모두 작은따옴표 안의 글은 그대로 읽는다(PowerShell 은 ‘ ’ ‚ ‛ 도 작은따옴표로
|
|
1670
|
+
// 읽는다). 그런 글자가 든 값은 큰따옴표로 감싸되, 두 셸이 큰따옴표 안에서 달리 읽는 글자($ ` " ! 와 “ ” „)도 들면 null 이다.
|
|
1671
|
+
const shellArg = (v) => (!/['\u{2018}-\u{201B}]/u.test(v) ? `'${v}'` : /[$`"!\u{201C}-\u{201E}]/u.test(v) ? null : `"${v}"`);
|
|
1345
1672
|
for (const [v, list] of Object.entries(foreignBy)) reportNoBackup(v, list, "이 도구가 만들지 않은 수정이 남아 있습니다");
|
|
1346
1673
|
for (const [v, list] of Object.entries(orphanBy)) reportNoBackup(v, list, "패치는 있는데 백업이 없어 되돌리지 못했습니다");
|
|
1347
1674
|
|
|
@@ -1398,6 +1725,7 @@ if (ROOT) {
|
|
|
1398
1725
|
"scrub-failed": () => ok("이미 고정됨"),
|
|
1399
1726
|
"scrub-needs": () => ok("이미 고정됨"),
|
|
1400
1727
|
"scrub-blocked": () => ok("이미 고정됨"),
|
|
1728
|
+
"scrub-unsure": () => ok("이미 고정됨"),
|
|
1401
1729
|
}[pin] || (() => {}))();
|
|
1402
1730
|
// 마켓플레이스를 못 찾아 고정하지 않은 PC 에도 옛 판이 남긴 기록은 있을 수 있다. 같은 판정으로 다룬다.
|
|
1403
1731
|
if (MODE !== "revert" && ["no-marketplace", "no-git", "no-remote"].includes(pin)) {
|
|
@@ -1423,27 +1751,42 @@ if (MODE === "check") {
|
|
|
1423
1751
|
// 헬퍼 자리 결함이 하나라도 있으면 «모두 적용» 이 아니다. 다시 적용만으로는 안 고쳐지므로
|
|
1424
1752
|
// 그 안내를 먼저 낸다(되돌린 뒤 적용하면 «패치 필요» 항목도 함께 처리된다).
|
|
1425
1753
|
const misplacedActive = misplacedBy[active] || 0;
|
|
1426
|
-
const
|
|
1754
|
+
const unreadActive = unreadBy[active] || 0;
|
|
1755
|
+
const pinNeeds = ["needs", "scrub-needs", "scrub-blocked", "scrub-unsure"].includes(pin);
|
|
1427
1756
|
const pinUnread = pin === "origin-unreadable";
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1430
|
-
//
|
|
1431
|
-
|
|
1432
|
-
const
|
|
1433
|
-
|
|
1434
|
-
:
|
|
1435
|
-
:
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
:
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1757
|
+
const toApply = (needBy[active] || 0) > 0 || !!hjNeedBy[active];
|
|
1758
|
+
const clean = !toApply && !hjFailBy[active] && !pinNeeds && !pinUnread && misplacedActive === 0 && !unreadActive && !activeGuessed;
|
|
1759
|
+
// 고정 기록 줄이 결과가 되는 때는 적용으로 고칠 항목이 없고 그 줄만 남은 때다. 고칠 항목이 함께 있으면 그 항목이 결과이고
|
|
1760
|
+
// 고정 기록 줄은 참고로 알린다(적용은 훅을 패치하고 그 칸은 남겨 실패로 센다).
|
|
1761
|
+
const pinOnly = !toApply && !hjFailBy[active] && !misplacedActive && !unreadActive && !activeGuessed;
|
|
1762
|
+
const PIN_LINES = {
|
|
1763
|
+
"scrub-blocked": "결과: 고정 기록(PINNED)의 옛 URL 칸이 이 도구로 지울 수 없는 곳에 있습니다. 위의 [!] 줄을 확인하십시오.",
|
|
1764
|
+
"scrub-unsure": "결과: 고정 기록(PINNED)을 확인하지 못했습니다. 옛 URL 칸이 남았을 수 있습니다. 위의 [!] 줄을 확인하십시오.",
|
|
1765
|
+
};
|
|
1766
|
+
const rerun = ROOT ? "--check 없이 같은 --root 로 다시 실행하십시오."
|
|
1767
|
+
: NO_UPDATE ? "--check 없이 --no-update 로 다시 실행하십시오." : "인자 없이 다시 실행하십시오.";
|
|
1768
|
+
// 결과 줄 표. 위에서부터 처음 맞는 줄 하나를 낸다. SKILL 1단계의 결과 줄 표도 이 순서다.
|
|
1769
|
+
const RESULTS = [
|
|
1770
|
+
[activeGuessed, "결과: 활성 버전을 확인하지 못했습니다. 위의 활성 버전 줄을 확인하십시오."],
|
|
1771
|
+
[hjFailBy[active], "결과: hooks.json 을 확인하지 못했습니다. 위의 [!] 줄을 확인하십시오."],
|
|
1772
|
+
[unreadActive, "결과: 훅 스크립트를 확인하지 못했습니다. 위의 [!] 줄을 확인하십시오."],
|
|
1773
|
+
[pinUnread, "결과: 마켓플레이스 origin 을 확인하지 못했습니다. 위의 [!] 줄을 확인하십시오."],
|
|
1774
|
+
[misplacedActive, `결과: 헬퍼 자리 결함 ${misplacedActive}건이 있습니다. ${misplacedHint()}`],
|
|
1775
|
+
[pinOnly && pin === "scrub-blocked", PIN_LINES["scrub-blocked"]],
|
|
1776
|
+
[pinOnly && pin === "scrub-unsure", PIN_LINES["scrub-unsure"]],
|
|
1777
|
+
[!clean, `결과: 적용되지 않은 항목이 있습니다. ${rerun}`],
|
|
1778
|
+
[true, "결과: 모두 적용된 상태입니다."],
|
|
1779
|
+
];
|
|
1780
|
+
log(RESULTS.find(([when]) => when)[1]);
|
|
1781
|
+
if (PIN_LINES[pin] && !pinOnly) {
|
|
1782
|
+
log(` 참고: ${PIN_LINES[pin].slice("결과: ".length)}`);
|
|
1783
|
+
log(` ${pin === "scrub-unsure" ? "칸이 남아 있으면 적용도 지우지 못하고" : "적용해도 그곳의 칸은 남고"}, 적용은 이 항목을 실패로 세어 종료 1 로 끝납니다.`);
|
|
1444
1784
|
}
|
|
1785
|
+
const others = targets.filter((v) => v !== active && ((needBy[v] || 0) > 0 || hjNeedBy[v] || (misplacedBy[v] || 0) > 0));
|
|
1445
1786
|
const unreadable = targets.filter((v) => v !== active && hjFailBy[v]);
|
|
1446
1787
|
if (unreadable.length) log(` 참고: 비활성 버전 ${unreadable.map(oneLine).join(", ")} 의 hooks.json 을 확인하지 못했습니다. 판정에는 넣지 않았습니다.`);
|
|
1788
|
+
const unreadHooks = targets.filter((v) => v !== active && unreadBy[v]);
|
|
1789
|
+
if (unreadHooks.length) log(` 참고: 비활성 버전 ${unreadHooks.map(oneLine).join(", ")} 의 훅 스크립트를 확인하지 못했습니다. 판정에는 넣지 않았습니다.`);
|
|
1447
1790
|
if (others.length) {
|
|
1448
1791
|
log(` 참고: 비활성 버전 ${others.map(oneLine).join(", ")} 에 미적용분이 있습니다.`);
|
|
1449
1792
|
log(" 지금 쓰이지 않는 버전이라 판정에 넣지 않았습니다. 그 버전으로 되돌릴 계획이면 함께 적용하십시오.");
|
|
@@ -1460,4 +1803,5 @@ if (MODE === "check") {
|
|
|
1460
1803
|
log(" 훅 스크립트 패치는 즉시 적용됩니다 (훅이 호출될 때마다 파일을 새로 읽습니다).");
|
|
1461
1804
|
log("[!] PostToolUse 훅 제거는 hooks.json 변경이라 각 세션이 다시 시작될 때부터 적용됩니다.");
|
|
1462
1805
|
}
|
|
1463
|
-
|
|
1806
|
+
// --check 가 활성 판을 추정했으면 확인하지 못한 것이라 종료 1 이다(읽지 못한 hooks.json, 훅, origin 과 같다).
|
|
1807
|
+
process.exit(r.fail > 0 || left.size > 0 || (MODE === "check" && activeGuessed) ? 1 : 0);
|