@kaydash9999/banker-plugins 0.18.0 → 0.19.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.
@@ -9,8 +9,13 @@
9
9
  * node anti-drm.mjs off --runtime claude|codex
10
10
  * node anti-drm.mjs status
11
11
  * node anti-drm.mjs config --deny <list> --alt <list> [--tsv tab|comma]
12
+ * node anti-drm.mjs on --runtime claude|codex --stdin
13
+ * node anti-drm.mjs config --stdin
12
14
  *
13
15
  * <list> is extensions split by commas or spaces, such as ".txt,.pdf".
16
+ * --stdin reads the settings from standard input instead, one line each:
17
+ * deny=<list>, alt=<list> and tsv=tab|comma. The skill passes them in a
18
+ * quoted heredoc, so no value the user chose is ever read by a shell.
14
19
  *
15
20
  * The settings are one JSON file both runtimes share, ~/.config/banker/anti-drm.json
16
21
  * ($BANKER_ANTI_DRM_CONFIG names another): the denied extensions, the
@@ -40,6 +45,7 @@ import {
40
45
  mkdirSync,
41
46
  openSync,
42
47
  readFileSync,
48
+ readSync,
43
49
  realpathSync,
44
50
  statSync,
45
51
  } from "node:fs";
@@ -50,9 +56,14 @@ import {
50
56
  KEEP_CLOSE,
51
57
  KEEP_OPEN,
52
58
  RUNTIMES,
59
+ count,
53
60
  denied,
61
+ done,
54
62
  hasMarker,
63
+ lf,
64
+ noBom,
55
65
  present,
66
+ refuse,
56
67
  stateLines,
57
68
  toggleSpec,
58
69
  turnOff as toggleOff,
@@ -80,12 +91,7 @@ const DEFAULTS = {
80
91
  skillFile: fileURLToPath(new URL("../SKILL.md", import.meta.url)),
81
92
  };
82
93
 
83
- const BOM = String.fromCharCode(0xfeff);
84
94
  const RUNTIME_NAME = { claude: "Claude Code", codex: "Codex" };
85
- const done = (message) => ({ ok: true, message });
86
- const refuse = (message) => ({ ok: false, message });
87
- const count = (text, needle) => text.split(needle).length - 1;
88
- const lf = (text) => text.replace(/\r\n/g, "\n");
89
95
  // A value from outside, as a message shows it. An extension is printable
90
96
  // ASCII, so anything else becomes "?" (a line break or a direction mark cannot
91
97
  // forge a line or reorder the text), at most 40 characters stay, and a value
@@ -136,10 +142,12 @@ export function checkConfig({ deny, alt, tsvDelimiter = "tab" }) {
136
142
  if (reserved.length > 0) return { error: `anti-drm 이 쓰는 형식이라 금지할 수 없습니다: ${reserved.join(" ")}` };
137
143
  const both = d.list.filter((x) => a.list.includes(x));
138
144
  if (both.length > 0) return { error: `금지 확장자와 대체 확장자에 함께 있습니다: ${both.join(" ")}` };
139
- if (tsvDelimiter !== "tab" && tsvDelimiter !== "comma") {
145
+ // In any letter case, as the --tsv flag takes it; kept lower-cased.
146
+ const tsv = typeof tsvDelimiter === "string" ? tsvDelimiter.toLowerCase() : tsvDelimiter;
147
+ if (tsv !== "tab" && tsv !== "comma") {
140
148
  return { error: `TSV 구분자는 tab 이나 comma 여야 합니다: ${shown(tsvDelimiter)}` };
141
149
  }
142
- return { config: { version: 1, deny: d.list, alt: a.list, tsvDelimiter } };
150
+ return { config: { version: 1, deny: d.list, alt: a.list, tsvDelimiter: tsv } };
143
151
  }
144
152
 
145
153
  /**
@@ -188,7 +196,7 @@ export function readConfig(file) {
188
196
  const { text } = got;
189
197
  let raw;
190
198
  try {
191
- raw = JSON.parse(text.startsWith(BOM) ? text.slice(BOM.length) : text);
199
+ raw = JSON.parse(noBom(text));
192
200
  } catch {
193
201
  return { error: "JSON 이 아닙니다" };
194
202
  }
@@ -244,7 +252,7 @@ const OWN_KEYS = ["version", "deny", "alt", "tsvDelimiter"];
244
252
  function ownSettings(bytes) {
245
253
  try {
246
254
  const text = bytes.toString("utf8");
247
- const raw = JSON.parse(text.startsWith(BOM) ? text.slice(BOM.length) : text);
255
+ const raw = JSON.parse(noBom(text));
248
256
  const plain = Boolean(raw) && typeof raw === "object" && !Array.isArray(raw);
249
257
  return plain && Object.keys(raw).every((k) => OWN_KEYS.includes(k)) && Boolean(checkConfig(raw).config);
250
258
  } catch {
@@ -337,13 +345,16 @@ const code = (ext) => `\`${ext}\``;
337
345
  export function renderRules(template, config) {
338
346
  const map = table(["금지 확장자", "대신 쓸 확장자"], config.deny.map((d) => [code(d), code(replacementFor(d, config.alt))]));
339
347
  const alts = table(["확장자", "쓰는 곳"], config.alt.map((a) => [code(a), useOf(a, config)]));
340
- return template.replace("{{map}}", () => map).replace("{{alts}}", () => alts);
348
+ const rules = template.replace("{{map}}", () => map).replace("{{alts}}", () => alts);
349
+ if (rules.includes("{{")) throw new Error("규칙을 채운 뒤에도 자리 표시({{)가 남았습니다");
350
+ return rules;
341
351
  }
342
352
 
343
353
  // ---- commands ----
344
354
 
345
355
  const USAGE =
346
- "사용법: anti-drm on | off | status | config [--runtime claude|codex] [--deny <확장자 목록> --alt <확장자 목록> [--tsv tab|comma]]";
356
+ "사용법: anti-drm on | off | status | config [--runtime claude|codex] [--stdin | --deny <확장자 목록> --alt <확장자 목록> [--tsv tab|comma]]\n" +
357
+ "--stdin 은 표준 입력이 끝날 때까지 기다립니다.";
347
358
 
348
359
  // The settings as `status` shows them.
349
360
  function configLines(o, found) {
@@ -416,8 +427,7 @@ function refresh(o, rules, skip) {
416
427
  // The settings from --deny, --alt and --tsv: { config }, { error }, or { usage } when a list is missing.
417
428
  function configFromFlags(flags) {
418
429
  if (flags.deny === undefined || flags.alt === undefined) return { usage: true };
419
- const tsvDelimiter = (flags.tsv ?? "tab").toLowerCase();
420
- return checkConfig({ deny: splitList(flags.deny), alt: splitList(flags.alt), tsvDelimiter });
430
+ return checkConfig({ deny: splitList(flags.deny), alt: splitList(flags.alt), tsvDelimiter: flags.tsv ?? "tab" });
421
431
  }
422
432
 
423
433
  const settingsGiven = (flags) => ["deny", "alt", "tsv"].some((k) => flags[k] !== undefined);
@@ -438,7 +448,7 @@ function applySettings(o, flags, skip) {
438
448
  // The rules from the saved settings: { rules, lines, ok } or { refusal }.
439
449
  function savedRules(o) {
440
450
  const found = readConfig(o.configFile);
441
- if (found === null) return { refusal: refuse("설정이 없습니다. 금지 확장자와 대체 확장자를 --deny, --alt 로 함께 주세요.") };
451
+ if (found === null) return { refusal: refuse("설정이 없습니다. 금지 확장자와 대체 확장자를 --stdin 이나 --deny, --alt 로 함께 주세요.") };
442
452
  if (found.error) {
443
453
  return { refusal: refuse(`설정 파일이 올바르지 않습니다 (${found.error}): ${where(o.configFile)}. config 로 다시 만드세요.`) };
444
454
  }
@@ -481,17 +491,89 @@ export const configure = (opts = {}, flags = {}) => cmdConfig({ ...DEFAULTS, ...
481
491
  const FLAG_NAMES = ["runtime", "deny", "alt", "tsv"];
482
492
  const COMMANDS = ["on", "off", "status", "config"];
483
493
 
484
- /** { command, runtime, flags, extra } from the command line; each flag takes "x" or "=x". */
494
+ /**
495
+ * { command, runtime, flags, extra, stdin } from the command line; each value
496
+ * flag takes "x" or "=x", and --stdin takes nothing.
497
+ */
485
498
  export function parseArgs(argv) {
486
499
  const rest = [];
487
500
  const flags = {};
501
+ let stdin = false;
488
502
  for (let i = 0; i < argv.length; i++) {
489
503
  const m = /^--([a-z]+)(?:=(.*))?$/.exec(argv[i]);
490
504
  if (m && FLAG_NAMES.includes(m[1])) flags[m[1]] = m[2] ?? argv[++i] ?? "";
505
+ else if (argv[i] === "--stdin") stdin = true;
491
506
  else rest.push(argv[i]);
492
507
  }
493
508
  const { runtime, ...settings } = flags;
494
- return { command: (rest[0] ?? "status").toLowerCase(), runtime: runtime?.toLowerCase() ?? null, flags: settings, extra: rest.slice(1) };
509
+ const command = (rest[0] ?? "status").toLowerCase();
510
+ return { command, runtime: runtime?.toLowerCase() ?? null, flags: settings, extra: rest.slice(1), stdin };
511
+ }
512
+
513
+ // Lines on standard input that hold the settings: the key, "=", the value.
514
+ const STDIN_LINE = /^(deny|alt|tsv)=(.*)$/;
515
+ const UTF8_STRICT = new TextDecoder("utf-8", { fatal: true });
516
+
517
+ /**
518
+ * The settings on standard input as flags: { flags } or { error }. At most
519
+ * 64 KiB of UTF-8; a leading byte order mark and CRLF line ends are fine;
520
+ * each of deny=, alt= and tsv= once; blank lines are skipped.
521
+ */
522
+ export function stdinSettings(bytes) {
523
+ if (bytes.length > MAX_BYTES) return { error: "표준 입력이 64KiB 보다 큽니다" };
524
+ let text;
525
+ try {
526
+ text = UTF8_STRICT.decode(bytes);
527
+ } catch {
528
+ return { error: "표준 입력이 UTF-8 이 아닙니다" };
529
+ }
530
+ const flags = {};
531
+ for (const line of noBom(text).split(/\r?\n/)) {
532
+ if (line.trim() === "") continue;
533
+ const m = STDIN_LINE.exec(line);
534
+ if (!m || flags[m[1]] !== undefined) return { error: `표준 입력의 설정 줄을 읽을 수 없습니다: ${shown(line)}` };
535
+ flags[m[1]] = m[2].trim();
536
+ }
537
+ return { flags };
538
+ }
539
+
540
+ const PAUSE = new Int32Array(new SharedArrayBuffer(4));
541
+
542
+ // Standard input, read up to cap bytes and one more: { bytes } at the end of input, or { tooBig: true } as soon as
543
+ // more than cap came. A pipe left non-blocking answers EAGAIN until data comes: wait a little and read again.
544
+ function readCapped(fd, cap) {
545
+ const chunks = [];
546
+ const buf = Buffer.alloc(64 * 1024);
547
+ let total = 0;
548
+ for (;;) {
549
+ const n = readSome(fd, buf);
550
+ if (n === 0) return { bytes: Buffer.concat(chunks) };
551
+ total += n;
552
+ if (total > cap) return { tooBig: true };
553
+ chunks.push(Buffer.from(buf.subarray(0, n)));
554
+ }
555
+ }
556
+
557
+ // One read into buf: how many bytes came, 0 at the end of input (Windows may say EOF instead).
558
+ function readSome(fd, buf) {
559
+ for (;;) {
560
+ try {
561
+ return readSync(fd, buf, 0, buf.length, null);
562
+ } catch (e) {
563
+ if (e.code === "EOF") return 0;
564
+ if (e.code !== "EAGAIN") throw e;
565
+ Atomics.wait(PAUSE, 0, 0, 10);
566
+ }
567
+ }
568
+ }
569
+
570
+ // The settings standard input held: an error when it was too big or unreadable, or held no settings line, which
571
+ // on and config both refuse rather than reach for the saved settings.
572
+ function stdinFrom(got) {
573
+ if (got.tooBig) return { error: "표준 입력이 64KiB 보다 큽니다" };
574
+ const read = stdinSettings(got.bytes);
575
+ if (read.error || Object.keys(read.flags).length > 0) return read;
576
+ return { error: "표준 입력에 설정 줄이 없습니다" };
495
577
  }
496
578
 
497
579
  // Settings flags go only with on and config, and config needs both lists.
@@ -501,9 +583,11 @@ function settingsUsage(command, flags) {
501
583
  }
502
584
 
503
585
  // Why the command line cannot run as given, or "" when it can.
504
- function usageError({ command, runtime, flags, extra }) {
586
+ function usageError({ command, runtime, flags, extra, stdin }) {
505
587
  if (extra.length > 0 || !COMMANDS.includes(command)) return USAGE;
506
588
  if (runtime !== null && !RUNTIMES.includes(runtime)) return USAGE;
589
+ // --stdin brings all the settings, so it goes with on and config only and never with a settings flag.
590
+ if (stdin) return (command === "on" || command === "config") && Object.keys(flags).length === 0 ? "" : USAGE;
507
591
  return settingsUsage(command, flags);
508
592
  }
509
593
 
@@ -517,25 +601,32 @@ function run(command, runtime, flags) {
517
601
  return command === "on" ? cmdOn(o, runtime, flags) : cmdOff(o, runtime);
518
602
  }
519
603
 
520
- function main(argv, env = process.env) {
604
+ // Writes a message line and gives back the exit status.
605
+ const say = (text, code) => {
606
+ process.stdout.write(`${text}\n`);
607
+ return code;
608
+ };
609
+
610
+ // Whether a command acts on one runtime and so needs to know which.
611
+ const needsRuntime = (command) => command === "on" || command === "off";
612
+ // The settings flags: from standard input with --stdin, else from the command line.
613
+ const settingsOf = (args, input) => (args.stdin ? stdinFrom(input()) : { flags: args.flags });
614
+
615
+ function main(argv, env = process.env, input = () => readCapped(0, MAX_BYTES)) {
521
616
  const args = parseArgs(argv);
522
617
  const why = usageError(args);
523
- if (why) {
524
- process.stdout.write(`${why}\n`);
525
- return 1;
526
- }
618
+ if (why) return say(why, 1);
527
619
  const runtime = runtimeOf(args.runtime, env);
528
- if ((args.command === "on" || args.command === "off") && !RUNTIMES.includes(runtime)) {
529
- process.stdout.write(`런타임을 정할 수 없습니다. --runtime claude 또는 --runtime codex 를 붙이세요.\n${USAGE}\n`);
530
- return 1;
620
+ if (needsRuntime(args.command) && !RUNTIMES.includes(runtime)) {
621
+ return say(`런타임을 정할 수 없습니다. --runtime claude 또는 --runtime codex 를 붙이세요.\n${USAGE}`, 1);
531
622
  }
532
623
  try {
533
- const { ok, message } = run(args.command, runtime, args.flags);
534
- process.stdout.write(`${message}\n`);
535
- return ok ? 0 : 1;
624
+ const got = settingsOf(args, input);
625
+ if (got.error) return say(`${got.error}\n${USAGE}`, 1);
626
+ const { ok, message } = run(args.command, runtime, got.flags);
627
+ return say(message, ok ? 0 : 1);
536
628
  } catch (e) {
537
- process.stdout.write(`예기치 못한 오류로 중단했습니다: ${where(e.message)}\nstatus 로 상태를 확인하세요.\n`);
538
- return 2;
629
+ return say(`예기치 못한 오류로 중단했습니다: ${where(e.message)}\nstatus 로 상태를 확인하세요.`, 2);
539
630
  }
540
631
  }
541
632