@kaydash9999/banker-plugins 0.20.0 → 0.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code skills + commands — QA, audit, docs, architecture, dependency setup, and wiki; the banker plugin marketplace.",
9
- "version": "0.20.0"
9
+ "version": "0.20.2"
10
10
  },
11
11
  "plugins": [
12
12
  {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "banker",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
4
4
  "description": "Claude Code plugin: QA/audit/docs/architecture skills + commands, a /banker:setup installer for OS dependencies (playwright, etc.), and wiki compaction.",
5
5
  "author": {
6
6
  "name": "smallOpenSource",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.20.2] - 2026-10-19
4
+
5
+ ### Added
6
+ - `setup-omc-hud` 설치 스크립트가 끝에 상태표시줄 래퍼를 입력 없이 한 번 돌려, OMC 를 불러오는지와 어느 판이 그리는지 알린다(`Render check`). 새 OMC 판이 불러오기에 실패해 옛 판으로 넘어가면 그 이유와 함께 알린다. 네트워크는 쓰지 않고, 설치 결과(종료 코드)는 바꾸지 않는다.
7
+
8
+ ### Changed
9
+ - 유지보수자용: payload-mon `status` 의 모듈 사본 없음 분기와 래퍼 v4 판별 꼴(주석의 `PL:` 글자)을 시험하고, 난수 대체 시험의 주석을 바로잡는다.
10
+
11
+ ### Fixed
12
+ - `setup-omc-hud` 를 다시 설치할 때마다 래퍼와 `settings.json` 의 백업이 하나씩 쌓이던 것. payload-mon, claude-update-last 블록만 다른 래퍼와 바뀌는 내용이 없는 `settings.json` 은 이제 백업하지 않는다.
13
+ - `setup-omc-hud` 설치 스크립트가 빈 `settings.json` 을 "JSON 객체가 아님" 으로 거부하던 것. Claude Code 처럼 `{}` 로 읽는다.
14
+ - `setup-omc-hud` 설치 스크립트가 주석이나 끝 쉼표가 든 `settings.json` 을 거부할 때 이유를 말하지 않던 것. 이제 그 이유와, Claude Code 도 이 파일을 읽지 못한다는 것을 알린다. Node 가 위치를 알려 주면 줄과 칸도 적는다. 파일 내용은 출력에 넣지 않는다.
15
+ - `payload-mon` 의 `on`, `off` 가 BOM 으로 시작하는 래퍼를 "node --check 실패" 로 거부하던 것. 그 래퍼는 node 로 정상 실행된다.
16
+ - `payload-mon` 블록이 CRLF 래퍼에 LF 줄끝으로 들어가던 것. 이제 래퍼의 줄끝을 따른다. 편집기가 래퍼 전체를 CRLF 로 저장한 뒤 `status` 가 "켜짐이 불완전" 이라 하던 것도 고친다. Windows 에서 Git 기본 설정으로 받은 설치본이 이런 꼴일 수 있다.
17
+ - 래퍼 파일만 다른 폴더로 링크하면(dotfiles 등) 래퍼 v4 의 `PL:` 칸이 보이지 않는데 `on` 과 `status` 는 PL 칸을 안내하던 것. 모듈 사본을 링크가 가리키는 파일 옆에 둔다. 0.20.1 로 켠 이런 설치는 `status` 가 불완전으로 알리고, `on` 한 번으로 고쳐진다.
18
+
19
+ ## [0.20.1] - 2026-10-19
20
+
21
+ ### Changed
22
+ - 유지보수자용: 암호 모듈이 없을 때의 난수 시험이 정확한 출력을 단언하고, 업데이트 확인과 체크인의 시험이 흉내가 `node:crypto` 를 가로챘는지 스스로 확인한다.
23
+
24
+ ### Fixed
25
+ - `payload-mon` 의 `on` 과 `status` 가 래퍼 v4 에서도 "8MB 이상이면 ctx 옆에 표시" 라고 안내하던 것. v4 는 크기를 `PL:` 칸에 늘 보이므로 이제 그렇게 안내한다. `status` 는 켜져 있을 때만 어디에 보이는지 적는다. 옛 래퍼의 `on` 안내는 그대로다. README 의 payload-mon 설명도 고친다.
26
+
3
27
  ## [0.20.0] - 2026-10-19
4
28
 
5
29
  ### Changed
package/README.md CHANGED
@@ -142,7 +142,7 @@ Claude Code 2.1.296 은 Claude 5 모델(Opus 5.5 등)에 작업 목록 도구(Ta
142
142
  | `update-banker` | 설치된 banker 를 3채널(Claude·npm·Codex) 순서 게이트로 최신화 |
143
143
  | `refresh-readme` | 코드와 어긋난 README 를 현재 상태에 맞게 갱신(드리프트 해소) |
144
144
  | `cleansing-memory` | 메모리 파일을 문서화된 threshold 내로 정리(중복 최신본화·무손실 압축) |
145
- | `payload-mon` | 상태표시줄 ctx 옆에 요청 payload 추정치(32MB 한도 대비, 8MB부터) 표시 켜기·끄기 |
145
+ | `payload-mon` | 상태표시줄에 요청 payload 추정치(32MB 한도 대비) 표시 켜기·끄기. 래퍼 v4 는 `PL:` 칸에 늘, 옛 래퍼는 8MB부터 ctx 옆에 |
146
146
  | `tone-compact` | 답변과 새 문서를 ASD-STE100 기반 한글 개조식(표·목록·원어 발음 표기·장식 기호 금지)으로 쓰는 문체 규칙 켜기·끄기(on 기본, 끌 때까지 유지) |
147
147
  | `anti-drm` | DRM 이 암호화하는 확장자(기본 11개: 텍스트·오피스·한글·PDF·CSV) 대신 대체 확장자(`.md`·`.html`·`.tsv`)로 파일을 만들게 하는 규칙 켜기·끄기(on 기본, 처음 켤 때 금지 확장자와 대체 확장자를 물음, `config` 로 변경, 끌 때까지 유지) |
148
148
  | `omc-patch` | OMC 자동 업데이트 뒤 사라진 훅 패치를 재적용하고 OMC 마켓플레이스 자동 업데이트를 고정(진단·확인·적용·검증·되돌리기). 옛 킷이 블록 안에 넣은 헬퍼(project-memory 꺼짐)를 찾아 고치고, project-memory 훅이 실제로 일하는지 임시 폴더에서 확인 |
@@ -180,7 +180,7 @@ USER_RESOURCES 가이드의 공통 요소를 OS별·런타임별(Claude Code/Cod
180
180
  | `setup-omc` | oh-my-claudecode(OMC) 설치·갱신 (Codex는 OMX) |
181
181
  | `setup-harness-factory` | revfactory/harness 팀 아키텍처 팩토리 설치·구성·사용안내 (Codex=meta-harness) |
182
182
  | `setup-playwright` | Playwright + headless 브라우저 (RHEL8/Rocky8·non-root·no-conda 폴백) |
183
- | `setup-omc-hud` | omc_hud 상태표시줄(래퍼 v4). 스킬에 든 파일로 오프라인 설치, 모든 OS 같은 명령 |
183
+ | `setup-omc-hud` | omc_hud 상태표시줄(래퍼 v4). 스킬에 든 파일로 오프라인 설치, 모든 OS 같은 명령, 설치 직후 렌더 확인 |
184
184
  | `setup-insane-search` | insane-search 플러그인 설치 (Claude·Codex) |
185
185
  | `setup-stitch` | Stitch 디자인 MCP 프록시 등록(RockyLinux8 proxy) |
186
186
  | `docs-setup` | arch-diagram·pdf-vision-extract 의존성(pptx·pymupdf·plantuml) 설치 |
@@ -247,7 +247,7 @@ banker uninstall # 제거
247
247
  | `setup-mcp` | `claude mcp add` (context7·seq-thinking·filesystem·git·fetch) | `[mcp_servers.*]` |
248
248
  | `setup-lsp` | LSP MCP 등록 | `[mcp_servers.lsp_bridge]` |
249
249
  | `setup-stitch` | `claude mcp add stitch` | `codex mcp add stitch` |
250
- | `setup-omc-hud` | `statusLine`(`settings.json` 백업) + `hud/omc-hud-custom.mjs`, `hud/omc-hud.mjs`, `.omc/hud-config.json`(바뀐 파일은 `.backup.<시각>` 백업) + payload-mon 켜기. 옛 판이 넣은 갱신 안내 블록은 `off` 로 원복 | — (Codex는 OMX `hud`) |
250
+ | `setup-omc-hud` | `statusLine`(바뀔 때 `settings.json` 백업) + `hud/omc-hud-custom.mjs`, `hud/omc-hud.mjs`, `.omc/hud-config.json`(바뀐 파일은 `.backup.<시각>` 백업) + payload-mon 켜기. 옛 판이 넣은 갱신 안내 블록은 `off` 로 원복 | — (Codex는 OMX `hud`) |
251
251
  | `setup-pwsh` | `env.CLAUDE_CODE_GIT_BASH_PATH` (병합) | — (네이티브 셸, 배선 불필요) |
252
252
  | `setup-harness-factory` | `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` (env 영속) | — |
253
253
  | `smart-compact` | `statusLine.command` 래핑 + `UserPromptSubmit` 훅 추가 (백업·멱등) | `~/.codex/` 대응 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaydash9999/banker-plugins",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
4
4
  "description": "banker — Claude Code + Codex CLI plugin (QA, audit, docs, architecture, wiki skills). Install via Claude Code marketplace, or `npm i -g @kaydash9999/banker-plugins` then `banker setup`.",
5
5
  "bin": {
6
6
  "banker": "bin/banker.js"
@@ -40,6 +40,7 @@ node "<이 스킬 디렉터리 절대경로>/scripts/payload-mon.mjs" <on|off|st
40
40
 
41
41
  - `on`: Claude Code 설정 폴더(`$CLAUDE_CONFIG_DIR`, 없으면 `~/.claude`)의 `hud/omc-hud-custom.mjs` 에서, ctx 를 넣는 줄 바로 뒤에 `// >>> payload-mon >>>` 블록 하나를 넣는다.
42
42
  블록이 불러오는 추정 모듈은 래퍼 옆 `hud/payload-mon/payload-size.mjs` 에 사본으로 둔다. 플러그인 설치 경로는 버전마다 바뀌어 업데이트 뒤에 사라지기 때문이다.
43
+ 래퍼 파일이 다른 곳을 가리키는 링크면 사본은 그 파일 옆에 둔다. 래퍼 v4 가 링크를 푼 자리에서 사본을 찾기 때문이다.
43
44
  이미 켜져 있으면 래퍼를 바꾸지 않고, 모듈 사본이 이 버전과 다를 때만 사본을 맞춘다. 예전 방식(블록 2개)으로 켜져 있었다면 새 방식으로 바꾼다.
44
45
  바꾸기 전 내용은 `omc-hud-custom.mjs.payload-mon.bak` 에 남는다.
45
46
  - `off`: 그 블록만 빼서 래퍼를 켜기 전 상태로 되돌리고, 모듈 사본과 추정치 캐시를 지운다. 래퍼 v4 에서는 모듈 사본이 없어져 `PL:` 칸도 사라진다.
@@ -59,7 +60,7 @@ node "<이 스킬 디렉터리 절대경로>/scripts/payload-mon.mjs" <on|off|st
59
60
  플러그인 파일을 손으로 고치지 말고, banker 를 최신으로 올린 뒤(`update-banker`) 다시 시도한다. 그래도 안 되면 banker 이슈로 알린다.
60
61
  - "node --check 실패": 블록을 넣은 결과가 문법 검사를 통과하지 못했다(예: 래퍼가 ctx 를 넣는 줄을 비동기가 아닌 함수 안으로 옮김). 파일은 그대로다.
61
62
  - "래퍼에 쓸 권한이 없습니다", 또는 `status` 의 "HUD 래퍼를 읽을 수 없음": 래퍼 파일의 권한을 확인한다. 파일은 그대로다.
62
- - "모듈 사본을 쓰지 못했습니다": 래퍼 옆 `hud/payload-mon/` 에 쓸 수 없다(권한, 같은 이름의 파일, 디스크 공간). 래퍼는 그대로다.
63
+ - "모듈 사본을 쓰지 못했습니다": 래퍼 옆(래퍼 파일이 링크면 링크가 가리키는 파일 옆) `payload-mon/` 에 쓸 수 없다(권한, 같은 이름의 파일, 디스크 공간). 래퍼는 그대로다.
63
64
  - "표시(>>> / <<<)의 짝이 맞지 않습니다": 래퍼에 payload-mon 표시 줄이 한쪽만 남았다. `on`·`off` 모두 거부하므로, 손으로 고치지 말라는 원칙의 유일한 예외다.
64
65
  `setup-omc-hud` 로 HUD 를 재설치하거나, 래퍼에서 짝 없이 남은 `// >>> payload-mon >>>` 또는 `// <<< payload-mon <<<` 줄을 지운 뒤 다시 실행한다.
65
66
  - HUD 를 재설치한 뒤 payload 가 안 보이면 `on` 을 다시 실행한다. `setup-omc-hud` 는 설치할 때마다 이 스킬을 늘 켠다(래퍼 v4 의 `PL:` 칸이 모듈 사본을 씀).
@@ -17,13 +17,20 @@
17
17
  * `const input`, runs the HUD in-process and keeps spawnSync for a fallback
18
18
  * branch. A block anchored on spawnSync would land in that branch, so the ctx
19
19
  * line is the only anchor. Earlier versions that also put a block before
20
- * spawnSync are converted on the next `on`.
20
+ * spawnSync are converted on the next `on`. Wrapper v4 (setup-omc-hud 0.20.0)
21
+ * shows the size in its own PL segment, so `on` and `status` point there.
21
22
  *
22
23
  * The block imports a copy of payload-size.mjs kept beside the wrapper
23
24
  * (<hud>/payload-mon/payload-size.mjs), not the module in this folder: a plugin
24
25
  * install lives under a versioned path that the next update retires, and an
25
26
  * import pointing there would go dark without a word. `on` writes the copy and
26
27
  * refreshes it when this version's module differs; `status` says when it does.
28
+ * When the wrapper file itself is a symlink, the copy goes beside the file it
29
+ * points at, where wrapper v4 looks.
30
+ *
31
+ * The block takes the ctx line's line ending, so a CRLF wrapper stays CRLF, and
32
+ * a block whose line endings an editor rewrote still counts as on. A wrapper
33
+ * that starts with a BOM is taken as it is.
27
34
  *
28
35
  * `on` converges on the current block and is a no-op when it is in place. It
29
36
  * writes only into the OMC custom HUD (signature-gated); `off` removes only
@@ -38,6 +45,7 @@ import {
38
45
  accessSync,
39
46
  constants,
40
47
  existsSync,
48
+ lstatSync,
41
49
  mkdirSync,
42
50
  mkdtempSync,
43
51
  readdirSync,
@@ -60,6 +68,8 @@ const BLOCKS = /^[ \t]*\/\/ >>> payload-mon >>>[\s\S]*?\/\/ <<< payload-mon <<<[
60
68
  const CTX_LINE = /^ {2}if \(f\.ctx\) colored\.push\(colorCtx\(f\.ctx\)\);\r?\n/m;
61
69
  // The statusline JSON the block reads, in either layout.
62
70
  const INPUT_DECL = /^(?:let|const) input\b/m;
71
+ // Wrapper v4 pushes its own PL segment: the size from 0MB on, in place of the block's segment.
72
+ const PL_SEGMENT = /colored\.push\(`PL:/;
63
73
  const MB = 1024 * 1024;
64
74
 
65
75
  // The same rules as setup-omc-hud's installer (OMC's getClaudeConfigDir): trim, expand a leading "~",
@@ -83,13 +93,29 @@ const DEFAULTS = {
83
93
  sessionId: process.env.CLAUDE_CODE_SESSION_ID,
84
94
  };
85
95
 
86
- // Where the block's module lives: beside the wrapper, outside any versioned path.
87
- const moduleCopy = (hudFile) => join(dirname(hudFile), "payload-mon", "payload-size.mjs");
96
+ // The file a wrapper that is itself a symlink (dotfiles) points at; any other path as it is. A wrapper
97
+ // reached through a linked folder keeps the path as configured.
98
+ function linkTarget(hudFile) {
99
+ try {
100
+ return lstatSync(hudFile).isSymbolicLink() ? realpathSync(hudFile) : hudFile;
101
+ } catch {
102
+ return hudFile;
103
+ }
104
+ }
105
+ // Where the block's module lives: beside the wrapper, outside any versioned path. For a wrapper file
106
+ // that is a link, beside the file it points at: wrapper v4 resolves its own path through the link and
107
+ // looks there for its PL segment.
108
+ const moduleCopy = (hudFile) => join(dirname(linkTarget(hudFile)), "payload-mon", "payload-size.mjs");
109
+ // Where 0.20.1 put the copy for such a link; `off` clears it too.
110
+ const besideLink = (hudFile) => join(dirname(hudFile), "payload-mon", "payload-size.mjs");
88
111
  const moduleUrl = (hudFile) => pathToFileURL(moduleCopy(hudFile)).href;
89
112
 
113
+ // The ctx line's line ending, which the block takes so that a CRLF wrapper stays CRLF.
114
+ const eolOf = (src) => (CTX_LINE.exec(src)?.[0].endsWith("\r\n") ? "\r\n" : "\n");
115
+
90
116
  // Right after ctx, colored like the 5h/wk rates: % on the ctx scale, MB in cyan.
91
117
  // Its names are block-scoped, so none can clash with the wrapper's own.
92
- const placeBlock = (url) =>
118
+ const placeBlock = (url, eol = "\n") =>
93
119
  [
94
120
  " " + OPEN,
95
121
  " // payload-mon: estimated API request payload against the 32MB request limit, shown",
@@ -107,9 +133,9 @@ const placeBlock = (url) =>
107
133
  " }",
108
134
  " " + CLOSE,
109
135
  "",
110
- ].join("\n");
136
+ ].join(eol);
111
137
 
112
- const insertBlock = (base, url) => base.replace(CTX_LINE, (line) => line + placeBlock(url));
138
+ const insertBlock = (base, url) => base.replace(CTX_LINE, (line) => line + placeBlock(url, eolOf(line)));
113
139
 
114
140
  const count = (text, needle) => text.split(needle).length - 1;
115
141
 
@@ -134,11 +160,14 @@ const readBytes = (file) => {
134
160
 
135
161
  const refusal = (why, hudFile) => ({ ok: false, message: `${why}. 래퍼를 그대로 두었습니다: ${hudFile}` });
136
162
 
137
- // The block exactly as `on` writes it for this URL, once, right after the ctx line.
138
- function inPlace(src, url) {
139
- if (count(src, OPEN) !== 1 || count(src, CLOSE) !== 1) return false;
163
+ // The line ending ("\n" or "\r\n") of the block that sits, exactly as `on` writes it for this URL,
164
+ // once, right after the ctx line; "" when it does not. Either ending counts as in place, so a block
165
+ // whose line endings an editor rewrote is still on.
166
+ function blockEol(src, url) {
167
+ if (count(src, OPEN) !== 1 || count(src, CLOSE) !== 1) return "";
140
168
  const ctx = CTX_LINE.exec(src);
141
- return ctx !== null && src.startsWith(placeBlock(url), ctx.index + ctx[0].length);
169
+ if (ctx === null) return "";
170
+ return ["\n", "\r\n"].find((eol) => src.startsWith(placeBlock(url, eol), ctx.index + ctx[0].length)) ?? "";
142
171
  }
143
172
 
144
173
  // Why the block cannot go into this wrapper, or "" when it can. Markers that do
@@ -199,12 +228,14 @@ function syncModule(o) {
199
228
  return true;
200
229
  }
201
230
 
202
- // The first lines of node's complaint about `text` as an ES module, "" if it parses.
231
+ // The first lines of node's complaint about `text` as an ES module, "" if it parses. A leading BOM
232
+ // is left out: node runs a wrapper that starts with one, but `node --check` rejects a BOM before a
233
+ // shebang (Node 20, 24).
203
234
  function syntaxError(text) {
204
235
  const dir = mkdtempSync(join(tmpdir(), "payload-mon-"));
205
236
  try {
206
237
  const file = join(dir, "check.mjs");
207
- writeFileSync(file, text);
238
+ writeFileSync(file, text.replace(/^\uFEFF/, ""));
208
239
  const r = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
209
240
  return r.status === 0 ? "" : String(r.stderr || r.error).trim().split("\n").slice(0, 3).join(" | ");
210
241
  } finally {
@@ -249,11 +280,21 @@ const notShownNote = (o) =>
249
280
  : `\n주의: statusLine 명령에 ${basename(o.hudFile)}가 보이지 않습니다. ` +
250
281
  "다른 래퍼(smart-compact 등)가 HUD를 감싸 호출하면 정상 표시되고, 아니면 화면에 나타나지 않습니다.";
251
282
 
283
+ const showsPL = (src) => src !== null && PL_SEGMENT.test(src);
284
+
285
+ // Where the size shows, named only while it does: the block in place and a
286
+ // module copy to import. "" otherwise, so status never promises a segment.
287
+ function displayWhere(o, src) {
288
+ if (src === null || blockEol(src, moduleUrl(o.hudFile)) === "" || copyState(o) === "missing") return "";
289
+ return showsPL(src) ? "PL 칸에 늘 표시" : `${SHOW_FROM_BYTES / MB}MB부터 표시`;
290
+ }
291
+
252
292
  export function turnOn(opts = {}) {
253
293
  const o = { ...DEFAULTS, ...opts };
254
294
  const src = readText(o.hudFile);
255
295
  if (src === null) return { ok: false, message: `HUD 래퍼가 없습니다: ${o.hudFile}` };
256
- if (inPlace(src, moduleUrl(o.hudFile))) {
296
+ // In place with the wrapper's own line ending; a block whose ending differs is written again.
297
+ if (blockEol(src, moduleUrl(o.hudFile)) === eolOf(src)) {
257
298
  try {
258
299
  const refreshed = syncModule(o);
259
300
  return { ok: true, message: (refreshed ? "모듈 사본을 이 버전으로 맞췄습니다." : "이미 켜져 있습니다 (변경 없음).") + notShownNote(o) };
@@ -263,7 +304,9 @@ export function turnOn(opts = {}) {
263
304
  }
264
305
  const why = misfit(src);
265
306
  if (why) return refusal(why, o.hudFile);
266
- const done = "켰습니다. 다음 statusline 갱신부터 8MB 이상이면 ctx 옆에 payload가 표시됩니다.";
307
+ const done = showsPL(src)
308
+ ? "켰습니다. 다음 statusline 갱신부터 PL 칸에 요청 크기가 늘 표시됩니다."
309
+ : "켰습니다. 다음 statusline 갱신부터 8MB 이상이면 ctx 옆에 payload가 표시됩니다.";
267
310
  const after = insertBlock(src.replace(BLOCKS, ""), moduleUrl(o.hudFile));
268
311
  return commit(o.hudFile, src, after, done + notShownNote(o), () => syncModule(o));
269
312
  }
@@ -278,20 +321,22 @@ function removeBlocks(o, src) {
278
321
 
279
322
  // What off leaves behind otherwise: the cache (when this account controls its
280
323
  // folder), and the module copy with its folder (only when nothing else is in
281
- // it). Neither failing undoes off.
324
+ // it), also the one 0.20.1 left beside a linked wrapper. None failing undoes off.
282
325
  function removeLeftovers(o) {
283
- const copy = moduleCopy(o.hudFile);
284
- for (const remove of [() => clearCache(o.cacheDir), () => rmSync(copy, { force: true })]) {
326
+ const copies = [...new Set([moduleCopy(o.hudFile), besideLink(o.hudFile)])];
327
+ for (const remove of [() => clearCache(o.cacheDir), ...copies.map((copy) => () => rmSync(copy, { force: true }))]) {
285
328
  try {
286
329
  remove();
287
330
  } catch {
288
331
  /* stale numbers or an unused copy; off stands */
289
332
  }
290
333
  }
291
- try {
292
- rmdirSync(dirname(copy));
293
- } catch {
294
- /* absent, or holds something that is not ours */
334
+ for (const copy of copies) {
335
+ try {
336
+ rmdirSync(dirname(copy));
337
+ } catch {
338
+ /* absent, or holds something that is not ours */
339
+ }
295
340
  }
296
341
  }
297
342
 
@@ -302,8 +347,12 @@ export function turnOff(opts = {}) {
302
347
  return res;
303
348
  }
304
349
 
305
- // "현재 세션: 0.8MB / 32MB (2%), 8MB부터 표시" for the session that runs this, "" if unknown.
306
- function sessionEstimate(o) {
350
+ // The estimate for the session that runs this, "" if unknown, ending with where
351
+ // it shows when `where` names it:
352
+ // "현재 세션: 0.8MB / 32MB (2%), PL 칸에 늘 표시" (wrapper v4, on)
353
+ // "현재 세션: 0.8MB / 32MB (2%), 8MB부터 표시" (older wrappers, on)
354
+ // "현재 세션: 0.8MB / 32MB (2%)" (off, or no module copy)
355
+ function sessionEstimate(o, where) {
307
356
  if (!o.sessionId || !existsSync(o.projectsDir)) return "";
308
357
  const file = `${o.sessionId}.jsonl`;
309
358
  const project = readdirSync(o.projectsDir).find((d) => existsSync(join(o.projectsDir, d, file)));
@@ -311,7 +360,7 @@ function sessionEstimate(o) {
311
360
  try {
312
361
  const bytes = estimatePayloadBytes(join(o.projectsDir, project, file), o.cacheDir);
313
362
  const pct = Math.floor((bytes / REQUEST_LIMIT_BYTES) * 100);
314
- return `현재 세션: ${(bytes / MB).toFixed(1)}MB / 32MB (${pct}%), ${SHOW_FROM_BYTES / MB}MB부터 표시`;
363
+ return `현재 세션: ${(bytes / MB).toFixed(1)}MB / 32MB (${pct}%)` + (where ? `, ${where}` : "");
315
364
  } catch {
316
365
  return "";
317
366
  }
@@ -328,7 +377,7 @@ function stateOf(src, o) {
328
377
  if (count(src, OPEN) + count(src, CLOSE) === 0) return "꺼짐";
329
378
  // on and off both refuse this, so pointing at on would be a dead end.
330
379
  if (count(src, OPEN) !== count(src, CLOSE)) return "표시(>>> / <<<)의 짝이 맞지 않습니다 (on·off 모두 거부: HUD 재설치 또는 남은 표시 줄 정리)";
331
- if (!inPlace(src, moduleUrl(o.hudFile))) return "켜짐이 불완전하거나 오래됨 (on 으로 복구)";
380
+ if (blockEol(src, moduleUrl(o.hudFile)) === "") return "켜짐이 불완전하거나 오래됨 (on 으로 복구)";
332
381
  return "켜짐" + COPY_NOTE[copyState(o)];
333
382
  }
334
383
 
@@ -347,7 +396,9 @@ export function status(opts = {}) {
347
396
  const lines = [`payload-mon: ${describe(o)} (${o.hudFile})`];
348
397
  const note = notShownNote(o);
349
398
  if (note) lines.push(note.trim());
350
- const estimate = sessionEstimate(o);
399
+ // null when the wrapper is missing or unreadable; describe has already said which.
400
+ const src = readBytes(o.hudFile)?.toString("utf8") ?? null;
401
+ const estimate = sessionEstimate(o, displayWhere(o, src));
351
402
  if (estimate) lines.push(estimate);
352
403
  return { ok: true, message: lines.join("\n") };
353
404
  }
@@ -72,13 +72,14 @@ node "<이 스킬 디렉터리 절대경로>\assets\apply-hud.mjs"
72
72
  ```
73
73
 
74
74
  - `[ok] statusLine -> node "<설정 폴더>/hud/omc-hud-custom.mjs"` 줄을 확인한다.
75
+ - `Render check` 줄을 확인한다. `[ok]` 면 정상이다. `[!!]` 나 `[..]` 면 그 줄을 그대로 보고한다. 이때도 설치는 끝났다(종료 0).
75
76
  - 기존 statusLine 이 다른 명령이었으면 `[..] statusLine ran ...` 줄이 나온다. 보고에 그대로 넣는다.
76
77
  - 종료 코드 1 이면 출력의 `[x]` 줄을 그대로 보고하고 멈춘다. 이때 바뀐 파일은 없다.
77
- - `settings.json` 이 JSON 객체가 아니면 사용자가 고친 뒤 다시 실행한다.
78
+ - `settings.json` 이 JSON 문법에 맞지 않거나(주석, 끝 쉼표 포함) 객체가 아니면 사용자가 고친 뒤 다시 실행한다. 빈 파일은 Claude Code 처럼 `{}` 로 읽는다.
78
79
  - OMC 가 없으면 OMC 를 먼저 설치한다(`setup-omc` 스킬). 이 스킬은 OMC 를 설치하지 않는다.
79
80
  - 설정 폴더 경로에 셸이 해석하는 글자가 있으면 그 글자가 없는 폴더를 쓰게 안내한다.
80
81
  - 쓸 수 없는 파일이나 폴더가 있으면 권한을 고친 뒤 다시 실행한다.
81
- - 종료 코드 2 면 예기치 못한 오류로 중간에 멈춘 것이다. 출력을 그대로 보고하고, 바뀐 파일의 이전 판은 `<파일>.backup.<시각>` 에 있다고 알린다.
82
+ - 종료 코드 2 면 예기치 못한 오류로 중간에 멈춘 것이다. 출력을 그대로 보고하고, 바꾼 파일의 이전 판은 `<파일>.backup.<시각>` 에 있다고 알린다. 로더와 도구 블록은 백업하지 않는다.
82
83
 
83
84
  ## 2. payload-mon 켜기
84
85
 
@@ -89,12 +90,13 @@ node "<이 스킬 디렉터리 절대경로>/../payload-mon/scripts/payload-mon.
89
90
  ```
90
91
 
91
92
  - Codex 사본에서 실행할 때 경로는 `<이 스킬 디렉터리 절대경로>/../banker-payload-mon/scripts/payload-mon.mjs` 다.
92
- - 이유: `PL:` 칸은 payload-mon 이 래퍼 옆(`hud/payload-mon/`)에 두는 크기 계산 모듈을 쓴다. 모듈이 없으면 `PL:` 이 나오지 않는다.
93
- - 이 단계의 출력 "8MB 이상이면 ctx 옆에 payload가 표시됩니다" 는 옛 래퍼 기준이다. 그대로 옮기지 말고 "PL 칸에 쓰는 모듈을 설치함" 으로 보고한다.
93
+ - 이유: `PL:` 칸은 payload-mon 이 래퍼 옆(`hud/payload-mon/`)에 두는 크기 계산 모듈을 쓴다. 모듈이 없으면 `PL:` 이 나오지 않는다. 래퍼 파일이 링크면 링크가 가리키는 파일 옆에 둔다.
94
+ - 출력 `켰습니다. 다음 statusline 갱신부터 PL 칸에 요청 크기가 늘 표시됩니다.` 를 확인한다. `이미 켜져 있습니다`, `모듈 사본을 이 버전으로 맞췄습니다` 도 정상이다.
94
95
 
95
96
  ## 3. 보고
96
97
 
97
98
  - 1단계 출력의 statusLine 줄과 백업 경로
99
+ - 1단계의 `Render check` 줄
98
100
  - 2단계 결과
99
101
  - statusLine 이 바뀌었으면 Claude Code 재시작 안내
100
102
 
@@ -113,6 +115,7 @@ node "<이 스킬 디렉터리 절대경로>/scripts/claude-update-last.mjs" off
113
115
  ## 함정
114
116
 
115
117
  - 다시 설치하면 래퍼와 `hud-config.json` 을 첨부 파일로 덮어쓴다. 손으로 고친 내용은 `.backup.<시각>` 에만 남는다.
118
+ - payload-mon, claude-update-last 블록만 다른 래퍼와 바뀌는 내용이 없는 `settings.json` 은 백업하지 않는다.
116
119
  - `settings.json` 의 `omcHud` 값은 `hud-config.json` 의 같은 키를 덮어쓴다. 래퍼가 기대는 아래 키를 바꾸면 칸이 달라지거나 OMC 원문이 그대로 나온다.
117
120
  - `omcLabel`, `useBars`
118
121
  - `maxWidth`, `maxOutputLines`
@@ -132,3 +135,5 @@ node "<이 스킬 디렉터리 절대경로>/scripts/claude-update-last.mjs" off
132
135
  - 설치는 네트워크를 쓰지 않는다. 다만 OMC 가 렌더할 때 네트워크로 얻는 정보(새 Claude Code 판 같은)는 오프라인에서 빠질 수 있다.
133
136
  - 래퍼, HUD 설정, 모듈 사본은 플러그인 밖(`<설정 폴더>/hud/`, `<설정 폴더>/.omc/`)에 있다. 플러그인을 지워도 상태표시줄은 남는다.
134
137
  - OMC 를 지울 때는 statusLine 도 지운다. OMC 로더는 플러그인을 못 찾으면 작업 폴더의 `node_modules` 에서 OMC HUD 를 찾아 불러오기 때문이다.
138
+ - 설치 끝의 렌더 확인은 래퍼를 입력 없이 한 번 돌려, OMC 진단의 첫 줄(`[OMC] HUD v<판> | preset: <이름>`)로 OMC 를 불러왔는지와 그린 판을 본다. 네트워크는 쓰지 않는다.
139
+ - 렌더 확인이 OMC 를 불러오지 못한다거나 옛 판이 그린다고 하면 OMC 쪽 문제다. `/oh-my-claudecode:omc-setup` 은 고치면서 statusLine 을 OMC 명령으로 되돌리므로, 쓴 뒤에는 이 스킬을 다시 실행한다. omc-patch 를 쓰는 PC 는 omc-patch 도 다시 적용한다.
@@ -7,7 +7,7 @@
7
7
  * - hud/lib/config-dir.mjs (loader dependency)
8
8
  * - hud/omc-hud-custom.mjs (the custom compact/colored formatter)
9
9
  * - .omc/hud-config.json (HUD preset/elements)
10
- * - settings.json (statusLine -> the custom formatter; existing file backed up)
10
+ * - settings.json (statusLine -> the custom formatter; backed up when it changes)
11
11
  *
12
12
  * Runs identically on Linux, macOS, and Windows (only needs node + an OMC install).
13
13
  * Run it directly with `node apply-hud.mjs` on every OS; the templates are read
@@ -15,16 +15,23 @@
15
15
  *
16
16
  * Based on smallOpenSource/omc_hud d3cedd5 (MIT, see LICENSE-omc_hud). Changes:
17
17
  * statusLine path quoted; a settings.json that is not a JSON object is refused
18
- * before any file changes (the original replaced it); UTF-8 BOM accepted;
19
- * CLAUDE_CONFIG_DIR trimmed, "~" expanded and made absolute; formatter and HUD
20
- * config backed up when they differ; pre-releases sort below their release; no
18
+ * before any file changes (the original replaced it), naming the line and
19
+ * column when Node reports one but never quoting it; one holding only white
20
+ * space is read as {}, as Claude Code does; UTF-8 BOM accepted; CLAUDE_CONFIG_DIR trimmed, "~" expanded
21
+ * and made absolute; formatter and HUD config backed up when they differ (the
22
+ * blocks payload-mon and claude-update-last add do not count), settings.json only
23
+ * when it changes; pre-releases sort below their release; no
21
24
  * folder is made when OMC is missing. Also refused before any change: a config
22
25
  * folder path the statusLine shell would act on, a file or folder it cannot
23
26
  * write, an OMC folder without both loader files, a relative OMC_PLUGIN_ROOT.
24
27
  * Backups keep the original bytes and mode; the files it writes are 0644 and new
25
28
  * folders 0755; the other statusLine fields (padding, ...) stay.
29
+ * At the end it runs the installed formatter once with no input and reports
30
+ * whether OMC's HUD loads and which version draws (the render check); that never
31
+ * changes the exit code. APPLY_HUD_CHECK_TIMEOUT_MS (tests) shortens its 25s limit.
26
32
  * Exit 1 means nothing changed; exit 2 means an unexpected error stopped it partway.
27
33
  */
34
+ import { spawnSync } from "node:child_process";
28
35
  import {
29
36
  accessSync,
30
37
  chmodSync,
@@ -34,10 +41,11 @@ import {
34
41
  mkdirSync,
35
42
  readdirSync,
36
43
  readFileSync,
44
+ statSync,
37
45
  writeFileSync,
38
46
  } from "node:fs";
39
47
  import { homedir } from "node:os";
40
- import { dirname, isAbsolute, join, resolve, sep } from "node:path";
48
+ import { delimiter, dirname, isAbsolute, join, resolve, sep } from "node:path";
41
49
  import { fileURLToPath } from "node:url";
42
50
 
43
51
  const SELF = dirname(fileURLToPath(import.meta.url));
@@ -72,18 +80,42 @@ const refuse = (lines) => {
72
80
  // file as it was. Refuse rather than replace: a fresh file would drop every
73
81
  // other setting. A BOM (Windows editors) is dropped when the file is written
74
82
  // back; the backup keeps the original bytes.
83
+ // Read as Claude Code (2.1.303) reads it: white space only is {}, and JSON
84
+ // with comments or trailing commas is invalid. A refusal says where the JSON
85
+ // breaks (from Node's "at position N") but never quotes it: Node's own message
86
+ // does, and the file can hold tokens.
87
+ function jsonErrorAt(text, message) {
88
+ const m = /at position (\d+)/.exec(message);
89
+ if (!m) return "";
90
+ const before = text.slice(0, Number(m[1]));
91
+ return `line ${before.split("\n").length}, column ${before.length - before.lastIndexOf("\n")}`;
92
+ }
93
+ const kindOf = (v) => (v === null ? "null" : Array.isArray(v) ? "an array" : `a ${typeof v}`);
94
+ function readSettings(raw) {
95
+ const text = raw.replace(/^\uFEFF/, "");
96
+ if (text.trim() === "") return { settings: {}, empty: true };
97
+ let value;
98
+ try {
99
+ value = JSON.parse(text);
100
+ } catch (e) {
101
+ const at = jsonErrorAt(text, String(e && e.message));
102
+ return {
103
+ refusal:
104
+ `[x] settings.json is not valid JSON${at ? ` (${at})` : ""}; nothing changed. ` +
105
+ "Claude Code cannot read it either: comments and trailing commas are not allowed. Fix it and re-run.",
106
+ };
107
+ }
108
+ if (value && typeof value === "object" && !Array.isArray(value)) return { settings: value, empty: false };
109
+ return { refusal: `[x] settings.json holds ${kindOf(value)}, not a JSON object; nothing changed. Fix it and re-run.` };
110
+ }
75
111
  let settings = {};
76
112
  let settingsRaw = null;
77
113
  if (existsSync(settingsPath)) {
78
114
  settingsRaw = readFileSync(settingsPath, "utf8");
79
- try {
80
- settings = JSON.parse(settingsRaw.replace(/^\uFEFF/, ""));
81
- } catch {
82
- settings = undefined;
83
- }
84
- if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
85
- refuse(["[x] Existing settings.json is not a JSON object; nothing changed. Fix it and re-run."]);
86
- }
115
+ const read = readSettings(settingsRaw);
116
+ if (read.refusal) refuse([read.refusal]);
117
+ settings = read.settings;
118
+ if (read.empty) log("[..] settings.json is empty; read as {} as Claude Code does");
87
119
  }
88
120
 
89
121
  // Inside the command's double quotes these still act: a quote ends the path, $ and `
@@ -166,10 +198,23 @@ function writeOwned(file, data) {
166
198
  if (process.platform !== "win32") chmodSync(file, FILE_MODE);
167
199
  }
168
200
 
201
+ // The marker blocks payload-mon and claude-update-last add to the installed formatter, matched as those
202
+ // tools match them (payload-mon.mjs BLOCKS, claude-update-last.mjs's block pattern). Read as latin1,
203
+ // which maps every byte to one character, so text outside the blocks compares byte for byte.
204
+ const TOOL_BLOCKS = [
205
+ /^[ \t]*\/\/ >>> payload-mon >>>[\s\S]*?\/\/ <<< payload-mon <<<[ \t]*\r?\n/gm,
206
+ /^[ \t]*\/\/ >>> claude-update-last >>>(?:(?!\/\/ >>> claude-update-last >>>)[\s\S])*?\/\/ <<< claude-update-last <<<[ \t]*(?:\r?\n|(?![\s\S]))/gm,
207
+ ];
208
+ const withoutToolBlocks = (text) => TOOL_BLOCKS.reduce((t, blocks) => t.replace(blocks, ""), text);
209
+ const sameBytes = (a, b) => a.equals(b);
210
+ // Those blocks alone are no reason for a backup: `payload-mon on` puts its block back, and wrapper v4
211
+ // needs no claude-update-last block.
212
+ const sameFormatter = (a, b) => withoutToolBlocks(a.toString("latin1")) === withoutToolBlocks(b.toString("latin1"));
213
+
169
214
  // Keep a timestamped copy, with its bytes and mode, of a file that differs from what replaces it.
170
- function backupIfDifferent(dst, next, label) {
215
+ function backupIfDifferent(dst, next, label, same = sameBytes) {
171
216
  if (!existsSync(dst)) return;
172
- if (readFileSync(dst).equals(next)) return;
217
+ if (same(readFileSync(dst), next)) return;
173
218
  const keep = dst + ".backup." + stamp;
174
219
  copyFileSync(dst, keep);
175
220
  log(`[ok] Backed up ${label} -> ${keep}`);
@@ -188,12 +233,12 @@ function installLoader() {
188
233
  }
189
234
 
190
235
  // 3. Install the custom formatter and HUD config from the bundled templates.
191
- // Files that differ from the templates (local edits, blocks added by
192
- // payload-mon / claude-update-last) are kept as timestamped backups first.
236
+ // Files that differ from the templates (local edits) are kept as timestamped
237
+ // backups first; the blocks payload-mon / claude-update-last add do not count.
193
238
  function installTemplates() {
194
239
  const wrapperSrc = readFileSync(join(SELF, "omc-hud-custom.mjs"));
195
240
  const configSrc = readFileSync(join(SELF, "hud-config.json"));
196
- backupIfDifferent(wrapperDst, wrapperSrc, "previous formatter");
241
+ backupIfDifferent(wrapperDst, wrapperSrc, "previous formatter", sameFormatter);
197
242
  backupIfDifferent(configDst, configSrc, "previous HUD config");
198
243
  writeOwned(wrapperDst, wrapperSrc);
199
244
  writeOwned(configDst, configSrc);
@@ -202,13 +247,9 @@ function installTemplates() {
202
247
  }
203
248
 
204
249
  // 4. Point settings.json statusLine at the custom formatter; its other fields (padding, ...) stay.
250
+ // A settings.json that already says exactly this is neither backed up nor written.
205
251
  const objectOr = (v) => (v && typeof v === "object" && !Array.isArray(v) ? v : {});
206
252
  function pointStatusLine() {
207
- if (settingsRaw !== null) {
208
- const backup = settingsPath + ".backup." + stamp;
209
- copyFileSync(settingsPath, backup); // the original bytes and mode
210
- log("[ok] Backed up settings.json -> " + backup);
211
- }
212
253
  // Quoted so a config dir with spaces (C:/Users/First Last/.claude) still runs.
213
254
  const command = `node "${cmdPath}"`;
214
255
  const prior = objectOr(settings.statusLine);
@@ -216,19 +257,117 @@ function pointStatusLine() {
216
257
  log(`[..] statusLine ran ${JSON.stringify(prior.command)}; it now runs the custom HUD (the old value is in the backup)`);
217
258
  }
218
259
  settings.statusLine = { ...prior, type: "command", command };
219
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
260
+ const next = JSON.stringify(settings, null, 2) + "\n";
261
+ if (next === settingsRaw) {
262
+ log(`[ok] statusLine -> ${command} (settings.json unchanged, no backup)`);
263
+ return;
264
+ }
265
+ if (settingsRaw !== null) {
266
+ const backup = settingsPath + ".backup." + stamp;
267
+ copyFileSync(settingsPath, backup); // the original bytes and mode
268
+ log("[ok] Backed up settings.json -> " + backup);
269
+ }
270
+ writeFileSync(settingsPath, next);
220
271
  log("[ok] statusLine -> " + command);
221
272
  }
222
273
 
274
+ // 5. Render check: run the installed formatter once with no input, as the statusLine command does,
275
+ // with `node` from PATH. With no input OMC prints its diagnostic, whose first line names the version
276
+ // that loaded ("[OMC] HUD v5.7.4 | preset: full"), and uses no network. Reported only: the install
277
+ // is done either way. The 25s limit outwaits the formatter, which gives up on its own within 20s
278
+ // and then ends its fallback child; killing it sooner would leave that child running.
279
+ // APPLY_HUD_CHECK_TIMEOUT_MS shortens the limit for tests.
280
+ const CHECK_MS = Number(process.env.APPLY_HUD_CHECK_TIMEOUT_MS) || 25000;
281
+ const BLAME =
282
+ " OMC's install is at fault. /oh-my-claudecode:omc-setup repairs it but points statusLine back at OMC, so run setup-omc-hud again after it.";
283
+ const clip = (s, n = 200) => String(s).slice(0, n);
284
+ // No input, and the config folder as working folder: OMC's loader looks in the working folder's
285
+ // node_modules and runs git from it, and Windows looks there first for a bare program name, so the
286
+ // folder the installer was started in (a project, say) must not be it.
287
+ const RUN_OPTS = { input: "", encoding: "utf8", timeout: CHECK_MS, windowsHide: true, cwd: CONFIG_DIR };
288
+
289
+ // node as the statusLine shell finds it, but only in PATH folders given as absolute paths: spawning the
290
+ // bare name would on Windows try the working folder first, where a project's own node.exe could run.
291
+ function findNode(pathEnv) {
292
+ const name = process.platform === "win32" ? "node.exe" : "node";
293
+ for (const entry of (pathEnv || "").split(delimiter)) {
294
+ const dir = entry.replace(/^"(.*)"$/, "$1");
295
+ if (!dir || !isAbsolute(dir)) continue;
296
+ try {
297
+ if (statSync(join(dir, name)).isFile()) return join(dir, name);
298
+ } catch {
299
+ /* not in this folder */
300
+ }
301
+ }
302
+ return null;
303
+ }
304
+
305
+ function omcVersion(root) {
306
+ if (!root) return null;
307
+ try {
308
+ const v = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version;
309
+ return typeof v === "string" ? v : null;
310
+ } catch {
311
+ return null;
312
+ }
313
+ }
314
+
315
+ // Why an OMC version does not load: its HUD's own error when run directly.
316
+ function loadReason(node, root) {
317
+ const hud = join(root, "dist", "hud", "index.js");
318
+ if (!existsSync(hud)) return "no dist/hud/index.js";
319
+ const r = spawnSync(node, [hud], RUN_OPTS);
320
+ const line = /^\w*Error\b.*$/m.exec(r.stderr || "");
321
+ return line ? clip(line[0]) : "";
322
+ }
323
+
324
+ // What the formatter printed, as report lines.
325
+ function judgeRender(out, node) {
326
+ const banner = /^\[OMC\] HUD v(\S+) \| preset: /m.exec(out);
327
+ if (banner) {
328
+ const installed = omcVersion(pluginRoot);
329
+ if (!installed || installed === banner[1]) return [`[ok] Render check: the statusline loads OMC ${banner[1]}.`];
330
+ const why = loadReason(node, pluginRoot);
331
+ const line = `[!!] Render check: the statusline loads OMC ${banner[1]}, not the installed ${installed}; ${installed} does not load`;
332
+ return [`${line}${why ? ` (${why})` : ""}.`, BLAME];
333
+ }
334
+ // OMC's line ends with the reason, after a path that can be long: keep it whole within reason.
335
+ const failed = /^\[OMC HUD\] .*$/m.exec(out);
336
+ if (failed) return [`[!!] Render check: OMC's HUD does not load: ${clip(failed[0], 1000)}`, BLAME];
337
+ return [`[..] Render check: unrecognized output: ${clip(out.split("\n")[0])}`];
338
+ }
339
+
340
+ function renderReport() {
341
+ const node = findNode(process.env.PATH);
342
+ if (!node) return ['[!!] Render check: no "node" on PATH; the statusLine command runs "node", so the statusline will not show.'];
343
+ const r = spawnSync(node, [wrapperDst], RUN_OPTS);
344
+ if (r.error && r.error.code === "ETIMEDOUT") return [`[!!] Render check: the statusline gave no answer in ${CHECK_MS / 1000}s.`];
345
+ if (r.error) throw r.error;
346
+ if (r.status !== 0) {
347
+ const err = (r.stderr || "").trim().split("\n")[0];
348
+ return [`[!!] Render check: the statusline command failed (exit ${r.status ?? r.signal})${err ? `: ${clip(err)}` : ""}`];
349
+ }
350
+ return judgeRender((r.stdout || "").replace(/\x1b\[[0-9;]*m/g, ""), node);
351
+ }
352
+
353
+ function renderCheck() {
354
+ try {
355
+ for (const line of renderReport()) log(line);
356
+ } catch (e) {
357
+ log(`[..] Render check: skipped (${e.code || e.message})`);
358
+ }
359
+ }
360
+
223
361
  try {
224
362
  installLoader();
225
363
  installTemplates();
226
364
  pointStatusLine();
227
365
  } catch (e) {
228
366
  log(`[x] Stopped partway (${e.code || e.message}); some files may have changed.`);
229
- log(` A replaced file's earlier version is in <file>.backup.${stamp}.`);
367
+ log(` Earlier versions it replaced, apart from the loader and the tool blocks, are in <file>.backup.${stamp}.`);
230
368
  process.exit(2);
231
369
  }
232
370
 
371
+ renderCheck();
233
372
  log("");
234
373
  log("Done. Restart Claude Code to load the new statusline.");