@kaydash9999/banker-plugins 0.19.0 → 0.19.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.
@@ -39,7 +39,7 @@ argument-hint: "on | off | status"
39
39
  | 코드 | 뜻 | 처리 |
40
40
  |---|---|---|
41
41
  | 0 | 완료. 이미 그 상태였던 경우 포함 | 결과 전달 |
42
- | 1 | 거부 또는 사용법 오류. 아무것도 바꾸지 않음 | 메시지 그대로 전달 |
42
+ | 1 | 거부 또는 사용법 오류. 아무것도 바꾸지 않음. 단 메시지에 `앞서 바꾼 파일:` 이 있으면 그 파일은 이미 바뀜 | 메시지 그대로 전달 |
43
43
  | 2 | 예기치 못한 오류 | 메시지 전달 후 `status` 로 확인 |
44
44
 
45
45
  - 출력이 비어 있으면 실행 실패. 성공으로 보고하지 않음
@@ -191,9 +191,12 @@ argument-hint: "on | off | status"
191
191
 
192
192
  ## 문제 해결
193
193
 
194
+ - 아래 표에서 파일을 보인다는 것은 `ls -l` 로 그 이름의 종류와 링크 대상 경로를 보이는 것이다. 링크가 아닌 일반 파일임을 본 뒤에만 내용을 연다. 심어 둔 링크가 가리키는 파일(키 등)이 세션에 오르지 않게 한다.
195
+ - `<파일>.tone-compact.bak` 은 내용을 열지 않고 경로만 알린다. 지침 파일이 링크였으면 가리키던 파일(키 등)의 사본일 수 있다.
196
+
194
197
  | 메시지 | 조치 |
195
198
  |---|---|
196
- | 같은 이름의 다른 파일이 있음 | `rules/banker-tone-compact.md` 가 banker 파일이 아님. 지우거나 이름을 바꾸지 않음. 내용을 보이고 어떻게 할지 물은 뒤 다시 실행 |
199
+ | 같은 이름의 다른 파일이 있음 | `rules/banker-tone-compact.md` 가 banker 파일이 아님. 지우거나 이름을 바꾸지 않음. 내용을 보이고(링크면 `ls -l` 로 링크와 대상 경로만 보이고 대상 내용은 열지 않음) 어떻게 할지 물은 뒤 다시 실행 |
197
200
  | 표시의 짝이 맞지 않음 | 지침 파일에 tone-compact 표시 줄이 한쪽만 있거나 문장 속에 들어 있음. 또는 블록 안에 다른 표시 줄이 있음. 짝을 새로 맞추지 않음. 남은 표시 줄을 보이고 지울지 물은 뒤 다시 실행 |
198
201
  | 설정 폴더가 없음 | 해당 런타임 미설치. 또는 설정 폴더 환경 변수 값 확인 |
199
202
  | 규칙이 이 버전과 다름 | `on` 한 번 실행 |
@@ -202,6 +205,8 @@ argument-hint: "on | off | status"
202
205
  | 다른 banker 블록만 있어 Codex 가 AGENTS.md 를 읽지 않음 | 메시지가 말한 스킬에서 `on` 실행 |
203
206
  | UTF-8 이 아닌 파일 | 지침 파일을 바꾸지 않음. 인코딩을 보이고, 백업을 만든 뒤 UTF-8 로 바꿀지 물은 뒤 다시 실행 |
204
207
  | 끊어진 링크라 그대로 두었습니다 | AGENTS.md 가 가리키는 파일이 없음. 링크를 지우거나 바꾸지 않음. 링크와 대상을 보이고 어떻게 할지 물은 뒤 다시 실행 |
208
+ | AGENTS.md 가 끊어진 링크라 on 이 블록을 옮기지 못함 | AGENTS.override.md 에 블록만 남았고 AGENTS.md 가 가리키는 파일이 없음. 링크를 지우거나 바꾸지 않음. 링크와 대상을 보이고 어떻게 할지 물은 뒤 `on` 실행 |
209
+ | 쓰는 사이에 같은 이름이 생겨 그대로 두었습니다 | 다른 프로그램이 그 사이에 같은 이름을 만들었음(링크일 수 있음). 지우거나 바꾸지 않음. 무엇인지 보이고(링크면 `ls -l` 로 링크와 대상 경로만 보이고 대상 내용은 열지 않음) 어떻게 할지 물은 뒤 다시 실행 |
205
210
  | 일반 파일이 아님 | 지침 파일 자리(AGENTS.md, AGENTS.override.md)의 폴더나 특수 파일은 지우지 않음. 무엇인지 사용자에게 보이고, 옮기거나 이름을 바꿀지 물은 뒤 다시 실행 |
206
211
  | 다른 banker 지침 작업이 끝나지 않아 그대로 두었습니다 | 다른 banker 스킬의 `on`, `off` 가 도는 중. 잠시 뒤 다시 실행. 끝나지 않은 작업이 남긴 잠금은 1분 뒤 저절로 넘어감. 1분이 지나도 같거나 잠금 자리가 폴더면 잠금 경로를 보이고 어떻게 할지 물은 뒤 다시 실행. 잠금을 임의로 지우지 않음 |
207
212
  | 쓸 권한이 없음 | 해당 파일과 폴더 권한 확인 |
@@ -42,7 +42,9 @@
42
42
  * the Codex folder), so both skills turned on at once keep both blocks; a lock
43
43
  * older than a minute is taken over.
44
44
  *
45
- * Results are { ok, message }; ok false means refused with nothing changed.
45
+ * Results are { ok, message }; ok false means refused with nothing changed, or,
46
+ * when a change of two files was refused at the second, with the first changed
47
+ * and named in the message ("앞서 바꾼 파일").
46
48
  * Anything else that goes wrong throws.
47
49
  */
48
50
  import {
@@ -95,7 +97,7 @@ const isLink = (path) => {
95
97
 
96
98
  const UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
97
99
 
98
- // What sits at a name that is not a regular file: a folder, a FIFO or a device.
100
+ // What sits at a name that is not a regular file: a folder, a FIFO, a socket, a device or a link loop.
99
101
  // It is never read; reading a FIFO waits for a writer.
100
102
  const SPECIAL = Object.freeze({ bytes: null, text: null, special: true });
101
103
  // What sits at a name that is a link to nothing: Codex finds no file there, and writing would follow the link.
@@ -148,7 +150,7 @@ function readFile(path) {
148
150
 
149
151
  /** A result that did, or found already done, what was asked. */
150
152
  export const done = (message) => ({ ok: true, message });
151
- /** A result that refused and changed nothing. */
153
+ /** A result that refused and changed nothing, unless its message names a file changed before (applied). */
152
154
  export const refuse = (message) => ({ ok: false, message });
153
155
 
154
156
  /** A skill's marks: the first line of its rules file, and the lines around its Codex block. */
@@ -226,6 +228,7 @@ function claudeOn(t, o, getRules) {
226
228
  const state = claudeState(t, readFile(file), rules);
227
229
  if (state === "foreign") return refuse(`같은 이름의 다른 파일이 있어 그대로 두었습니다: ${shown}`);
228
230
  if (state === "on") return done(`Claude Code: 이미 켜져 있습니다 (변경 없음). ${shown}`);
231
+ const wasLink = isLink(file);
229
232
  try {
230
233
  mkdirSync(dirname(file), { recursive: true, mode: 0o755 });
231
234
  writeFresh(file, claudeText(t, rules));
@@ -233,13 +236,13 @@ function claudeOn(t, o, getRules) {
233
236
  if (denied(e)) return refuse(`쓸 권한이 없어 켜지 못했습니다 (${e.code}): ${shown}`);
234
237
  throw e;
235
238
  }
236
- return done(
237
- state === "stale"
238
- ? `Claude Code: ${t.updatedText}. ${shown}`
239
- : `Claude Code: 켰습니다. 다음 세션부터 자동 적용됩니다. ${shown}`,
240
- );
239
+ const words = state === "stale" ? `Claude Code: ${t.updatedText}. ${shown}` : `Claude Code: 켰습니다. 다음 세션부터 자동 적용됩니다. ${shown}`;
240
+ return done(wasLink ? `${words}\n${LINK_REPLACED}` : words);
241
241
  }
242
242
 
243
+ /** The note for a name that was a link (to nothing, or to an older copy) and is now a regular file. */
244
+ export const LINK_REPLACED = "참고: 링크였던 자리를 일반 파일로 바꿨습니다. 링크가 가리키던 곳은 건드리지 않았습니다.";
245
+
243
246
  function claudeOff(t, o) {
244
247
  const file = claudeFile(t, o);
245
248
  const state = claudeState(t, readFile(file));
@@ -435,10 +438,14 @@ function withBlock(t, text, rules) {
435
438
  }
436
439
 
437
440
  // The state when the block is not in its place: "hidden" (only in an override with no text of its own),
438
- // "misplaced" (only in AGENTS.md while Codex reads the override), "dangling" (nowhere, and AGENTS.md is a link to
439
- // nothing that on must not write through) or "off".
441
+ // "hiddenbroken" (the same, with AGENTS.md a link to nothing, so on cannot move it), "misplaced" (only in AGENTS.md
442
+ // while Codex reads the override), "dangling" (nowhere, and AGENTS.md is a link to nothing that on must not write
443
+ // through) or "off".
440
444
  function placeState(files) {
441
- if (hasBlock(files.other)) return files.other === files.all[0] ? "hidden" : "misplaced";
445
+ if (hasBlock(files.other)) {
446
+ if (files.other !== files.all[0]) return "misplaced";
447
+ return files.target.dangling ? "hiddenbroken" : "hidden";
448
+ }
442
449
  return files.target.dangling ? "dangling" : "off";
443
450
  }
444
451
 
@@ -477,12 +484,32 @@ function writeBlocked(changes) {
477
484
  // whatever the umask allows more. A file left empty is removed, unless it is a
478
485
  // link or has other names (a hard link), whose other names would keep the old
479
486
  // text: it is emptied instead. Codex skips an empty file anyway.
487
+ // A file that was not there when it was read is created only if it still is not ("wx"): a name that appeared in
488
+ // between, a link planted there among them, is not written through.
480
489
  function apply(t, changes) {
481
490
  for (const { file, after } of changes) {
482
491
  if (file.bytes !== null) writeFresh(`${file.path}.${t.slug}.bak`, file.bytes, 0o600);
483
492
  if (after === "" && !isLink(file.path) && !hasOtherNames(file.path)) rmSync(file.path, { force: true });
484
- else writeFileSync(file.path, after, { mode: 0o644 });
493
+ else writeFileSync(file.path, after, { mode: 0o644, flag: file.bytes === null ? "wx" : "w" });
494
+ }
495
+ }
496
+
497
+ // Applies the changes, or the refusal for a write that failed with nothing of the file changed: a name that
498
+ // appeared in between (EEXIST), or a write a sandbox refused after access() allowed it. The refusal names the place
499
+ // the system refused (a rename's target, as the backup, or the file it was about), and a file changed before the
500
+ // failing one. Other errors propagate.
501
+ function applied(t, changes) {
502
+ let i = 0;
503
+ try {
504
+ for (; i < changes.length; i++) apply(t, [changes[i]]);
505
+ } catch (e) {
506
+ if (e.code !== "EEXIST" && !denied(e)) throw e;
507
+ const why = e.code === "EEXIST" ? "쓰는 사이에 같은 이름이 생겨 그대로 두었습니다" : `쓸 권한이 없어 그대로 두었습니다 (${where(e.code)})`;
508
+ const at = [e.dest, e.path].find((p) => typeof p === "string") ?? changes[i].file.path;
509
+ const before = i === 0 ? "" : `\n앞서 바꾼 파일: ${changes.slice(0, i).map((c) => where(c.file.path)).join(", ")}`;
510
+ return refuse(`${why}: ${where(at)}${before}`);
485
511
  }
512
+ return null;
486
513
  }
487
514
 
488
515
  // Whether the file at path is also reachable under another name: a hard link.
@@ -523,6 +550,7 @@ function dropStale(path) {
523
550
  st = lstatSync(path);
524
551
  } catch (e) {
525
552
  if (e.code === "ENOENT") return true; /* gone in between */
553
+ if (e.code === "ESTALE") return false; /* a network share's stale handle: rest and look again, not spin */
526
554
  throw e;
527
555
  }
528
556
  if (st.isDirectory() || Math.abs(Date.now() - st.mtimeMs) <= LOCK_STALE_MS) return false;
@@ -628,7 +656,8 @@ function codexOnHeld(t, o, getRules) {
628
656
  if (hasBlock(files.other)) changes.push({ file: files.other, after: stripped(files.other) });
629
657
  const why = writeBlocked(changes);
630
658
  if (why) return refuse(why);
631
- apply(t, changes);
659
+ const failed = applied(t, changes);
660
+ if (failed) return failed;
632
661
  const note = files.target === files.all[1] ? `\n${OMX_NOTE}` : "";
633
662
  return done(`Codex: ${words}. ${tail}${note}`);
634
663
  }
@@ -646,7 +675,8 @@ function codexOffHeld(t, o) {
646
675
  if (changes.length === 0) return done("Codex: 이미 꺼져 있습니다 (변경 없음).");
647
676
  const why = writeBlocked(changes);
648
677
  if (why) return refuse(why);
649
- apply(t, changes);
678
+ const failed = applied(t, changes);
679
+ if (failed) return failed;
650
680
  const paths = changes.map((c) => where(c.file.path)).join(", ");
651
681
  const baks = changes.map((c) => where(`${c.file.path}.${t.slug}.bak`)).join(", ");
652
682
  return done(`Codex: 껐습니다. 다음 세션부터 적용되지 않습니다. ${paths}\n지우기 전 파일: ${baks}`);
@@ -661,6 +691,7 @@ const STATE_TEXT = {
661
691
  foreign: "꺼짐, 같은 이름의 다른 파일이 있음 (손대지 않음)",
662
692
  misplaced: "꺼짐, 단 블록이 Codex 가 읽지 않는 AGENTS.md 에 있음 (on 으로 옮김)",
663
693
  hidden: "켜짐, 단 AGENTS.override.md 에 블록만 있어 AGENTS.md 가 가려짐 (on 으로 옮김)",
694
+ hiddenbroken: "켜짐, 단 AGENTS.override.md 에 블록만 있고 AGENTS.md 가 끊어진 링크라 on 이 블록을 옮기지 못함 (링크 확인 필요)",
664
695
  notutf8: "UTF-8 이 아닌 지침 파일이 있어 손대지 않음 (on, off 모두 거부)",
665
696
  notregular: "일반 파일이 아닌 지침 파일이 있어 손대지 않음 (on, off 모두 거부)",
666
697
  unpaired: "표시의 짝이 맞지 않음 (on, off 모두 거부. 남은 표시 줄 정리 필요)",
@@ -15,7 +15,8 @@
15
15
  *
16
16
  * `off` never needs the rules, so it works even when SKILL.md cannot be read.
17
17
  * Exit status 0 means done or already so; 1 means refused or a usage error, with
18
- * nothing changed; 2 means an unexpected error stopped the command partway, so
18
+ * nothing changed (a refusal at the second of two files names the first, which
19
+ * it changed); 2 means an unexpected error stopped the command partway, so
19
20
  * check `status`.
20
21
  */
21
22
  import { readFileSync, realpathSync } from "node:fs";