@kaydash9999/banker-plugins 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +82 -0
- package/README.md +50 -17
- package/codex/manifest.json +11 -8
- package/codex/transform-matrix.md +16 -4
- package/commands/front-qa.md +8 -5
- package/commands/setup.md +13 -4
- package/hooks/graceful-pause.mjs +222 -0
- package/hooks/hooks.json +2 -1
- package/hooks/register.mjs +12 -0
- package/package.json +4 -1
- package/skills/3d-intro-build/SKILL.md +1 -1
- package/skills/3d-intro-setup/SKILL.md +1 -1
- package/skills/all-in-one/SKILL.md +160 -55
- package/skills/append_wiki/SKILL.md +1 -1
- package/skills/arch-diagram/SKILL.md +1 -1
- package/skills/audit-mock/SKILL.md +1 -1
- package/skills/audit-security/SKILL.md +1 -1
- package/skills/audit-web-page/SKILL.md +1 -1
- package/skills/cleansing-memory/SKILL.md +1 -1
- package/skills/compact-copy/SKILL.md +14 -13
- package/skills/compact-wiki/SKILL.md +1 -1
- package/skills/curation/SKILL.md +36 -3
- package/skills/deep-init/SKILL.md +1 -1
- package/skills/deep-research/SKILL.md +1 -1
- package/skills/docs-setup/SKILL.md +1 -1
- package/skills/humanizer/SKILL.md +1 -1
- package/skills/interval-report/SKILL.md +1 -1
- package/skills/lineage/SKILL.md +146 -16
- package/skills/lineage/lineage.py +1091 -107
- package/skills/make-notion-guide/SKILL.md +1 -1
- package/skills/motion-graphic-make/SKILL.md +1 -1
- package/skills/motion-graphic-setup/SKILL.md +1 -1
- package/skills/nothing-design/SKILL.md +1 -1
- package/skills/obsidizer/SKILL.md +1 -1
- package/skills/omc-reference/SKILL.md +68 -33
- package/skills/payload-mon/SKILL.md +66 -0
- package/skills/payload-mon/scripts/payload-mon.mjs +377 -0
- package/skills/payload-mon/scripts/payload-size.mjs +238 -0
- package/skills/pdf-vision-extract/SKILL.md +1 -1
- package/skills/play-qa/SKILL.md +1 -1
- package/skills/ralph-qa/SKILL.md +263 -112
- package/skills/ralph-qa/references/gemini-read-only.toml +9 -0
- package/skills/ralph-qa/references/gemini-seat.mjs +175 -0
- package/skills/ralph-qa/references/payload-scan.mjs +130 -0
- package/skills/ralph-qa/references/verifier-probe.mjs +872 -780
- package/skills/ready-compact/SKILL.md +114 -67
- package/skills/refresh-git-ignore/SKILL.md +1 -1
- package/skills/refresh-readme/SKILL.md +3 -3
- package/skills/rfp-author/SKILL.md +3 -3
- package/skills/setup-bypass-permissions/SKILL.md +123 -0
- package/skills/setup-bypass-permissions/agents/openai.yaml +4 -0
- package/skills/setup-bypass-permissions/scripts/bypass-permissions.mjs +381 -0
- package/skills/setup-bypass-permissions/scripts/fallback/bypass-permissions.ps1 +247 -0
- package/skills/setup-bypass-permissions/scripts/fallback/bypass-permissions.py +241 -0
- package/skills/{harness-factory → setup-harness-factory}/SKILL.md +3 -3
- package/skills/setup-insane-search/SKILL.md +1 -1
- package/skills/setup-java/SKILL.md +1 -1
- package/skills/setup-lsp/SKILL.md +1 -1
- package/skills/setup-mcp/SKILL.md +1 -1
- package/skills/setup-node/SKILL.md +1 -1
- package/skills/setup-omc/SKILL.md +11 -4
- package/skills/setup-omc-hud/SKILL.md +26 -1
- package/skills/setup-omc-hud/scripts/claude-update-last.mjs +247 -0
- package/skills/setup-playwright/SKILL.md +1 -1
- package/skills/setup-pwsh/SKILL.md +1 -1
- package/skills/setup-python/SKILL.md +1 -1
- package/skills/setup-sandbox/SKILL.md +1 -1
- package/skills/setup-stitch/SKILL.md +1 -1
- package/skills/setup-tmux/SKILL.md +1 -1
- package/skills/smart-compact/SKILL.md +10 -9
- package/skills/summary-wiki/SKILL.md +1 -1
- package/skills/tone-compact/SKILL.md +203 -0
- package/skills/tone-compact/scripts/tone-compact.mjs +499 -0
- package/skills/ultra-init/SKILL.md +53 -42
- package/skills/ultra-interview/SKILL.md +1 -1
- package/skills/ultra-ui-qa/SKILL.md +1 -1
- package/skills/update-banker/SKILL.md +1 -1
- package/skills/vertical-pptx/SKILL.md +1 -1
- package/skills/vertical-pptx-setup/SKILL.md +1 -1
- package/skills/visual-ralph/SKILL.md +1 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""setup-bypass-permissions step 3 on macOS and Linux, for when node cannot run the main script.
|
|
3
|
+
|
|
4
|
+
python3 bypass-permissions.py on --yes set permissions.defaultMode = "bypassPermissions"
|
|
5
|
+
python3 bypass-permissions.py off remove that key (Claude Code's default start mode)
|
|
6
|
+
|
|
7
|
+
The file is <config>/settings.json, <config> being $CLAUDE_CONFIG_DIR or ~/.claude. Every other
|
|
8
|
+
key stays, and so does a BOM. `on` keeps the text from before in settings.json.bypass-permissions.bak,
|
|
9
|
+
readable by this account alone; a second `on` finds the mode set and writes nothing. Both files are
|
|
10
|
+
replaced whole through a temp file and a rename, so a stop partway leaves the old text, and a link at
|
|
11
|
+
the settings name stays a link. Only the node script's `off` knows what `on` replaced; this `off`
|
|
12
|
+
removes the key.
|
|
13
|
+
|
|
14
|
+
`on` refuses (exit 1, nothing written) where Claude Code would not honour the mode or not start with
|
|
15
|
+
it: managed policy (managed-settings.json or its managed-settings.d drop-ins) or the settings file
|
|
16
|
+
itself sets permissions.disableBypassPermissionsMode, or the account is root outside a sandbox Claude
|
|
17
|
+
Code recognises (IS_SANDBOX=1, or its bubblewrap one). It also refuses a file that is not a JSON object,
|
|
18
|
+
a policy file or folder this account cannot read, and a machine with no Claude Code config folder.
|
|
19
|
+
|
|
20
|
+
Exit status: 0 done, or nothing to do. 1 refused. 2 unexpected error. 3 this account may not write
|
|
21
|
+
the file or its folder. Every result ends with the line `settings file: <path>` on stdout.
|
|
22
|
+
Python 3.6 or later.
|
|
23
|
+
"""
|
|
24
|
+
import errno
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
import tempfile
|
|
29
|
+
|
|
30
|
+
MODE = "bypassPermissions"
|
|
31
|
+
BOM = b"\xef\xbb\xbf"
|
|
32
|
+
POLICY_HOMES = {"darwin": "/Library/Application Support/ClaudeCode"}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Refused(Exception):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def settings_path():
|
|
40
|
+
config = os.environ.get("CLAUDE_CONFIG_DIR") or os.path.expanduser("~/.claude")
|
|
41
|
+
return os.path.join(config, "settings.json")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def policy_home():
|
|
45
|
+
# The override exists for this skill's tests alone.
|
|
46
|
+
if os.environ.get("BANKER_BYPASS_TEST") == "1" and os.environ.get("BANKER_BYPASS_POLICY_HOME"):
|
|
47
|
+
return os.environ["BANKER_BYPASS_POLICY_HOME"]
|
|
48
|
+
return POLICY_HOMES.get(sys.platform, "/etc/claude-code")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def forbids(data):
|
|
52
|
+
perm = data.get("permissions") if isinstance(data, dict) else None
|
|
53
|
+
value = perm.get("disableBypassPermissionsMode") if isinstance(perm, dict) else None
|
|
54
|
+
return value is True or value == "disable"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# A policy path that is not there, or is not a file of that kind: no policy, not a failure.
|
|
58
|
+
ABSENT = (errno.ENOENT, errno.ENOTDIR, errno.EISDIR)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def unreadable_policy(path):
|
|
62
|
+
return Refused("this machine's managed policy cannot be read (" + path + "); ask the administrator whether it allows the mode")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def policy_files():
|
|
66
|
+
"""The managed settings file and its drop-ins, in the order Claude Code reads them."""
|
|
67
|
+
home = policy_home()
|
|
68
|
+
files = [os.path.join(home, "managed-settings.json")]
|
|
69
|
+
drop = os.path.join(home, "managed-settings.d")
|
|
70
|
+
try:
|
|
71
|
+
names = os.listdir(drop)
|
|
72
|
+
except OSError as e:
|
|
73
|
+
if e.errno not in ABSENT:
|
|
74
|
+
raise unreadable_policy(drop)
|
|
75
|
+
names = []
|
|
76
|
+
files += [os.path.join(drop, n) for n in sorted(n for n in names if n.endswith(".json") and not n.startswith("."))]
|
|
77
|
+
return files
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def policy_block():
|
|
81
|
+
"""The policy file that forbids the mode, or ""; Refused for a policy this account cannot read."""
|
|
82
|
+
for path in policy_files():
|
|
83
|
+
try:
|
|
84
|
+
with open(path, "rb") as handle:
|
|
85
|
+
raw = handle.read()
|
|
86
|
+
except OSError as e:
|
|
87
|
+
if e.errno in ABSENT:
|
|
88
|
+
continue
|
|
89
|
+
raise unreadable_policy(path)
|
|
90
|
+
try:
|
|
91
|
+
if forbids(json.loads(raw.decode("utf-8-sig"))):
|
|
92
|
+
return path
|
|
93
|
+
except ValueError:
|
|
94
|
+
pass # not JSON: nothing this fallback can see forbids it
|
|
95
|
+
return ""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def load(path):
|
|
99
|
+
"""(data, raw bytes or None when there is no file) for a JSON object; Refused otherwise."""
|
|
100
|
+
try:
|
|
101
|
+
with open(path, "rb") as handle:
|
|
102
|
+
raw = handle.read()
|
|
103
|
+
except FileNotFoundError:
|
|
104
|
+
return {}, None
|
|
105
|
+
try:
|
|
106
|
+
text = raw.decode("utf-8-sig")
|
|
107
|
+
except UnicodeDecodeError:
|
|
108
|
+
raise Refused("settings.json is not valid UTF-8")
|
|
109
|
+
if not text.strip():
|
|
110
|
+
return {}, raw
|
|
111
|
+
def reject(token):
|
|
112
|
+
raise ValueError("the constant " + token + " is not JSON")
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
data = json.loads(text, parse_constant=reject)
|
|
116
|
+
except ValueError as e:
|
|
117
|
+
raise Refused("settings.json is not valid JSON (" + str(e) + ")")
|
|
118
|
+
if not isinstance(data, dict):
|
|
119
|
+
raise Refused("settings.json is not a JSON object")
|
|
120
|
+
if not isinstance(data.get("permissions", {}), dict):
|
|
121
|
+
raise Refused("the permissions entry of settings.json is not an object")
|
|
122
|
+
return data, raw
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def replace(path, data, mode):
|
|
126
|
+
"""Write bytes to path through a temp file in the same folder and a rename."""
|
|
127
|
+
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path), prefix=".bypass-permissions.")
|
|
128
|
+
try:
|
|
129
|
+
with os.fdopen(fd, "wb") as handle:
|
|
130
|
+
handle.write(data)
|
|
131
|
+
os.chmod(tmp, mode)
|
|
132
|
+
try:
|
|
133
|
+
os.replace(tmp, path)
|
|
134
|
+
except OSError as e:
|
|
135
|
+
if e.errno not in (errno.EBUSY, errno.EXDEV):
|
|
136
|
+
raise
|
|
137
|
+
with open(path, "wb") as target: # a bind-mounted single file refuses the rename: write in place
|
|
138
|
+
target.write(data)
|
|
139
|
+
os.remove(tmp)
|
|
140
|
+
except BaseException:
|
|
141
|
+
if os.path.exists(tmp):
|
|
142
|
+
os.remove(tmp)
|
|
143
|
+
raise
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def save(path, data, raw):
|
|
147
|
+
"""Replace the real file behind path (a link stays a link), keeping its mode and BOM."""
|
|
148
|
+
real = os.path.realpath(path)
|
|
149
|
+
mode = os.stat(real).st_mode & 0o777 if os.path.exists(real) else 0o600
|
|
150
|
+
bom = BOM if raw is not None and raw.startswith(BOM) else b""
|
|
151
|
+
replace(real, bom + (json.dumps(data, indent=2, ensure_ascii=False) + "\n").encode("utf-8"), mode)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def check_on(path, data):
|
|
155
|
+
# Claude Code lets root use the mode inside a sandbox it recognises: IS_SANDBOX exactly "1", or
|
|
156
|
+
# CLAUDE_CODE_BUBBLEWRAP set to a true value (2.1.289 reads 1, true, yes, on; 0 or false is not one).
|
|
157
|
+
bubblewrap = os.environ.get("CLAUDE_CODE_BUBBLEWRAP", "").strip().lower() in ("1", "true", "yes", "on")
|
|
158
|
+
sandboxed = os.environ.get("IS_SANDBOX") == "1" or bubblewrap
|
|
159
|
+
if hasattr(os, "geteuid") and os.geteuid() == 0 and not sandboxed:
|
|
160
|
+
raise Refused("Claude Code will not start in bypassPermissions as root; IS_SANDBOX=1 marks a real sandbox")
|
|
161
|
+
blocked = policy_block()
|
|
162
|
+
if blocked:
|
|
163
|
+
raise Refused("managed policy forbids bypassPermissions: " + blocked)
|
|
164
|
+
if forbids(data):
|
|
165
|
+
raise Refused("settings.json itself sets permissions.disableBypassPermissionsMode")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def forget_record(path):
|
|
169
|
+
"""Drop the node script's record of what `on` replaced. This fallback keeps no record, so one left by an
|
|
170
|
+
earlier node `on` is stale: the mode is no longer what it describes, and a later node `off` would restore it."""
|
|
171
|
+
try:
|
|
172
|
+
os.remove(path + ".bypass-permissions.json")
|
|
173
|
+
except FileNotFoundError:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def turn_on(path):
|
|
178
|
+
if not os.path.isdir(os.path.dirname(path)):
|
|
179
|
+
raise Refused("there is no Claude Code config folder at " + os.path.dirname(path))
|
|
180
|
+
data, raw = load(path)
|
|
181
|
+
check_on(path, data)
|
|
182
|
+
if data.get("permissions", {}).get("defaultMode") == MODE:
|
|
183
|
+
print("defaultMode = " + MODE + " (already, nothing changed)")
|
|
184
|
+
return 0
|
|
185
|
+
if raw is not None and not os.access(path, os.W_OK):
|
|
186
|
+
raise PermissionError(errno.EACCES, "this account may not write", path)
|
|
187
|
+
if raw is not None:
|
|
188
|
+
replace(path + ".bypass-permissions.bak", raw, 0o600)
|
|
189
|
+
forget_record(path) # the mode is not on, so any record is stale
|
|
190
|
+
data.setdefault("permissions", {})["defaultMode"] = MODE
|
|
191
|
+
save(path, data, raw)
|
|
192
|
+
print("defaultMode = " + MODE)
|
|
193
|
+
return 0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def turn_off(path):
|
|
197
|
+
data, raw = load(path)
|
|
198
|
+
current = data.get("permissions", {}).get("defaultMode")
|
|
199
|
+
if current != MODE:
|
|
200
|
+
print("defaultMode = " + str(current) + " (not bypassPermissions, nothing changed)")
|
|
201
|
+
return 0
|
|
202
|
+
if not os.access(path, os.W_OK): # a read-only settings file is a lock: off honours it as on does
|
|
203
|
+
raise PermissionError(errno.EACCES, "this account may not write", path)
|
|
204
|
+
del data["permissions"]["defaultMode"]
|
|
205
|
+
save(path, data, raw)
|
|
206
|
+
forget_record(path) # only after the write: if it failed, the record is still the way back
|
|
207
|
+
print("defaultMode removed (Claude Code's default start mode)")
|
|
208
|
+
return 0
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def outcome(argv, path):
|
|
212
|
+
actions = {("on", "--yes"): turn_on, ("off",): turn_off}
|
|
213
|
+
action = actions.get(tuple(argv[1:]))
|
|
214
|
+
if action is None:
|
|
215
|
+
print("refused: usage is `bypass-permissions.py on --yes` or `bypass-permissions.py off`", file=sys.stderr)
|
|
216
|
+
return 1
|
|
217
|
+
try:
|
|
218
|
+
return action(path)
|
|
219
|
+
except Refused as e:
|
|
220
|
+
print("refused: " + str(e) + "; settings.json left as it was", file=sys.stderr)
|
|
221
|
+
return 1
|
|
222
|
+
except OSError as e:
|
|
223
|
+
if e.errno in (errno.EACCES, errno.EPERM, errno.EROFS):
|
|
224
|
+
print("write blocked: " + str(e) + "; settings.json left as it was", file=sys.stderr)
|
|
225
|
+
return 3
|
|
226
|
+
print("unexpected error: " + str(e) + "; check settings.json", file=sys.stderr)
|
|
227
|
+
return 2
|
|
228
|
+
except Exception as e: # anything else stopped it partway too
|
|
229
|
+
print("unexpected error: " + type(e).__name__ + ": " + str(e) + "; check settings.json", file=sys.stderr)
|
|
230
|
+
return 2
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def main(argv):
|
|
234
|
+
path = settings_path()
|
|
235
|
+
code = outcome(argv, path)
|
|
236
|
+
print("settings file: " + path)
|
|
237
|
+
return code
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
if __name__ == "__main__":
|
|
241
|
+
sys.exit(main(sys.argv))
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: harness-factory
|
|
3
|
-
description: "
|
|
2
|
+
name: setup-harness-factory
|
|
3
|
+
description: "도메인 한 줄로 전문 에이전트 팀을 만들어 주는 `revfactory/harness` 플러그인을 설치하고 구성한 뒤 사용법을 안내. Claude Code 는 `harness@harness-marketplace`, Codex 는 `meta-harness`. 'setup-harness-factory'/'harness-factory'/'하네스 팩토리'/'에이전트팀 생성' 또는 `/banker:setup` 시 사용."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# harness-factory: revfactory/harness 팀 아키텍처 팩토리 설치·구성
|
|
6
|
+
# setup-harness-factory: revfactory/harness 팀 아키텍처 팩토리 설치·구성
|
|
7
7
|
|
|
8
8
|
`harness`는 도메인을 한 줄로 받아 `.claude/agents/`+`.claude/skills/`(전문 에이전트팀 + 그 에이전트가 쓸
|
|
9
9
|
스킬)를 생성하는 **메타 스킬**이다. 이 플러그인의 본체는 설치물이 아니라 **방법론**이다. 따라서 이
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: setup-insane-search
|
|
3
|
-
description: "
|
|
3
|
+
description: "차단이나 봇 방어가 있는 사이트도 우회해 검색하고 가져오는 플러그인을 설치. Claude Code 는 `gptaku-plugins` 의 `insane-search`, Codex 는 `gptaku-codex` 의 `insane-research-codex`. 'setup-insane-search'/'insane-search 설치' 또는 `/banker:setup` 시 사용."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# setup-insane-search: insane-search 플러그인 설치
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: setup-java
|
|
3
|
-
description: "
|
|
3
|
+
description: "JDK 21 을 설치하고 `JAVA_HOME` 을 설정. `alternatives` 로 `java` 와 `javac` 의 버전 비대칭을 막고, Debian 은 Adoptium 만 씀. jdtls 같은 Java LSP 의 전제. 'setup-java'/'JDK 설치'/'Java 21 설치' 또는 `/banker:setup` 시 사용."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# setup-java: JDK 21 + JAVA_HOME 설치
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: setup-lsp
|
|
3
|
-
description: "
|
|
3
|
+
description: "언어별 LSP 서버(vtsls, basedpyright, bash, jdtls, spring)와 lsp-mcp 브리지를 설치하고 연동. 'setup-lsp'/'LSP 설치'/'언어 서버' 또는 `/banker:setup` 시 사용."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# setup-lsp: 언어별 LSP 서버 + lsp-mcp 브리지
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: setup-mcp
|
|
3
|
-
description: "(
|
|
3
|
+
description: "범용 MCP 서버(context7, sequential-thinking, filesystem, git, fetch, 선택 사항인 tavily)를 Claude Code(`claude mcp add`)와 Codex(`codex mcp add`, `config.toml`)에 등록. serena 와 `lsp_bridge` 는 제외. 'setup-mcp'/'MCP 서버 설치'/'context7 설치'/'sequential-thinking 설치' 또는 `/banker:setup` 시 사용."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# setup-mcp: 범용 MCP 서버 설치
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: setup-node
|
|
3
|
-
description: "
|
|
3
|
+
description: "nvm 과 Node 22(LTS)를 설치(Windows 는 winget 이나 nvm-windows). Claude Code 의 npm 설치, Codex 와 OMX, npx 로 도는 MCP 서버의 전제. 'setup-node'/'Node 설치'/'nvm 설치' 또는 `/banker:setup` 시 사용."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# setup-node: nvm + Node 22(LTS) 런타임 설치
|
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: setup-omc
|
|
3
|
-
description: "
|
|
3
|
+
description: "`all-in-one`, `ultra-init`, `front-qa` 가 의존하는 oh-my-claudecode(OMC)를 설치하거나 갱신. Codex 에서는 oh-my-codex(OMX)가 대상. 'setup-omc'/'OMC 설치'/'oh-my-claudecode 설치' 또는 `/banker:setup` 시 사용."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# setup-omc: oh-my-claudecode(OMC) 설치/갱신
|
|
7
7
|
|
|
8
8
|
banker의 오케스트레이터(`all-in-one`·`ultra-init`)와 `/banker:front-qa` 커맨드는 OMC의
|
|
9
|
-
`ralplan`·`ralph`·`
|
|
9
|
+
`ralplan`·`ralph`·`verify` 와 `omc` CLI(`omc ralph verify`)에 의존한다. OMC 5부터 `ralph` 가 첫 반복에 `omc ralph verify` 를 직접 부르므로, 플러그인만 새것이고 전역 `omc` CLI 가 낡으면(4.x) `ralph` 가 시작하자마자 멈춘다. 이 스킬은 OMC를 설치하거나 최신화한다(이미 있으면 갱신만; 멱등). 답변은 한글.
|
|
10
10
|
|
|
11
11
|
## 0. 감지 (먼저, 추정 금지)
|
|
12
12
|
```bash
|
|
13
13
|
command -v omc >/dev/null && omc --version || echo "omc: 미설치"
|
|
14
|
+
omc ralph verify --help 2>&1 | grep -q 'Usage: omc ralph verify' && echo "omc CLI: ralph verify 있음" || echo "omc CLI: ralph verify 없음 (미설치 또는 4.x)"
|
|
14
15
|
command -v claude >/dev/null || echo "claude: 미설치"
|
|
15
16
|
```
|
|
17
|
+
- `ralph verify` 는 종료 코드가 아니라 **출력 문구로 판정**한다. 4.x CLI 는 모르는 명령에 `--help` 를 붙이면 전체 도움말을 찍고 0 으로 끝난다. `--help` 없이 부르면 Claude 실행으로 넘어가므로 점검에는 반드시 붙인다.
|
|
16
18
|
|
|
17
19
|
## Claude Code · OMC
|
|
18
20
|
- **이미 설치(`omc` 존재)** 면 최신화만 한다:
|
|
19
21
|
```bash
|
|
20
22
|
omc update # 플러그인·npm 패키지·CLAUDE.md 동기화
|
|
21
23
|
```
|
|
24
|
+
- `omc update` 가 중간에 실패하거나 끝난 뒤에도 `ralph verify` 가 없으면, CLI 만 직접 올린다:
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g oh-my-claude-sisyphus@latest
|
|
27
|
+
```
|
|
28
|
+
- 네이티브 모듈(`better-sqlite3`) 빌드가 낡은 기본 컴파일러 때문에 실패하는 배포판(예: RHEL·Rocky 8)은 새 gcc 를 켠 셸에서 같은 명령을 실행한다(예: `source /opt/rh/gcc-toolset-14/enable`).
|
|
22
29
|
- **미설치** 면 공식 배포처 절차를 따른다(일반적으로 npm 전역 설치 후 `omc setup`). 정확한 설치 명령이 불명확하면 추정하지 말고 사용자에게 확인한다.
|
|
23
|
-
- 확인: `omc --version`
|
|
30
|
+
- 확인: `omc --version`, 위 `ralph verify` 점검 통과, Claude 재시작·`/reload-plugins` 후 `/oh-my-claudecode:*` 노출.
|
|
24
31
|
|
|
25
32
|
## Codex CLI · OMX (oh-my-codex)
|
|
26
33
|
Codex에서의 대응 프레임워크는 OMX다.
|
|
@@ -30,7 +37,7 @@ command -v omx >/dev/null && omx setup # 유저/프로젝트 레벨 OMX 구
|
|
|
30
37
|
- 미설치면 OMX 배포처 안내대로 설치 후 `omx setup`.
|
|
31
38
|
|
|
32
39
|
## 검증·보고
|
|
33
|
-
- OMC: `omc --version` / OMX: `omx --version`. 반영엔 재시작·`/reload-plugins`(Claude)가 필요할 수 있다.
|
|
40
|
+
- OMC: `omc --version` + `ralph verify` 점검 / OMX: `omx --version`. 반영엔 재시작·`/reload-plugins`(Claude)가 필요할 수 있다.
|
|
34
41
|
- 실패·불명확은 정직 보고(가짜 성공 금지).
|
|
35
42
|
|
|
36
43
|
## 함정
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: setup-omc-hud
|
|
3
|
-
description: "
|
|
3
|
+
description: "Claude Code 의 상태표시줄 omc_hud 를 OS별 적용 스크립트로 설치하고, Claude Code 갱신 안내를 줄 맨 끝으로 옮김. Codex 는 OMX 의 `hud` 스킬을 씀. 'setup-omc-hud'/'omc_hud 설치'/'Claude 갱신 안내 위치' 또는 `/banker:setup` 시 사용."
|
|
4
|
+
argument-hint: "[off|status]"
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# setup-omc-hud — omc_hud 상태표시줄 설치 (OS별)
|
|
@@ -9,10 +10,20 @@ description: "(banker) omc_hud(Claude Code 상태표시줄)를 OS별 apply 스
|
|
|
9
10
|
|
|
10
11
|
**런타임:** omc_hud 는 Claude Code 상태표시줄(`~/.claude/settings.json` statusLine) 대상이다. **Codex 런타임에선 oh-my-codex(OMX)가 자체 `hud` 스킬/구성을 제공**하므로, Codex에선 이 스킬 대신 OMX 의 `hud` 를 사용한다.
|
|
11
12
|
|
|
13
|
+
**인자가 `off` 또는 `status` 일 때:** 0~3단계를 건너뛰고 3단계의 Claude 갱신 안내 스크립트만 그 인자로 실행해 결과를 보고한다(`off` 는 안내를 래퍼가 정한 자리로 되돌림, `status` 는 상태만 확인).
|
|
14
|
+
|
|
12
15
|
## 0. 감지
|
|
13
16
|
```bash
|
|
14
17
|
case "$(uname -s)" in Darwin) OS=mac;; Linux) OS=linux;; *) OS=windows;; esac; echo "OS=$OS"
|
|
15
18
|
command -v wget >/dev/null && echo "dl=wget" || { command -v curl >/dev/null && echo "dl=curl" || echo "dl=none(설치 필요)"; }
|
|
19
|
+
# 적용 스크립트가 래퍼를 덮어쓰면 payload-mon 표시 블록이 빠진다. 지금 켜져 있는지 기억해 둔다(3단계에서 사용).
|
|
20
|
+
HUD_FILE="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/hud/omc-hud-custom.mjs"
|
|
21
|
+
grep -qF '// >>> payload-mon >>>' "$HUD_FILE" 2>/dev/null && echo "payload-mon=on" || echo "payload-mon=off"
|
|
22
|
+
```
|
|
23
|
+
Windows(PowerShell)에서는 같은 확인을 이렇게 한다:
|
|
24
|
+
```powershell
|
|
25
|
+
$cfg = if ($env:CLAUDE_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR } else { "$env:USERPROFILE\.claude" }
|
|
26
|
+
if (Select-String -Quiet -SimpleMatch -LiteralPath "$cfg\hud\omc-hud-custom.mjs" -Pattern '// >>> payload-mon >>>' -ErrorAction SilentlyContinue) { "payload-mon=on" } else { "payload-mon=off" }
|
|
16
27
|
```
|
|
17
28
|
|
|
18
29
|
## 1. mac / linux — apply 스크립트 적용
|
|
@@ -41,8 +52,22 @@ git 없으면 GitHub "Download ZIP" → 해제 → 동봉 적용 스크립트
|
|
|
41
52
|
- 적용 스크립트가 `~/.claude/settings.json` 의 statusLine 등을 갱신했는지(스크립트 출력 확인).
|
|
42
53
|
- HUD는 **다음 프롬프트/세션**부터 반영될 수 있음 → 안 보이면 재시작 안내.
|
|
43
54
|
- `~/dummy` 임시파일 정리 확인. 이미 적용돼 있으면 재적용 불필요.
|
|
55
|
+
- **payload-mon 복구**: 0단계에서 `payload-mon=on` 이었다면, 적용 스크립트가 래퍼를 덮어써 payload 표시 블록이 빠진 상태다.
|
|
56
|
+
payload-mon 스킬(Claude Code `/banker:payload-mon`, Codex `banker-payload-mon`)을 `on` 으로 실행해 다시 켜고, 그 결과를 함께 보고한다.
|
|
57
|
+
`payload-mon=off` 였다면 아무것도 하지 않는다(사용자가 켠 적 없는 기능을 켜지 않는다).
|
|
58
|
+
- **Claude 갱신 안내 위치**: payload-mon 처리 뒤에 이 스킬의 스크립트를 `on` 으로 실행한다. 재적용을 건너뛴 경우에도 실행하고, Windows 도 같은 명령이다.
|
|
59
|
+
```bash
|
|
60
|
+
node "<이 스킬 디렉터리 절대경로>/scripts/claude-update-last.mjs" on
|
|
61
|
+
```
|
|
62
|
+
새 Claude Code 가 나오면 OMC 가 띄우는 `[Claude#2.1.288] -> 2.1.289 claude update` 를 상태표시줄 1번째 줄 맨 끝(OMC 표시 뒤)으로 옮긴다. 2번째 줄의 `[!] claude 2.1.289 - paste: ! claude update` 안내는 그대로 둔다.
|
|
63
|
+
출력을 그대로 보고한다. 종료 코드 1(거부)이면 래퍼는 바뀌지 않았으니 이유만 보고하고, 2(예기치 못한 오류로 중간에 멈춤)면 `status` 결과를 함께 보고한다.
|
|
64
|
+
되돌리기는 `off`, 상태 확인은 `status`.
|
|
44
65
|
|
|
45
66
|
## 함정
|
|
46
67
|
- `curl` 다운로드는 **대문자 `-O`**(원격 파일명 유지). 소문자 `-o`는 출력명 지정이라 다름.
|
|
47
68
|
- raw URL 의 `refs/heads/main` 브랜치명이 바뀌면 404 → 저장소 확인.
|
|
48
69
|
- `.mjs` 적용은 Node 필요(`node -v`).
|
|
70
|
+
- `claude-update-last` 를 `off` 로 꺼 두었어도 이 스킬을 다시 실행하면 3단계에서 다시 켜진다. 래퍼가 정한 자리를 원하면 끝난 뒤 `off`.
|
|
71
|
+
- 블록은 OMC 의 안내 문구(`[Claude#X] -> Y claude update`)로 안내를 찾는다. OMC 가 문구를 바꾸면 안내는 래퍼가 정한 자리에 남고, 상태표시줄은 깨지지 않는다.
|
|
72
|
+
- 블록은 래퍼가 세그먼트를 합치는 줄에서 `segs`·`colored` 를 읽는다. 래퍼 구조가 바뀌어 그 줄에서 이 이름이 보이지 않으면 `on` 은 성공해도 효과가 없다(상태표시줄은 그대로). 새 Claude Code 가 나왔을 때 안내가 맨 끝에 오는지로 확인한다.
|
|
73
|
+
- 이 블록은 플러그인 밖(`~/.claude/hud/`)에 남는다. 플러그인을 지우기 전에 `off` 를 실행한다.
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* claude-update-last: puts OMC's Claude Code update notice at the end of the statusline.
|
|
4
|
+
*
|
|
5
|
+
* node claude-update-last.mjs on add the block to the OMC custom HUD wrapper
|
|
6
|
+
* node claude-update-last.mjs off take it out again
|
|
7
|
+
* node claude-update-last.mjs status say whether it is on
|
|
8
|
+
*
|
|
9
|
+
* When a newer Claude Code is out, OMC's HUD prints "[Claude#<running>] -> <latest>
|
|
10
|
+
* claude update" on its main line. The omc_hud wrapper lays that line out again in
|
|
11
|
+
* a fixed order: the older wrapper drops segments it does not know, so the notice
|
|
12
|
+
* never shows, and the current one shows them dimmed before the path, mid-line.
|
|
13
|
+
* The block runs right before the wrapper joins its segments, takes the notice out
|
|
14
|
+
* of wherever it landed and adds it, dimmed, after every other segment. OMC's own
|
|
15
|
+
* hint on the line below ("[!] claude <latest> - paste: ! claude update") is left
|
|
16
|
+
* as the wrapper shows it.
|
|
17
|
+
*
|
|
18
|
+
* The wrapper is <config>/hud/omc-hud-custom.mjs, where <config> is
|
|
19
|
+
* $CLAUDE_CONFIG_DIR or ~/.claude; $CLAUDE_UPDATE_LAST_HUD_FILE overrides the path.
|
|
20
|
+
* Blocks other tools add with their own markers (payload-mon's, say) are left alone.
|
|
21
|
+
*
|
|
22
|
+
* `on` converges on the current block and is a no-op when it is in place. Both
|
|
23
|
+
* commands refuse markers that do not pair up, keep the text from before each
|
|
24
|
+
* change in <wrapper>.claude-update-last.bak, and write nothing that fails
|
|
25
|
+
* `node --check`. Exit status 1 means nothing was changed; 2 means an unexpected
|
|
26
|
+
* error stopped the command partway, so check `status`.
|
|
27
|
+
*/
|
|
28
|
+
import { spawnSync } from "node:child_process";
|
|
29
|
+
import { accessSync, constants, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
30
|
+
import { homedir, tmpdir } from "node:os";
|
|
31
|
+
import { join } from "node:path";
|
|
32
|
+
import { fileURLToPath } from "node:url";
|
|
33
|
+
|
|
34
|
+
const OPEN = "// >>> claude-update-last >>>";
|
|
35
|
+
const CLOSE = "// <<< claude-update-last <<<";
|
|
36
|
+
// A block's body never crosses another opening marker, so a stray one above a
|
|
37
|
+
// block is left over (and refused) instead of taking the code between with it.
|
|
38
|
+
const BLOCKS =
|
|
39
|
+
/^[ \t]*\/\/ >>> claude-update-last >>>(?:(?!\/\/ >>> claude-update-last >>>)[\s\S])*?\/\/ <<< claude-update-last <<<[ \t]*(?:\r?\n|(?![\s\S]))/gm;
|
|
40
|
+
// The block goes right before the line that joins the segments into the statusline.
|
|
41
|
+
const JOIN_LINE = /^ {2}let result = colored\.join\(SEP\);\r?$/gm;
|
|
42
|
+
// What the block reads at that line, all in the wrapper's own scope there.
|
|
43
|
+
const NAMES = [/\bconst segs\b/, /\bconst colored\b/, /\bconst stripAnsi\b/, /\bconst A = /];
|
|
44
|
+
|
|
45
|
+
const CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
46
|
+
|
|
47
|
+
const DEFAULTS = {
|
|
48
|
+
hudFile: process.env.CLAUDE_UPDATE_LAST_HUD_FILE || join(CONFIG_DIR, "hud", "omc-hud-custom.mjs"),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// OMC 5.6.1 renders the notice as `[Claude${tag}] -> ${latest} claude update`, the
|
|
52
|
+
// tag being "#<running version>" or empty. The block's names are block-scoped, so
|
|
53
|
+
// none can clash with the wrapper's own.
|
|
54
|
+
const BLOCK = [
|
|
55
|
+
OPEN,
|
|
56
|
+
"// claude-update-last (setup-omc-hud): OMC's Claude Code update notice",
|
|
57
|
+
'// ("[Claude#X] -> Y claude update") goes after every other segment.',
|
|
58
|
+
"// `claude-update-last off` removes this block; on any error the order stays as it was.",
|
|
59
|
+
"try {",
|
|
60
|
+
String.raw` const notice = /^\[Claude(?:#[^\]]*)?\]\s*->\s*\S+\s+claude\s+update\b/;`,
|
|
61
|
+
" const plain = (s) => stripAnsi(String(s)).trim();",
|
|
62
|
+
" const found = segs.find((s) => notice.test(plain(s)));",
|
|
63
|
+
" if (found) {",
|
|
64
|
+
' const moved = A("2", plain(found));',
|
|
65
|
+
" for (let i = colored.length - 1; i >= 0; i--) if (notice.test(plain(colored[i]))) colored.splice(i, 1);",
|
|
66
|
+
" colored.push(moved);",
|
|
67
|
+
" }",
|
|
68
|
+
"} catch {",
|
|
69
|
+
" /* keep the order the wrapper chose */",
|
|
70
|
+
"}",
|
|
71
|
+
CLOSE,
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
// The block as `on` writes it, in the line endings of the line it goes before.
|
|
75
|
+
const blockText = (eol) => BLOCK.map((line) => " " + line).join(eol) + eol;
|
|
76
|
+
|
|
77
|
+
const count = (text, needle) => text.split(needle).length - 1;
|
|
78
|
+
// Markers that do not pair up, in number or in order: removing the blocks leaves one behind.
|
|
79
|
+
const unpaired = (src) => {
|
|
80
|
+
const rest = src.replace(BLOCKS, "");
|
|
81
|
+
return rest.includes(OPEN) || rest.includes(CLOSE);
|
|
82
|
+
};
|
|
83
|
+
const PAIR = "claude-update-last 표시(>>> / <<<)의 짝이 맞지 않습니다(개수나 순서)";
|
|
84
|
+
|
|
85
|
+
// Every line that joins the segments, as [offset, line ending].
|
|
86
|
+
const joinLines = (src) => [...src.matchAll(JOIN_LINE)].map((m) => [m.index, m[0].endsWith("\r") ? "\r\n" : "\n"]);
|
|
87
|
+
|
|
88
|
+
// The wrapper's text, null when there is no such file. Any other read failure
|
|
89
|
+
// (permissions, say) is not "missing" and propagates.
|
|
90
|
+
const readText = (file) => {
|
|
91
|
+
try {
|
|
92
|
+
return readFileSync(file, "utf8");
|
|
93
|
+
} catch (e) {
|
|
94
|
+
if (e.code === "ENOENT" || e.code === "ENOTDIR") return null;
|
|
95
|
+
throw e;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const refusal = (why, hudFile) => ({ ok: false, message: `${why}. 래퍼를 그대로 두었습니다: ${hudFile}` });
|
|
100
|
+
|
|
101
|
+
// The block exactly as `on` writes it, once, right before the one joining line.
|
|
102
|
+
function inPlace(src) {
|
|
103
|
+
if (count(src, OPEN) !== 1 || count(src, CLOSE) !== 1) return false;
|
|
104
|
+
const lines = joinLines(src);
|
|
105
|
+
if (lines.length !== 1) return false;
|
|
106
|
+
const [at, eol] = lines[0];
|
|
107
|
+
const block = blockText(eol);
|
|
108
|
+
return src.slice(at - block.length, at) === block;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Why the block cannot go into this wrapper, or "" when it can. Markers that do
|
|
112
|
+
// not pair up would let the block pattern swallow the code between them.
|
|
113
|
+
function misfit(src) {
|
|
114
|
+
if (unpaired(src)) return PAIR;
|
|
115
|
+
const base = src.replace(BLOCKS, "");
|
|
116
|
+
if (!base.includes("omc-hud.mjs")) return "OMC 커스텀 HUD 래퍼가 아닙니다";
|
|
117
|
+
const places = joinLines(base).length;
|
|
118
|
+
if (places === 0) return "삽입 위치를 찾지 못했습니다(세그먼트를 합치는 colored.join(SEP) 줄이 없음)";
|
|
119
|
+
if (places > 1) return "삽입 위치가 두 곳 이상입니다(colored.join(SEP) 줄이 여러 개)";
|
|
120
|
+
if (!NAMES.every((name) => name.test(base))) return "블록이 쓰는 이름(segs, colored, stripAnsi, A)을 래퍼에서 찾지 못했습니다";
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function insertBlock(base) {
|
|
125
|
+
const [[at, eol]] = joinLines(base);
|
|
126
|
+
return base.slice(0, at) + blockText(eol) + base.slice(at);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Replaces file through a fresh temp file ("wx") and a rename: a link planted at
|
|
130
|
+
// either name is replaced rather than written through, and a write that fails
|
|
131
|
+
// leaves the old file as it was.
|
|
132
|
+
function writeFresh(file, data) {
|
|
133
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
134
|
+
rmSync(tmp, { force: true });
|
|
135
|
+
try {
|
|
136
|
+
writeFileSync(tmp, data, { flag: "wx" });
|
|
137
|
+
renameSync(tmp, file);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
rmSync(tmp, { force: true });
|
|
140
|
+
throw e;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The first lines of node's complaint about `text` as an ES module, "" if it parses.
|
|
145
|
+
function syntaxError(text) {
|
|
146
|
+
const dir = mkdtempSync(join(tmpdir(), "claude-update-last-"));
|
|
147
|
+
try {
|
|
148
|
+
const file = join(dir, "check.mjs");
|
|
149
|
+
writeFileSync(file, text);
|
|
150
|
+
const r = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
|
|
151
|
+
return r.status === 0 ? "" : String(r.stderr || r.error).trim().split("\n").slice(0, 3).join(" | ");
|
|
152
|
+
} finally {
|
|
153
|
+
rmSync(dir, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Writes `after` over the wrapper if it parses, keeping `before` in the .bak.
|
|
158
|
+
function commit(hudFile, before, after, done) {
|
|
159
|
+
const err = syntaxError(after);
|
|
160
|
+
if (err) return { ok: false, message: `node --check 실패로 래퍼를 그대로 두었습니다: ${err}` };
|
|
161
|
+
try {
|
|
162
|
+
accessSync(hudFile, constants.W_OK);
|
|
163
|
+
} catch (e) {
|
|
164
|
+
return refusal(`래퍼에 쓸 권한이 없습니다(${e.code || e.message})`, hudFile);
|
|
165
|
+
}
|
|
166
|
+
writeFresh(`${hudFile}.claude-update-last.bak`, before);
|
|
167
|
+
writeFileSync(hudFile, after);
|
|
168
|
+
return { ok: true, message: done };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function turnOn(opts = {}) {
|
|
172
|
+
const o = { ...DEFAULTS, ...opts };
|
|
173
|
+
const src = readText(o.hudFile);
|
|
174
|
+
if (src === null) return { ok: false, message: `HUD 래퍼가 없습니다: ${o.hudFile}` };
|
|
175
|
+
if (inPlace(src)) return { ok: true, message: "이미 켜져 있습니다 (변경 없음)." };
|
|
176
|
+
const why = misfit(src);
|
|
177
|
+
if (why) return refusal(why, o.hudFile);
|
|
178
|
+
const done = "켰습니다. 다음 statusline 갱신부터 Claude Code 갱신 안내가 줄 맨 끝에 표시됩니다.";
|
|
179
|
+
return commit(o.hudFile, src, insertBlock(src.replace(BLOCKS, "")), done);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function turnOff(opts = {}) {
|
|
183
|
+
const o = { ...DEFAULTS, ...opts };
|
|
184
|
+
const src = readText(o.hudFile);
|
|
185
|
+
if (src === null) return { ok: true, message: `HUD 래퍼가 없어 끌 것이 없습니다: ${o.hudFile}` };
|
|
186
|
+
if (unpaired(src)) return refusal(PAIR, o.hudFile);
|
|
187
|
+
const out = src.replace(BLOCKS, "");
|
|
188
|
+
if (out === src) return { ok: true, message: "이미 꺼져 있습니다 (래퍼 변경 없음)." };
|
|
189
|
+
return commit(o.hudFile, src, out, "껐습니다. 다음 statusline 갱신부터 원래 순서로 표시됩니다.");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function stateOf(src) {
|
|
193
|
+
if (src === null) return "HUD 래퍼 없음";
|
|
194
|
+
if (count(src, OPEN) + count(src, CLOSE) === 0) return "꺼짐";
|
|
195
|
+
// on and off both refuse this, so pointing at on would be a dead end.
|
|
196
|
+
if (unpaired(src)) return "표시(>>> / <<<)의 짝이 맞지 않습니다 (on, off 모두 거부: HUD 재설치 또는 남은 표시 줄 정리)";
|
|
197
|
+
return inPlace(src) ? "켜짐" : "켜짐이 불완전하거나 오래됨 (on 으로 복구)";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// What status says about the wrapper. A wrapper it cannot read is reported, not
|
|
201
|
+
// thrown: status is where an exit 2 sends the user to look.
|
|
202
|
+
function describe(o) {
|
|
203
|
+
try {
|
|
204
|
+
return stateOf(readText(o.hudFile));
|
|
205
|
+
} catch (e) {
|
|
206
|
+
return `HUD 래퍼를 읽을 수 없음 (${e.code || e.message})`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function status(opts = {}) {
|
|
211
|
+
const o = { ...DEFAULTS, ...opts };
|
|
212
|
+
return { ok: true, message: `claude-update-last: ${describe(o)} (${o.hudFile})` };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const COMMANDS = new Map([
|
|
216
|
+
["on", turnOn],
|
|
217
|
+
["off", turnOff],
|
|
218
|
+
["status", status],
|
|
219
|
+
]);
|
|
220
|
+
|
|
221
|
+
function main(arg) {
|
|
222
|
+
const run = COMMANDS.get(arg);
|
|
223
|
+
if (!run) {
|
|
224
|
+
process.stdout.write("사용법: claude-update-last on | off | status\n");
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const { ok, message } = run();
|
|
229
|
+
process.stdout.write(message + "\n");
|
|
230
|
+
return ok ? 0 : 1;
|
|
231
|
+
} catch (e) {
|
|
232
|
+
process.stdout.write(`예기치 못한 오류로 중단했습니다: ${e.message}\nstatus 로 상태를 확인하세요.\n`);
|
|
233
|
+
return 2;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Run as a command, also when reached through a symlinked path: Node resolves
|
|
238
|
+
// symlinks for import.meta.url but not for process.argv[1].
|
|
239
|
+
const invokedDirectly = () => {
|
|
240
|
+
try {
|
|
241
|
+
return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
242
|
+
} catch {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if (invokedDirectly()) process.exitCode = main((process.argv[2] || "status").toLowerCase());
|