@kaydash9999/banker-plugins 0.23.2 → 0.24.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.
Files changed (78) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +56 -0
  4. package/README.md +14 -6
  5. package/codex/manifest.json +6 -2
  6. package/codex/skills/teamwork/SKILL.md +19 -6
  7. package/codex/skills/teamwork/scripts/teamwork.mjs +87 -22
  8. package/codex/transform-matrix.md +6 -2
  9. package/package.json +1 -1
  10. package/skills/build-testbox/SKILL.md +213 -0
  11. package/skills/build-testbox/scripts/args.mjs +156 -0
  12. package/skills/build-testbox/scripts/browser-cell.mjs +181 -0
  13. package/skills/build-testbox/scripts/browsers.mjs +164 -0
  14. package/skills/build-testbox/scripts/containers.mjs +237 -0
  15. package/skills/build-testbox/scripts/detect.mjs +166 -0
  16. package/skills/build-testbox/scripts/proc.mjs +91 -0
  17. package/skills/build-testbox/scripts/report.mjs +109 -0
  18. package/skills/build-testbox/scripts/setup.mjs +144 -0
  19. package/skills/build-testbox/scripts/testbox.mjs +148 -0
  20. package/skills/git-init/SKILL.md +152 -0
  21. package/skills/git-init/scripts/git-init.mjs +440 -0
  22. package/skills/git-init/scripts/marker-block.mjs +311 -0
  23. package/skills/git-init/scripts/payload-scan.mjs +186 -0
  24. package/skills/init-dir/SKILL.md +172 -0
  25. package/skills/init-dir/scripts/init-dir.mjs +308 -0
  26. package/skills/init-dir/scripts/marker-block.mjs +311 -0
  27. package/skills/init-dir/scripts/templates.mjs +100 -0
  28. package/skills/init-dir/templates/KB/README.md +15 -0
  29. package/skills/init-dir/templates/docs/README.md +14 -0
  30. package/skills/init-dir/templates/logs/README.md +14 -0
  31. package/skills/init-dir/templates/logs/gitignore +3 -0
  32. package/skills/init-dir/templates/mockup/README.md +7 -0
  33. package/skills/init-dir/templates/scripts/README.md +36 -0
  34. package/skills/init-dir/templates/scripts/lib/service.mjs +540 -0
  35. package/skills/init-dir/templates/scripts/restart.cmd +13 -0
  36. package/skills/init-dir/templates/scripts/restart.sh +10 -0
  37. package/skills/init-dir/templates/scripts/service.conf +5 -0
  38. package/skills/init-dir/templates/scripts/start.cmd +13 -0
  39. package/skills/init-dir/templates/scripts/start.sh +10 -0
  40. package/skills/init-dir/templates/scripts/status.cmd +13 -0
  41. package/skills/init-dir/templates/scripts/status.sh +10 -0
  42. package/skills/init-dir/templates/scripts/stop.cmd +13 -0
  43. package/skills/init-dir/templates/scripts/stop.sh +10 -0
  44. package/skills/init-dir/templates/tests/README.md +15 -0
  45. package/skills/init-dir/templates/tests/run.cmd +13 -0
  46. package/skills/init-dir/templates/tests/run.mjs +160 -0
  47. package/skills/init-dir/templates/tests/run.sh +10 -0
  48. package/skills/init-dir/templates/tests/tests.conf +7 -0
  49. package/skills/init-dir/templates/work/README.md +8 -0
  50. package/skills/progress-mon/README.md +11 -9
  51. package/skills/progress-mon/SKILL.md +53 -16
  52. package/skills/progress-mon/assets/fixture/README.md +1 -1
  53. package/skills/progress-mon/assets/fixture/adr.json +35 -0
  54. package/skills/progress-mon/assets/fixture/arch.json +1 -1
  55. package/skills/progress-mon/assets/fixture/flow.json +2 -2
  56. package/skills/progress-mon/assets/fixture/variants/arch-auto/arch.json +1 -1
  57. package/skills/progress-mon/assets/fixture/variants/legacy-init/rules.json +6 -0
  58. package/skills/progress-mon/assets/fixture/variants/legacy-init/state.json +5 -0
  59. package/skills/progress-mon/references/adr.md +177 -0
  60. package/skills/progress-mon/references/authoring.md +13 -5
  61. package/skills/progress-mon/scripts/pm-adr.mjs +585 -0
  62. package/skills/progress-mon/scripts/pm-check.mjs +48 -20
  63. package/skills/progress-mon/scripts/pm-data.mjs +1 -1
  64. package/skills/progress-mon/scripts/pm-html.mjs +1 -0
  65. package/skills/progress-mon/scripts/pm-memory.mjs +124 -29
  66. package/skills/progress-mon/scripts/pm-render.mjs +6 -4
  67. package/skills/progress-mon/scripts/pm-server.mjs +82 -30
  68. package/skills/progress-mon/scripts/pm-sources.mjs +1 -0
  69. package/skills/progress-mon/scripts/progress-mon.mjs +3 -2
  70. package/skills/tune-tmux/SKILL.md +235 -0
  71. package/skills/tune-tmux/scripts/app-notes.mjs +38 -0
  72. package/skills/tune-tmux/scripts/marker-block.mjs +311 -0
  73. package/skills/tune-tmux/scripts/psmux-caps.json +71 -0
  74. package/skills/tune-tmux/scripts/tmux-config.mjs +251 -0
  75. package/skills/tune-tmux/scripts/tune-tmux.mjs +284 -0
  76. package/skills/ultra-chain/SKILL.md +52 -22
  77. package/skills/ultra-chain/references/chain-ledger.mjs +93 -11
  78. package/skills/ultra-chain/references/chain-report.mjs +13 -2
@@ -0,0 +1,235 @@
1
+ ---
2
+ name: tune-tmux
3
+ description: "tmux, psmux 설정 파일에 표지 블록을 넣고 빼서, tmux 안에서 Claude Code 나 Codex 를 쓸 때 마우스 드래그 복사와 PageUp/PageDown 스크롤이 동작하게 맞춤. 전체 화면 앱의 키와 마우스는 빼앗지 않음. 첫 원본 백업, `off` 로 블록만 뺌, `--reload` 로 떠 있는 서버에 반영. 'tune-tmux'/'tmux 설정'/'psmux 설정'/'tmux 마우스 복사'/'tmux 드래그 복사'/'tmux PageUp 스크롤' 시 사용."
4
+ argument-hint: "[status|on|off] [--target auto|tmux|psmux] [--conf <경로>] [--follow-symlink] [--force-drag] [--reload] [--dry-run] [--json]"
5
+ ---
6
+
7
+ # tune-tmux: tmux, psmux 의 드래그 복사와 PageUp/PageDown 스크롤
8
+
9
+ tmux 설정 파일에 `# banker:tune-tmux:start` .. `# banker:tune-tmux:end` 블록 하나를 넣거나 뺀다.
10
+ 블록 밖 줄은 건드리지 않는다.
11
+ 답변은 한글.
12
+
13
+ | 하위 명령 | 동작 |
14
+ |---|---|
15
+ | `status`(기본) | 대상 도구와 판, 고른 설정 파일, 블록 상태, 넣을 줄과 빼는 줄의 이유, 상충 경고, 앱별 동작 표 |
16
+ | `on` | 블록이 없으면 끝에 덧붙이고, 있으면 블록 내용만 바꿈 |
17
+ | `off` | 블록만 뺌. 나머지 바이트는 그대로 |
18
+
19
+ ## 언제 쓰나
20
+
21
+ - tmux 안의 셸에서 PageUp 으로 기록을 보고 싶을 때
22
+ - 마우스로 끌어 고른 글을 OS 클립보드로 바로 복사하고 싶을 때
23
+ - tmux 안에서 Claude Code, Codex 를 쓰면서 위 두 가지가 앱 동작과 부딪히지 않게 하고 싶을 때
24
+
25
+ ## 쓰지 않을 때
26
+
27
+ | 할 일 | 쓸 스킬 |
28
+ |---|---|
29
+ | tmux, psmux 설치(Rocky8 소스 빌드, Windows psmux) | banker `setup-tmux` |
30
+ | Claude Code, Codex 설정 바꾸기(alternate screen 끄기 등) | 이 스킬은 하지 않음. 아래 앱별 표의 안내를 사용자에게 전함 |
31
+
32
+ - 역할 나눔: `setup-tmux` 는 도구를 설치하고, `tune-tmux` 는 설치된 도구의 설정 파일만 고친다
33
+ - tmux, psmux 가 없으면 `on`, `off` 는 종료 3 과 "setup-tmux 로 먼저 설치" 를 낸다
34
+
35
+ ## 자리 표시
36
+
37
+ | 자리 | 뜻 |
38
+ |---|---|
39
+ | `<S>` | 이 스킬 디렉터리 절대경로(아래 런타임 표의 "스킬 디렉터리") |
40
+
41
+ - 셸 변수는 도구 호출 사이에 남지 않는다. 명령마다 실제 경로를 적는다
42
+
43
+ ## 런타임 표
44
+
45
+ | 항목 | Claude Code | Codex |
46
+ |---|---|---|
47
+ | 스킬 디렉터리 | 스킬 맨 위 "Base directory for this skill:" 경로 | Codex 스킬 목록이 보이는 이 SKILL.md 경로(file)의 폴더. npm 설치는 `~/.codex/skills/banker-tune-tmux/`(프로젝트 범위 설치면 `./.codex/skills/banker-tune-tmux/`), 플러그인 설치(`codex plugin add`)는 `~/.codex/plugins/cache/<마켓플레이스>/banker/<판>/skills/tune-tmux/`. 절대경로로 씀 |
48
+ | `status` | 바로 실행 | 바로 실행(읽기와 사설 tmux 서버 하나) |
49
+ | `on`, `off` | 바로 실행 | 설정 파일(`~/.tmux.conf` 등)이 작업 폴더 밖이라 명령에 `sandbox_permissions: "require_escalated"` 로 승인 요청. `--add-dir` 로 설정 폴더를 넓히지 않음 |
50
+ | `--reload` | 바로 실행 | tmux 서버 소켓(`/tmp/tmux-<uid>/`)이 작업 폴더 밖이라 같은 승인 요청 |
51
+
52
+ ## 실행
53
+
54
+ 1. 인자 확인
55
+ - 하위 명령은 `status`, `on`, `off`. 그 밖은 실행하지 않고 사용법 안내
56
+ - 사용자 입력을 셸 명령에 그대로 붙이지 않음
57
+ 2. 상태 먼저 보기
58
+
59
+ ```bash
60
+ node "<S>/scripts/tune-tmux.mjs" status
61
+ ```
62
+
63
+ 3. 넣기와 빼기
64
+
65
+ ```bash
66
+ node "<S>/scripts/tune-tmux.mjs" on [--reload] [--force-drag]
67
+ node "<S>/scripts/tune-tmux.mjs" off [--reload]
68
+ ```
69
+
70
+ - 계획만 보려면 `--dry-run`. 기계가 읽을 출력은 `--json`(JSON 한 줄)
71
+ - `--force-drag` 는 사용자가 맞바꿈(아래 절)을 듣고 고를 때만 붙임
72
+ 4. 결과 처리
73
+ - `결과:` 줄, `넣지 않은 줄:` 의 이유, `경고:` 줄을 그대로 전함
74
+ - psmux 의 `넣은 줄 0: psmux 기본값 유지. 미확인 n, 안 됨 n` 줄과 그 아래 세 목록(되는 것, 확인 못 한 것, 안 되는 것)을 그대로 전함. 종료 0 을 "원하는 동작이 다 됨" 으로 보고하지 않음
75
+ - `status` 의 앱별 동작 표를 사용자에게 보이고, 쓰는 앱(Claude Code, Codex)의 행을 짚어 줌
76
+ 5. 종료 코드
77
+
78
+ | 코드 | 뜻 | 처리 |
79
+ |---|---|---|
80
+ | 0 | 완료. 바뀐 것 없음 포함. psmux 에 넣을 줄이 없는 경우 포함(파일 처리 성공일 뿐, 미확인과 안 되는 동작이 남음) | 결과 보고. psmux 는 미확인, 안 됨 수와 목록도 |
81
+ | 1 | 거부(링크, 하드 링크, 소유자, 표지 손상, UTF-8 아님, 특수 파일), 쓰기 실패, `--reload` 반영 실패. 거부는 아무것도 바꾸지 않음 | 메시지 그대로 전하고 아래 "문제 해결" |
82
+ | 2 | 사용 오류 | 인자를 고쳐 다시 실행 |
83
+ | 3 | `on`, `off` 인데 tmux, psmux 둘 다 없음 | banker `setup-tmux` 안내. `status` 는 이때 종료 0 과 "미설치" |
84
+
85
+ - 출력이 비어 있으면 실행 실패. 성공으로 보고하지 않음
86
+
87
+ ## 인자
88
+
89
+ | 인자 | 뜻 | 기본 |
90
+ |---|---|---|
91
+ | `--target auto\|tmux\|psmux` | `auto` 는 Windows 에서 psmux(없고 tmux 가 있으면 tmux), 그 밖은 tmux. `tmux -V` 둘째 줄이 psmux 면 psmux 로 처리 | `auto` |
92
+ | `--conf <경로>` | 블록을 넣을 파일. tmux 가 스스로 읽지 않는 파일이면 경고 | 아래 고르기 규칙 |
93
+ | `--follow-symlink` | 설정 파일이 링크(dotfiles)일 때 가리키는 파일에 씀. 링크는 링크로 남음 | 꺼짐(링크면 종료 1) |
94
+ | `--force-drag` | 앱이 마우스를 잡아도 드래그를 tmux copy-mode 선택으로 바꿈 | 꺼짐 |
95
+ | `--reload` | 떠 있는 서버에 반영 | 꺼짐 |
96
+ | `--socket <이름>` | 기본 서버 대신 `tmux -L <이름>` 서버에 반영(시험과 사설 서버용) | 없음 |
97
+ | `--dry-run`, `--json` | 계획만, JSON 한 줄 | 꺼짐 |
98
+
99
+ ## 블록 내용(tmux)
100
+
101
+ | 줄 | 효과 | 조건 |
102
+ |---|---|---|
103
+ | `set -g mouse on` | 마우스 사용 | 늘 |
104
+ | `set -s set-clipboard on` | OSC 52 로 터미널 클립보드에도 씀 | 늘 |
105
+ | `set -s copy-command '<클립보드 명령>'` | copy-mode 의 기본 `MouseDragEnd1Pane`(복사 후 끝냄)이 OS 클립보드로 보냄 | tmux 3.2 이상, 클립보드 도구 있음 |
106
+ | `bind -T copy-mode`(와 `copy-mode-vi`) `MouseDragEnd1Pane send -X copy-pipe-and-cancel '<클립보드 명령>'` | 같은 효과 | tmux 2.4 이상 3.2 미만, 클립보드 도구 있음 |
107
+ | `bind -n PPage if -F "#{\|\|:#{alternate_on},#{pane_in_mode}}" "send-keys PPage" "copy-mode -eu"` | 일반 화면에서 PageUp 은 copy-mode 진입과 한 쪽 위로. alternate screen 이나 모드 중이면 키를 그대로 넘김 | 사설 서버에서 `#{\|\|:0,1}#{\|\|:0,0}` 가 `10` 일 때 |
108
+ | `bind -n MouseDrag1Pane if -F "#{pane_in_mode}" "send-keys -M" "copy-mode -M"` | 앱이 마우스를 잡아도 tmux 선택 | `--force-drag` 일 때만 |
109
+
110
+ - 루트 `MouseDrag1Pane` 기본(앱이 마우스를 잡으면 앱으로, 아니면 copy-mode 선택)은 바꾸지 않는다
111
+ - PageDown 은 바인딩하지 않는다. copy-mode 안에서는 copy-mode 표가 받고(바닥에 닿으면 `-e` 로 빠짐), 밖에서는 앱이 받는다
112
+ - 클립보드 명령 고르기: macOS `pbcopy`, Wayland(`WAYLAND_DISPLAY`) `wl-copy`, X11(`DISPLAY`) `xclip -selection clipboard -in` 다음 `xsel -ib`, WSL `clip.exe`. 하나도 없으면 클립보드 줄을 넣지 않고 OSC 52 만 쓴다. 이때 터미널이 OSC 52 를 허용해야 한다
113
+ - 조건식 탐지는 `tmux -L banker-tune-<난수> -f /dev/null` 사설 서버를 띄워 묻고 바로 끈다. 사용자 서버는 건드리지 않는다
114
+
115
+ ## 설정 파일 고르기
116
+
117
+ | 대상 | 순서 | 실측 |
118
+ |---|---|---|
119
+ | tmux 3.1 이상 | `--conf` > `~/.tmux.conf`(있으면) > `$XDG_CONFIG_HOME/tmux/tmux.conf` > `~/.config/tmux/tmux.conf`. 셋 다 없으면 `~/.tmux.conf` 를 새로 만듦 | tmux 3.6a 는 있는 파일을 모두 읽음(사용자 옵션 `@banker_a`, `@banker_b`, `@banker_c` 를 각 파일에 두고 `show -gv` 로 확인) |
120
+ | tmux 3.1 미만 | `--conf` > `~/.tmux.conf` | tmux 2.7 은 `~/.tmux.conf` 만 읽음(같은 방법) |
121
+ | psmux | `--conf` > `PSMUX_CONFIG_FILE` > `~/.psmux.conf`, `~/.psmuxrc`, `~/.tmux.conf`, `~/.config/psmux/psmux.conf` 중 먼저 있는 것. 없으면 `~/.psmux.conf` | psmux 3.3.8 은 처음 찾은 파일 하나만 읽음(`-f` 가 가장 앞) |
122
+
123
+ - 상충 점검: tmux 가 함께 읽는 다른 파일에 `mouse off`, `set-clipboard off`, 루트 `PPage` 바인딩이 있으면 `status`, `on` 이 파일과 줄 번호로 경고한다. tmux 2.7 처럼 그 파일을 읽지 않는 판에서는 경고하지 않는다
124
+
125
+ ## 앱별 동작(tmux, 블록을 넣은 뒤)
126
+
127
+ | 앱 상태 | PageUp/PageDown | 드래그 | 출처 |
128
+ |---|---|---|---|
129
+ | 일반 화면(셸, Codex `--no-alt-screen` 이나 `/tui` 의 Scrollback, Claude Code 옛 렌더러) | PageUp 은 tmux copy-mode 진입과 한 쪽 위로. copy-mode 안 PageDown 은 아래로, 바닥에 닿으면 copy-mode 를 빠짐 | copy-mode 선택. 놓으면 OS 클립보드(도구가 없으면 OSC 52) | 이 스킬의 통합 시험(tmux 3.6a, 2.7) |
130
+ | Claude Code 전체 화면(alternate screen, 앱이 마우스를 잡음) | 둘 다 앱이 받음(반 화면 스크롤). "Keyboard scrolling with PgUp and PgDn works either way". 휠은 tmux `mouse on` 이 필요 | 앱 안 선택. 놓으면 클립보드와 tmux paste buffer. 앱 밖 선택은 Shift 드래그(Terminal.app 은 Fn, iTerm2 는 Option). 마우스를 끄려면 `CLAUDE_CODE_DISABLE_MOUSE=1` | https://code.claude.com/docs/en/fullscreen |
131
+ | Codex Fullscreen(기본, alternate screen) | 둘 다 앱이 받음(휠, PgUp/PgDn, Ctrl+Home/End 로 Codex 기록 스크롤) | Codex 가 선택하고 바로 복사(`[tui] copy_on_select` 기본 `auto`) | https://github.com/openai/codex/discussions/49129 |
132
+
133
+ - Codex 한계: Fullscreen 에서는 기록이 Codex 안에 있고 tmux 는 현재 화면만 본다. tmux copy-mode 로 Codex 기록을 스크롤하는 것은 tmux 설정으로 고칠 수 없다
134
+ - 공식 우회: `/tui` 에서 Scrollback 을 고른 뒤 재시작, `codex --no-alt-screen`, 설정 `tui.alternate_screen = "never"`, 복사용 `tui.raw_output_mode`(`/raw`, `alt-r`)
135
+ - `--no-alt-screen` 이어도 전체 다시 그리기 때문에 스크롤백이 덜 쌓인다는 보고가 있다(Zellij, tmux 미확인)
136
+ - 이 스킬은 Codex 설정을 바꾸지 않는다. 우회는 사용자에게 안내만 한다
137
+ - Claude Code 의 옛 렌더러(`CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1` 이나 `/tui default`)에서는 일반 화면 행대로 tmux copy-mode 를 쓴다
138
+ - tmux 의 검색과 copy-mode 는 Claude Code 전체 화면의 대화를 못 본다. Claude Code 의 `Ctrl+o` 뒤 `[` 가 대화를 네이티브 스크롤백에 쓴다
139
+
140
+ ## psmux(Windows)
141
+
142
+ psmux 3.3.8 실측(Windows 10 22H2 박스, 2026-11-01) 결과 블록에 넣을 줄이 없다.
143
+ psmux 기본값으로 모든 요구가 채워지는 것은 아니다. 되는 것, 확인 못 한 것, 안 되는 것이 나뉜다.
144
+ `on` 은 종료 0 과 `넣은 줄 0: psmux 기본값 유지. 미확인 2, 안 됨 3` 줄, 그 아래 세 목록을 낸다(`--json` 은 `note` 와 `psmux`).
145
+ 그 설정 파일에 옛 tune-tmux 블록이 있으면 `off` 처럼 블록만 뺀다(`넣을 줄이 0개라 옛 tune-tmux 블록을 뺌` 줄). 없으면 파일을 바꾸지 않는다.
146
+ 판단 근거는 `scripts/psmux-caps.json` 의 표다. `verified: true` 이고 psmux 기본값이 아닌 줄만 넣는다. 세 목록은 같은 파일의 `behavior` 다.
147
+
148
+ | 구분 | 내용 |
149
+ |---|---|
150
+ | 기본값으로 되는 것 | 마우스(`mouse on`) |
151
+ | 기본값으로 되는 것 | `set-clipboard on` |
152
+ | 기본값으로 되는 것 | 휠로 copy-mode 진입(`scroll-enter-copy-mode on`) |
153
+ | 확인 못 한 것 | 드래그한 글이 Windows 클립보드로 가는지 |
154
+ | 확인 못 한 것 | 전체 화면 앱 안의 PageUp/PageDown 을 앱이 받는지 |
155
+ | 안 되는 것 | 앱 밖 PageUp 은 prefix 뒤에만 copy-mode 진입 |
156
+ | 안 되는 것 | PageDown 이 바닥에서 copy-mode 를 빠지지 않음 |
157
+ | 안 되는 것 | `#{alternate_on}` 이 늘 0 이라 전체 화면 앱을 구분하지 못함 |
158
+
159
+ | 사용자 요구 | psmux 기본값으로 되는 것 | 안 되거나 미확인 |
160
+ |---|---|---|
161
+ | 드래그 복사 | `mouse on`, `mouse-selection on`, `set-clipboard on` 이 기본이라 내장 선택이 동작 | 고른 글이 OS 클립보드로 가는지는 실제 마우스와 데스크톱이 필요해 미확인 |
162
+ | PageUp | prefix 뒤 PageUp 이 copy-mode 진입과 한 쪽 위로(기본 바인딩 `copy-mode -u`) | 루트 PageUp 바인딩은 넣지 않음. Windows 10 에서 `#{alternate_on}` 이 늘 0 이라 넣으면 전체 화면 앱이 PageUp 을 못 받음(실측: 앱 로그가 빔) |
163
+ | PageDown | copy-mode 안에서 아래로 스크롤 | 바닥에서 copy-mode 를 빠지지 않음(`-e` 미지원). Escape 로 빠짐 |
164
+ | 전체 화면 앱(Claude Code, Codex) | tune-tmux 가 키를 가로채지 않음 | 블록 없이 앱이 PageUp 을 받는지는 미실측. 휠은 psmux 이슈 #597, #598 |
165
+
166
+ - psmux 함정(실측)
167
+ - 붙인 짧은 플래그를 풀지 못한다. `copy-mode -eu` 는 `-u` 가 사라지므로 `copy-mode -e -u` 처럼 따로 쓴다
168
+ - `copy-pipe-and-cancel` 바인딩과 루트 `MouseDrag1Pane` 바인딩은 경고 없이 버려진다. 그래서 `--force-drag` 줄도 넣지 않는다
169
+ - CLI 의 `bind` 는 따옴표 묶음을 잃는다. 바인딩은 설정 파일로만 넣는다
170
+ - 줄 끝 주석은 3.3.8 에서 값으로 읽히지 않는다
171
+
172
+ ## `--force-drag` 맞바꿈
173
+
174
+ - 얻는 것: Claude Code 전체 화면이나 Codex 처럼 마우스를 잡는 앱 위에서도 드래그가 늘 tmux 선택이 되어, 놓으면 tmux 의 복사 경로로 간다
175
+ - 잃는 것: 앱의 마우스 클릭, 드래그 처리를 tmux 가 빼앗는다. Claude Code 전체 화면의 앱 안 선택과 클릭이 안 된다
176
+ - 기본은 꺼짐이다. 사용자가 이 맞바꿈을 듣고 고를 때만 붙인다. 되돌리려면 `--force-drag` 없이 `on --reload`
177
+
178
+ ## `--reload`
179
+
180
+ - 기본 서버(`tmux ls` 성공)가 떠 있을 때만 반영한다. 없으면 "떠 있는 서버 없음. 다음 시작 때 적용" 으로 종료 0
181
+ - 예전 블록에만 있던 줄의 효과를 먼저 되돌린다
182
+ - 옵션은 `set -gu`, `set -su`
183
+ - 키는 설정 없이 띄운 사설 서버의 기본 바인딩을 그대로 다시 넣고, 기본이 없으면 `unbind`
184
+ - 그 뒤 설정 파일 전체를 `source-file` 해 블록 밖 사용자 줄을 되살린다. 사용자 파일의 다른 명령도 다시 실행된다
185
+ - `off --reload` 뒤 루트 `PPage` 는 비고, 사용자가 둔 `set -g mouse on` 같은 줄은 다시 살아 있다(통합 시험)
186
+
187
+ ## 오프라인
188
+
189
+ - 네트워크를 쓰지 않는다
190
+
191
+ ## 함정
192
+
193
+ - tmux 3.1 이상은 `~/.tmux.conf` 와 `~/.config/tmux/tmux.conf` 를 모두 읽는다. 뒤 파일의 `mouse off` 가 블록을 덮을 수 있어 경고한다
194
+ - 블록을 손으로 고치면 다음 `on` 이 다시 쓴다. 손으로 쓸 줄은 블록 밖에 둔다
195
+ - 표지 줄 하나만 지우거나 문장 안에 표지를 옮기면 거부(종료 1)한다. 짝을 임의로 맞추지 않는다
196
+ - `off` 로 블록만 있던 파일(이 스킬이 만든 파일)은 빈 파일로 남는다. 지우지 않는다
197
+ - `source-file` 은 지운 바인딩을 되돌리지 않는다. 그래서 `--reload` 가 되돌리기 명령을 먼저 보낸다
198
+ - tmux 3.6a 의 `send-keys -K` 는 바인딩 없는 PageDown 을 `k` 로 보냈다(실측). 키 시험은 바깥 서버 창에 키를 보내 실제 터미널 입력 경로로 한다
199
+
200
+ ## 문제 해결
201
+
202
+ - 아래 표에서 파일을 보인다는 것은 `ls -l` 로 그 이름의 종류와 링크 대상 경로를 보이는 것이다. 링크가 아닌 일반 파일임을 본 뒤에만 내용을 연다
203
+
204
+ | 메시지 | 조치 |
205
+ |---|---|
206
+ | 심볼릭 링크라 그대로 둠 | 설정 파일이 링크(dotfiles). 링크와 대상 경로를 보이고, 대상에 쓸지 물은 뒤 `--follow-symlink` 로 다시 실행 |
207
+ | 끊어진 링크라 그대로 둠 | 링크가 가리키는 파일이 없음. 링크를 지우거나 바꾸지 않음. 어떻게 할지 물음 |
208
+ | 하드 링크가 n개라 그대로 둠 | 같은 파일에 이름이 여럿. 쓰면 링크가 갈라짐. 어느 이름을 쓸지 물음 |
209
+ | 다른 사용자 소유라 그대로 둠 | 파일 소유자가 실행 사용자와 다름. 소유자 확인을 안내 |
210
+ | 표지 손상이라 그대로 둠 | 표지 줄이 한쪽만 있거나 문장 안에 있거나 블록이 둘 이상. 남은 표지 줄을 보이고 지울지 물은 뒤 다시 실행 |
211
+ | UTF-8 이 아니라 그대로 둠 | 인코딩을 보이고 UTF-8 로 바꿀지 물음 |
212
+ | 이 tmux 는 #{\|\|:} 조건식을 읽지 못함 | PageUp 줄을 넣지 않은 이유. 새 tmux 는 banker `setup-tmux` |
213
+ | 외부 클립보드 도구 없음 | 터미널 설정에서 OSC 52 클립보드 쓰기를 허용하거나 `xclip`, `wl-copy` 등을 설치한 뒤 `on` 다시 실행 |
214
+ | 떠 있는 서버 반영 실패 | 파일은 바뀌었음. 메시지의 `source-file` 오류 줄을 보이고 사용자 설정의 해당 줄을 확인 |
215
+
216
+ ## 수동 확인 항목(자동 시험 아님)
217
+
218
+ - 실제 Claude Code 전체 화면을 tmux 안에서 띄워 드래그와 PageUp/PageDown 이 앱별 표대로인지
219
+ - 실제 Codex Fullscreen 을 tmux 안에서 띄워 같은 확인
220
+ - psmux 에서 드래그한 글이 Windows 클립보드로 가는지, 블록 없이 전체 화면 앱이 PageUp 을 받는지
221
+
222
+ ## 출처(조사한 것)
223
+
224
+ | 주제 | 확인한 사실 | 출처 |
225
+ |---|---|---|
226
+ | Claude Code 전체 화면 | PgUp/PgDn 과 휠을 앱이 처리, 앱 안 선택은 클립보드와 tmux paste buffer, 앱 밖은 Shift 드래그, 끄는 환경 변수 | https://code.claude.com/docs/en/fullscreen |
227
+ | Codex Fullscreen | 기본 Fullscreen, 기록은 Codex 안, 선택 즉시 복사, `/tui` 로 Scrollback | https://github.com/openai/codex/discussions/49129, https://github.com/openai/codex/issues/48057 |
228
+ | Codex tmux 스크롤 | tmux `Ctrl+B [` 가 스크롤 안 됨, 공식 답은 Scrollback 이나 `--no-alt-screen` | https://community.openai.com/t/umm-looks-like-recent-versions-may-prevent-scrolling-up-for-more-information-in-tmux-session-via-ctrl-b/1402100 |
229
+ | Codex 설정 | `tui.alternate_screen = auto \| always \| never`, `tui.raw_output_mode` | https://learn.chatgpt.com/docs/config-file/config-reference |
230
+ | Codex `--no-alt-screen` 한계 | Zellij 에서 스크롤백이 거의 안 쌓임 | https://github.com/openai/codex/issues/10331 |
231
+ | tmux 관용구와 판 | PageUp `if -F` 관용구, `copy-mode -e` 2.1, `copy-command` 3.2 | https://raw.githubusercontent.com/tmux/tmux/master/CHANGES |
232
+ | tmux 기본 바인딩 | 3.6a 루트 `MouseDrag1Pane` 은 `#{\|\|:#{pane_in_mode},#{mouse_any_flag}}` 면 앱으로, copy-mode `MouseDragEnd1Pane` 은 `copy-pipe-and-cancel`. 2.7 은 `copy-selection-and-cancel` | `tmux -f /dev/null` 사설 서버의 `list-keys`(실측) |
233
+ | tmux 2.7 조건식 | `#{\|\|:0,1}` 은 `1`, `#{\|\|:0,0}` 은 `0`. 2.7 도 조건식을 읽음 | Rocky 8 의 `/usr/bin/tmux` 2.7 사설 서버(실측) |
234
+ | psmux | 설정 파일 순서, 기본값, 플래그와 바인딩 동작, `alternate_on` | psmux 3.3.8 Windows 10 박스 실측, https://github.com/psmux/psmux |
235
+ | psmux 이슈 | SGR 마우스가 앱으로 안 감(#597), Codex 휠(#598) | https://github.com/psmux/psmux/issues/597, https://github.com/psmux/psmux/issues/598 |
@@ -0,0 +1,38 @@
1
+ /**
2
+ * app-notes: how PageUp/PageDown and mouse drag behave per app state with the tune-tmux block, for `status`.
3
+ * Same content as the "앱별 동작" tables in SKILL.md (sources there). Text only; nothing is read or written.
4
+ */
5
+
6
+ const TMUX = [
7
+ {
8
+ state: "일반 화면(셸, Codex `--no-alt-screen` 이나 /tui Scrollback, Claude Code 옛 렌더러)",
9
+ keys: "PageUp 은 tmux copy-mode 진입과 한 쪽 위로. copy-mode 안 PageDown 은 아래로, 바닥에 닿으면 copy-mode 를 빠짐",
10
+ drag: "tmux copy-mode 선택. 놓으면 OS 클립보드(도구가 없으면 OSC 52 로 터미널 클립보드)",
11
+ },
12
+ {
13
+ state: "Claude Code 전체 화면(alternate screen, 앱이 마우스를 잡음)",
14
+ keys: "둘 다 앱이 받음(반 화면 스크롤). tune-tmux 는 가로채지 않음",
15
+ drag: "앱 안 선택. 놓으면 클립보드와 tmux paste buffer. 앱 밖 선택은 Shift 드래그. 늘 tmux 선택을 원하면 `--force-drag`(앱 클릭을 빼앗음)",
16
+ },
17
+ {
18
+ state: "Codex Fullscreen(alternate screen)",
19
+ keys: "둘 다 앱이 받음(Codex 기록 스크롤). tmux copy-mode 로는 Codex 기록을 못 봄(tmux 설정으로 못 고침). tmux 스크롤이 필요하면 /tui 의 Scrollback, `codex --no-alt-screen`, `tui.alternate_screen = \"never\"`",
20
+ drag: "Codex 가 선택하고 바로 복사(`tui.copy_on_select`). 늘 tmux 선택을 원하면 `--force-drag`",
21
+ },
22
+ ];
23
+
24
+ const PSMUX = [
25
+ {
26
+ state: "일반 화면(셸)",
27
+ keys: "psmux 기본에는 루트 PageUp 바인딩이 없음. prefix 뒤 PageUp 이 copy-mode 진입과 한 쪽 위로. copy-mode 안 PageUp/PageDown 은 스크롤하지만 바닥에서 빠지지 않음(-e 미지원, Escape 로 빠짐)",
28
+ drag: "psmux 기본(mouse on, mouse-selection on, set-clipboard on)의 내장 선택. OS 클립보드로 가는지는 미확인",
29
+ },
30
+ {
31
+ state: "전체 화면 앱(Claude Code, Codex)",
32
+ keys: "tune-tmux 는 블록을 넣지 않으므로 키를 가로채지 않음. 앱이 받는지는 미실측. Windows 10 에서 #{alternate_on} 이 늘 0 이라 tmux 식 PageUp 바인딩을 넣으면 앱이 PageUp 을 못 받음",
33
+ drag: "앱의 선택 동작을 따름. psmux 3.3.8 은 SGR 마우스가 앱으로 안 가는 이슈(#597)가 열려 있음",
34
+ },
35
+ ];
36
+
37
+ /** The rows for target "tmux" or "psmux": { state, keys, drag }. */
38
+ export const appRows = (target) => (target === "psmux" ? PSMUX : TMUX);
@@ -0,0 +1,311 @@
1
+ /**
2
+ * marker-block: puts one marked block of lines into a text file, replaces it or takes it out, and leaves every
3
+ * byte outside the block as it was.
4
+ *
5
+ * The canonical copy is skills/init-dir/scripts/marker-block.mjs. tune-tmux and git-init carry byte-identical
6
+ * copies, since Codex installs every skill folder on its own; scripts/sync-adapter.js mirrors and checks them.
7
+ * Edit the canonical copy only.
8
+ *
9
+ * Policy (one place for every skill that edits a user's file):
10
+ * - Markers are whole lines `<open> <id>:start <close>` and `<open> <id>:end <close>`, with the comment syntax
11
+ * given (`#` or `<!-- -->`). A marker inside a sentence, an unpaired marker, an end before a start or a second
12
+ * block is refused: nothing is cut on a guess.
13
+ * - Bytes outside the block stay as they were. CRLF files get CRLF blocks, a byte order mark stays in front.
14
+ * A file that is not UTF-8 is refused.
15
+ * - A folder, FIFO, socket or device at the name is refused without being read.
16
+ * - A symbolic link is refused (refused: "symlink") unless followSymlink is set; then the file it leads to is
17
+ * written and the link stays a link. A link to nothing is refused either way ("dangling").
18
+ * - A file with two or more hard links is refused ("hardlink"): the rename below would split them.
19
+ * - A file owned by another user is refused ("owner"). The permission bits of the file carry over to the new
20
+ * file. Windows has no owner or permission bits to check.
21
+ * - Writes go to a temp file in the same folder and a rename. A failed write removes the temp file.
22
+ * - When the bytes change, the first original is kept in `<file>.<skill>.bak`; an existing backup is never
23
+ * overwritten.
24
+ *
25
+ * Results are { ok, changed, refused?, message, path, file }. Callers map refused to exit status 1.
26
+ */
27
+ import {
28
+ chmodSync,
29
+ closeSync,
30
+ constants,
31
+ fstatSync,
32
+ lstatSync,
33
+ openSync,
34
+ readFileSync,
35
+ readlinkSync,
36
+ realpathSync,
37
+ renameSync,
38
+ rmSync,
39
+ statSync,
40
+ writeFileSync,
41
+ } from "node:fs";
42
+ import { basename, dirname, join, resolve } from "node:path";
43
+
44
+ /** Comment syntaxes. */
45
+ export const HTML = Object.freeze({ open: "<!--", close: "-->" });
46
+ export const HASH = Object.freeze({ open: "#", close: "" });
47
+
48
+ const BOM = "\u{FEFF}";
49
+ const UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
50
+ const MAX_BYTES = 32 * 1024 * 1024;
51
+ const NEW_FILE_MODE = 0o644;
52
+ const BACKUP_MODE = 0o600;
53
+
54
+ /** The start and end marker lines for id in a comment syntax. */
55
+ export function markers(comment, id) {
56
+ const tail = comment.close ? ` ${comment.close}` : "";
57
+ return { start: `${comment.open} ${id}:start${tail}`, end: `${comment.open} ${id}:end${tail}` };
58
+ }
59
+
60
+ const refusal = (code, message, path) => ({ ok: false, changed: false, refused: code, message, path });
61
+ const bare = (line) => line.replace(/\r?\n$/, "").replace(/[ \t]+$/, "");
62
+ const eolOf = (text) => (text.includes("\r\n") ? "\r\n" : "\n");
63
+ const readlinkOr = (path) => {
64
+ try {
65
+ return readlinkSync(path);
66
+ } catch {
67
+ return "?";
68
+ }
69
+ };
70
+ const realOrNull = (path) => {
71
+ try {
72
+ return realpathSync(path);
73
+ } catch {
74
+ return null;
75
+ }
76
+ };
77
+
78
+ // Where the bytes live: the path itself, or with followSymlink the file a link leads to.
79
+ function locate(path, follow) {
80
+ const lst = lstatSync(path, { throwIfNoEntry: false });
81
+ if (!lst) return { file: path, exists: false };
82
+ if (!lst.isSymbolicLink()) return { file: path, exists: true };
83
+ const target = readlinkOr(path);
84
+ if (!follow) return { refuse: refusal("symlink", `심볼릭 링크라 그대로 둠: ${path} -> ${target}`, path) };
85
+ const real = realOrNull(path);
86
+ if (real === null) return { refuse: refusal("dangling", `끊어진 링크라 그대로 둠: ${path} -> ${target}`, path) };
87
+ return { file: real, exists: true, link: path };
88
+ }
89
+
90
+ // Why the file at file may not be read or replaced, or null.
91
+ function fileProblem(st, file, uid) {
92
+ if (!st.isFile()) return refusal("special", `일반 파일이 아니라 그대로 둠: ${file}`, file);
93
+ if (st.nlink > 1) return refusal("hardlink", `하드 링크가 ${st.nlink}개라 그대로 둠: ${file}`, file);
94
+ if (uid !== undefined && st.uid !== uid) return refusal("owner", `다른 사용자 소유라 그대로 둠: ${file}`, file);
95
+ if (st.size > MAX_BYTES) return refusal("too-big", `32MiB 보다 커서 그대로 둠: ${file}`, file);
96
+ return null;
97
+ }
98
+
99
+ // The file's stat and bytes, opened without blocking so a FIFO planted after the look never waits.
100
+ function readChecked(file, uid) {
101
+ const first = statSync(file);
102
+ const problem = fileProblem(first, file, uid);
103
+ if (problem) return { refuse: problem };
104
+ const fd = openSync(file, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
105
+ try {
106
+ const st = fstatSync(fd);
107
+ const late = fileProblem(st, file, uid);
108
+ return late ? { refuse: late } : { st, bytes: readFileSync(fd) };
109
+ } finally {
110
+ closeSync(fd);
111
+ }
112
+ }
113
+
114
+ // What one line is: "start", "end", "inline" (a marker inside other text) or null.
115
+ function kindOf(line, mk) {
116
+ const b = bare(line);
117
+ if (b === mk.start) return "start";
118
+ if (b === mk.end) return "end";
119
+ return line.includes(mk.start) || line.includes(mk.end) ? "inline" : null;
120
+ }
121
+
122
+ // Takes line i of kind into state; returns why the markers are broken, or null.
123
+ function step(state, kind, i) {
124
+ if (kind === "inline") return `문장 안에 표지가 있음(${i + 1}행)`;
125
+ if (kind === "start") {
126
+ if (state.open !== -1 || state.found) return `표지 짝이 맞지 않거나 블록이 둘 이상(${i + 1}행)`;
127
+ state.open = i;
128
+ } else if (kind === "end") {
129
+ if (state.open === -1) return `끝 표지가 시작 표지보다 먼저(${i + 1}행)`;
130
+ state.found = { first: state.open, last: i };
131
+ state.open = -1;
132
+ }
133
+ return null;
134
+ }
135
+
136
+ /** The block's first and last line in lines, null when there is none, or { error } when markers are broken. */
137
+ export function findBlock(lines, mk) {
138
+ const state = { open: -1, found: null };
139
+ for (let i = 0; i < lines.length; i++) {
140
+ const error = step(state, kindOf(lines[i], mk), i);
141
+ if (error) return { error };
142
+ }
143
+ if (state.open !== -1) return { error: `짝 없는 시작 표지(${state.open + 1}행)` };
144
+ return state.found;
145
+ }
146
+
147
+ // Whether line i sits inside an open wrap region (more wrap opens than closes before it).
148
+ function insideWrap(lines, i, wrap) {
149
+ let depth = 0;
150
+ for (let j = 0; j < i; j++) {
151
+ const b = bare(lines[j]);
152
+ if (b === wrap.open) depth++;
153
+ else if (b === wrap.close && depth > 0) depth--;
154
+ }
155
+ return depth > 0;
156
+ }
157
+
158
+ // The span [first, last] widened to wrap lines directly around it, and whether new text must bring its own wrap.
159
+ function spanOf(lines, found, wrap) {
160
+ if (!wrap) return { ...found, wrapNew: false };
161
+ const wrapped = found.first > 0 && bare(lines[found.first - 1]) === wrap.open && bare(lines[found.last + 1] ?? "") === wrap.close;
162
+ if (wrapped) return { first: found.first - 1, last: found.last + 1, wrapNew: true };
163
+ return { ...found, wrapNew: !insideWrap(lines, found.first, wrap) };
164
+ }
165
+
166
+ const blockText = (mk, body, wrap, eol) => {
167
+ const inner = [mk.start, ...body.replace(/\r\n/g, "\n").replace(/\n$/, "").split("\n"), mk.end];
168
+ return [...(wrap ? [wrap.open, ...inner, wrap.close] : inner), ""].join(eol);
169
+ };
170
+
171
+ // The text with the block put in, replaced or (body null) taken out. Appending puts one line break between the
172
+ // text and the block; taking a block out at the end of the file takes that break back, so add then remove gives
173
+ // the bytes from before.
174
+ function render(text, found, o) {
175
+ const lines = text.split(/(?<=\n)/);
176
+ const eol = eolOf(text);
177
+ if (!found) {
178
+ if (o.body === null) return text;
179
+ return text === "" ? blockText(o.mk, o.body, o.wrap, eol) : text + eol + blockText(o.mk, o.body, o.wrap, eol);
180
+ }
181
+ const span = spanOf(lines, found, o.wrap);
182
+ const before = lines.slice(0, span.first).join("");
183
+ const after = lines.slice(span.last + 1).join("");
184
+ if (o.body !== null) return before + blockText(o.mk, o.body, span.wrapNew ? o.wrap : null, eol) + after;
185
+ return after === "" ? before.replace(/\r?\n$/, "") : before + after;
186
+ }
187
+
188
+ function checkBody(o) {
189
+ if (o.body === null) return;
190
+ const forbidden = [o.mk.start, o.mk.end, ...(o.wrap ? [o.wrap.open, o.wrap.close] : [])];
191
+ if (o.body.split(/\r?\n/).some((line) => forbidden.includes(bare(line)))) throw new Error("block body holds a marker line");
192
+ }
193
+
194
+ // The new bytes for the old ones, or { refuse } when the old ones may not be edited.
195
+ function nextBytes(bytes, o, file) {
196
+ let text;
197
+ try {
198
+ text = UTF8.decode(bytes);
199
+ } catch {
200
+ return { refuse: refusal("encoding", `UTF-8 이 아니라 그대로 둠: ${file}`, file) };
201
+ }
202
+ const bom = text.startsWith(BOM) ? BOM : "";
203
+ const plain = text.slice(bom.length);
204
+ const found = findBlock(plain.split(/(?<=\n)/), o.mk);
205
+ if (found?.error) return { refuse: refusal("markers", `표지 손상이라 그대로 둠: ${file}: ${found.error}`, file) };
206
+ return { bytes: Buffer.from(bom + render(plain, found, o), "utf8") };
207
+ }
208
+
209
+ const options = (opts) => ({
210
+ mk: markers(opts.comment, opts.id),
211
+ body: opts.body ?? null,
212
+ wrap: opts.wrap ?? null,
213
+ skill: opts.skill,
214
+ follow: opts.followSymlink === true,
215
+ uid: "uid" in opts ? opts.uid : process.getuid?.(),
216
+ });
217
+
218
+ /**
219
+ * Works out what applyBlock would write, without writing: { ok, changed, refused?, message, path, file, key,
220
+ * before, after, mode }. key names the file (device and inode, or the path of a file not there yet).
221
+ */
222
+ export function planBlock(path, opts) {
223
+ const o = options(opts);
224
+ checkBody(o);
225
+ const at = locate(path, o.follow);
226
+ if (at.refuse) return at.refuse;
227
+ if (!at.exists) {
228
+ const after = o.body === null ? null : Buffer.from(blockText(o.mk, o.body, o.wrap, "\n"), "utf8");
229
+ return { ok: true, changed: after !== null, path, file: at.file, key: `path:${resolve(at.file)}`, before: null, after, mode: NEW_FILE_MODE };
230
+ }
231
+ const read = readChecked(at.file, o.uid);
232
+ if (read.refuse) return read.refuse;
233
+ const next = nextBytes(read.bytes, o, at.file);
234
+ if (next.refuse) return next.refuse;
235
+ const changed = !next.bytes.equals(read.bytes);
236
+ const key = `ino:${read.st.dev}:${read.st.ino}`;
237
+ return { ok: true, changed, path, file: at.file, key, before: read.bytes, after: next.bytes, mode: read.st.mode & 0o7777 };
238
+ }
239
+
240
+ // Writes data to a fresh temp file beside file and renames it over file.
241
+ function writeAtomic(file, data, mode) {
242
+ const tmp = join(dirname(file), `.${basename(file)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`);
243
+ let fd = openSync(tmp, "wx", mode);
244
+ try {
245
+ writeFileSync(fd, data);
246
+ closeSync(fd);
247
+ fd = null;
248
+ if (process.platform !== "win32") chmodSync(tmp, mode);
249
+ renameSync(tmp, file);
250
+ } catch (e) {
251
+ if (fd !== null) closeSync(fd);
252
+ rmSync(tmp, { force: true });
253
+ throw e;
254
+ }
255
+ }
256
+
257
+ // Keeps the first original: writes <file>.<skill>.bak only when nothing is at that name yet.
258
+ function backup(file, skill, bytes) {
259
+ const bak = `${file}.${skill}.bak`;
260
+ if (lstatSync(bak, { throwIfNoEntry: false })) return null;
261
+ const fd = openSync(bak, "wx", BACKUP_MODE);
262
+ try {
263
+ writeFileSync(fd, bytes);
264
+ } finally {
265
+ closeSync(fd);
266
+ }
267
+ return bak;
268
+ }
269
+
270
+ /** Writes what planBlock worked out. A plan that changes nothing or was refused is returned as a result. */
271
+ export function commitBlock(plan, skill) {
272
+ if (!plan.ok) return plan;
273
+ const result = { ok: true, changed: plan.changed, path: plan.path, file: plan.file };
274
+ if (!plan.changed) return { ...result, message: `변경 없음: ${plan.path}` };
275
+ if (plan.before === null && plan.after !== null) {
276
+ const fd = openSync(plan.file, "wx", plan.mode);
277
+ try {
278
+ writeFileSync(fd, plan.after);
279
+ } finally {
280
+ closeSync(fd);
281
+ }
282
+ return { ...result, created: true, message: `새로 만듦: ${plan.path}` };
283
+ }
284
+ const bak = backup(plan.file, skill, plan.before);
285
+ writeAtomic(plan.file, plan.after, plan.mode);
286
+ return { ...result, backup: bak, message: `바꿈: ${plan.path}` };
287
+ }
288
+
289
+ /** Puts in, replaces or (body null) takes out the block in one file. dryRun plans only. */
290
+ export function applyBlock(path, opts) {
291
+ const plan = planBlock(path, opts);
292
+ if (opts.dryRun || !plan.ok) return plan;
293
+ return commitBlock(plan, opts.skill);
294
+ }
295
+
296
+ /**
297
+ * Applies blocks to several files, each entry { path, ...opts }. Every file is planned first: one refusal and
298
+ * none is written. Two entries naming one file (after links are followed) write it once, by the first entry.
299
+ */
300
+ export function applyBlocks(entries, { dryRun = false } = {}) {
301
+ const plans = entries.map((e) => ({ entry: e, plan: planBlock(e.path, e) }));
302
+ const refused = plans.filter((p) => !p.plan.ok).map((p) => p.plan);
303
+ if (refused.length > 0) return { ok: false, refused, results: plans.map((p) => p.plan) };
304
+ const seen = new Map();
305
+ const results = plans.map(({ entry, plan }) => {
306
+ if (seen.has(plan.key)) return { ok: true, changed: false, path: plan.path, file: plan.file, sameAs: seen.get(plan.key), message: `같은 파일이라 한 번만 씀: ${plan.path}` };
307
+ seen.set(plan.key, plan.path);
308
+ return dryRun ? plan : commitBlock(plan, entry.skill);
309
+ });
310
+ return { ok: true, refused: [], results };
311
+ }
@@ -0,0 +1,71 @@
1
+ {
2
+ "psmux": "3.3.8",
3
+ "probed": "2026-11-01, Windows 10 22H2 test box, psmux 3.3.8 (winget marlocarlo.psmux)",
4
+ "rule": "A line goes into the block only when verified is true and default is not true (psmux already has it).",
5
+ "behavior": {
6
+ "default": ["마우스(mouse on)", "set-clipboard on", "휠로 copy-mode 진입(scroll-enter-copy-mode on)"],
7
+ "unverified": [
8
+ "드래그한 글이 Windows 클립보드로 가는지(실제 마우스와 데스크톱이 필요해 미실측)",
9
+ "전체 화면 앱(Claude Code, Codex) 안의 PageUp/PageDown 을 앱이 받는지(미실측)"
10
+ ],
11
+ "broken": [
12
+ "앱 밖(셸) PageUp 은 prefix 뒤에만 copy-mode 진입. 루트 PageUp 바인딩은 넣지 않음",
13
+ "PageDown 이 바닥에서 copy-mode 를 빠지지 않음(-e 미지원, Escape 로 빠짐)",
14
+ "#{alternate_on} 이 늘 0 이라 전체 화면 앱을 구분하지 못함(앱 구분 바인딩 불가)"
15
+ ]
16
+ },
17
+ "options": [
18
+ {
19
+ "id": "mouse",
20
+ "line": "set -g mouse on",
21
+ "verified": true,
22
+ "default": true,
23
+ "note": "psmux 기본값이 on. 블록에 넣지 않음"
24
+ },
25
+ {
26
+ "id": "set-clipboard",
27
+ "line": "set -s set-clipboard on",
28
+ "verified": true,
29
+ "default": true,
30
+ "note": "psmux 기본값이 on. show -s 는 show -g 와 같음"
31
+ },
32
+ {
33
+ "id": "scroll-enter-copy-mode",
34
+ "line": "set -g scroll-enter-copy-mode on",
35
+ "verified": true,
36
+ "default": true,
37
+ "note": "psmux 기본값이 on"
38
+ },
39
+ {
40
+ "id": "page-up",
41
+ "line": "bind -n PPage if -F \"#{||:#{alternate_on},#{pane_in_mode}}\" \"send-keys PPage\" \"copy-mode -e -u\"",
42
+ "verified": false,
43
+ "note": "등록, copy-mode 진입, 스크롤은 됨. 그러나 Windows 10 에서 #{alternate_on} 이 늘 0 이라 전체 화면 앱의 PageUp 을 빼앗음. 바닥에서 빠지는 -e 미지원"
44
+ },
45
+ {
46
+ "id": "page-up-joined-flags",
47
+ "line": "bind -n PPage copy-mode -eu",
48
+ "verified": false,
49
+ "note": "붙인 짧은 플래그를 풀지 못해 -eu 가 사라짐"
50
+ },
51
+ {
52
+ "id": "copy-command",
53
+ "line": "set -s copy-command 'clip.exe'",
54
+ "verified": false,
55
+ "note": "값 저장만 확인. 드래그 복사에 쓰는지 미확인"
56
+ },
57
+ {
58
+ "id": "copy-pipe",
59
+ "line": "bind -T copy-mode MouseDragEnd1Pane send -X copy-pipe-and-cancel 'clip.exe'",
60
+ "verified": false,
61
+ "note": "경고 없이 버려짐"
62
+ },
63
+ {
64
+ "id": "force-drag",
65
+ "line": "bind -n MouseDrag1Pane if -F \"#{pane_in_mode}\" \"send-keys -M\" \"copy-mode -M\"",
66
+ "verified": false,
67
+ "forceDrag": true,
68
+ "note": "경고 없이 버려짐"
69
+ }
70
+ ]
71
+ }