@kaydash9999/banker-plugins 0.18.0 → 0.19.1
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 +103 -0
- package/README.md +18 -7
- package/bin/lib/telemetry-config.mjs +25 -0
- package/codex/manifest.json +1 -0
- package/codex/transform-matrix.md +1 -0
- package/hooks/graceful-pause.mjs +4 -2
- package/hooks/obsidize-hook.mjs +61 -32
- package/hooks/progress-plan.mjs +14 -8
- package/hooks/progress.mjs +187 -48
- package/hooks/register.mjs +15 -7
- package/hooks/telemetry-count-skill.mjs +4 -6
- package/hooks/telemetry-count.mjs +4 -6
- package/hooks/update-checkin.mjs +46 -43
- package/hooks/update-fetch.mjs +8 -7
- package/package.json +1 -1
- package/skills/anti-drm/SKILL.md +46 -15
- package/skills/anti-drm/scripts/anti-drm.mjs +213 -61
- package/skills/anti-drm/scripts/instruction-toggle.mjs +280 -44
- package/skills/lineage/SKILL.md +4 -1
- package/skills/lineage/lineage.py +196 -73
- package/skills/omc-patch/SKILL.md +37 -11
- package/skills/omc-patch/scripts/omc-patch.mjs +635 -103
- package/skills/theme-selection/SKILL.md +165 -0
- package/skills/theme-selection/assets/LICENSE +21 -0
- package/skills/theme-selection/assets/NOTICE.md +30 -0
- package/skills/theme-selection/assets/palettes.json +81 -0
- package/skills/theme-selection/references/contrast.md +138 -0
- package/skills/theme-selection/references/integrate.md +161 -0
- package/skills/theme-selection/scripts/themes.mjs +1067 -0
- package/skills/tone-compact/SKILL.md +14 -5
- package/skills/tone-compact/scripts/instruction-toggle.mjs +280 -44
- package/skills/tone-compact/scripts/tone-compact.mjs +4 -4
- package/skills/update-banker/SKILL.md +1 -1
|
@@ -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,8 +45,8 @@ import {
|
|
|
40
45
|
mkdirSync,
|
|
41
46
|
openSync,
|
|
42
47
|
readFileSync,
|
|
48
|
+
readSync,
|
|
43
49
|
realpathSync,
|
|
44
|
-
statSync,
|
|
45
50
|
} from "node:fs";
|
|
46
51
|
import { homedir } from "node:os";
|
|
47
52
|
import { dirname, join } from "node:path";
|
|
@@ -50,9 +55,15 @@ import {
|
|
|
50
55
|
KEEP_CLOSE,
|
|
51
56
|
KEEP_OPEN,
|
|
52
57
|
RUNTIMES,
|
|
58
|
+
count,
|
|
53
59
|
denied,
|
|
60
|
+
done,
|
|
54
61
|
hasMarker,
|
|
62
|
+
lf,
|
|
63
|
+
LINK_REPLACED,
|
|
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
|
-
|
|
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(
|
|
199
|
+
raw = JSON.parse(noBom(text));
|
|
192
200
|
} catch {
|
|
193
201
|
return { error: "JSON 이 아닙니다" };
|
|
194
202
|
}
|
|
@@ -196,9 +204,9 @@ export function readConfig(file) {
|
|
|
196
204
|
return checkConfig({ deny: raw.deny, alt: raw.alt, tsvDelimiter: raw.tsvDelimiter ?? "tab" });
|
|
197
205
|
}
|
|
198
206
|
|
|
199
|
-
// What sits at the settings path: "none", "file" (a regular file,
|
|
200
|
-
//
|
|
201
|
-
//
|
|
207
|
+
// What sits at the settings path: "none", "file" (a regular file with this one name), "link" (a link, wherever it
|
|
208
|
+
// leads), "hardlink" (a regular file with another name too) or "other" (a folder, a FIFO). The save replaces a link
|
|
209
|
+
// or a hard link by this name only: what it led to stays, so nothing of it is read or kept.
|
|
202
210
|
function pathKind(file) {
|
|
203
211
|
let st;
|
|
204
212
|
try {
|
|
@@ -207,31 +215,64 @@ function pathKind(file) {
|
|
|
207
215
|
if (e.code === "ENOENT" || e.code === "ENOTDIR") return "none";
|
|
208
216
|
throw e;
|
|
209
217
|
}
|
|
210
|
-
if (st.
|
|
211
|
-
if (!st.
|
|
218
|
+
if (st.isSymbolicLink()) return "link";
|
|
219
|
+
if (!st.isFile()) return "other";
|
|
220
|
+
return st.nlink > 1 ? "hardlink" : "file";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Opened without blocking. O_NOFOLLOW, where the system has it, keeps the open from following a link put at the name
|
|
224
|
+
// after the first look; Windows has none, so there the name is looked at again after the open.
|
|
225
|
+
const PRIOR_OPEN = constants.O_RDONLY | (constants.O_NONBLOCK ?? 0);
|
|
226
|
+
const NO_FOLLOW = constants.O_NOFOLLOW ?? 0;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* What the settings name holds after the open, against the opened file, from their bigint stats: "same", "link" (a
|
|
230
|
+
* link now: the open followed it), "hardlink" (the opened file has another name now) or "changed" (gone, not a regular
|
|
231
|
+
* file, or another file: the file IDs differ; an ID of 0, which some file systems give, proves nothing).
|
|
232
|
+
*/
|
|
233
|
+
export function nameNow(opened, now) {
|
|
234
|
+
if (now?.isSymbolicLink()) return "link";
|
|
235
|
+
if (!now?.isFile()) return "changed";
|
|
236
|
+
if (opened.ino !== 0n && (now.ino !== opened.ino || now.dev !== opened.dev)) return "changed";
|
|
237
|
+
return now.nlink > 1n ? "hardlink" : "same";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// The name's own stats in bigint, or null when it cannot be looked at.
|
|
241
|
+
function lstatOrNull(file) {
|
|
212
242
|
try {
|
|
213
|
-
return
|
|
243
|
+
return lstatSync(file, { bigint: true });
|
|
214
244
|
} catch {
|
|
215
|
-
return
|
|
245
|
+
return null;
|
|
216
246
|
}
|
|
217
247
|
}
|
|
218
248
|
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
249
|
+
// What the file opened at the settings path gives, as priorBytes says. Without O_NOFOLLOW the name must still hold it.
|
|
250
|
+
function openedBytes(file, fd, noFollow) {
|
|
251
|
+
const st = fstatSync(fd);
|
|
252
|
+
if (!st.isFile() || st.nlink > 1) return { bytes: null };
|
|
253
|
+
const now = noFollow ? "same" : nameNow(fstatSync(fd, { bigint: true }), lstatOrNull(file));
|
|
254
|
+
if (now === "changed") return { refusal: "확인하는 사이에 바뀐 자리라 설정을 저장하지 않았습니다" };
|
|
255
|
+
if (now !== "same") return { bytes: null };
|
|
256
|
+
if (st.size > MAX_BYTES) return { refusal: "64KiB 보다 큰 파일이라 설정을 저장하지 않았습니다" };
|
|
257
|
+
return { bytes: readFileSync(fd) };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The bytes of the regular file with this one name at the settings path: { bytes } (null when it turned out to be
|
|
262
|
+
* something else: gone, a link, not a regular file, or a file with another name too, which the save loses nothing
|
|
263
|
+
* of) or { refusal }. noFollow is O_NOFOLLOW where the system has it; 0 takes the Windows way.
|
|
264
|
+
*/
|
|
265
|
+
export function priorBytes(file, noFollow = NO_FOLLOW) {
|
|
222
266
|
let fd;
|
|
223
267
|
try {
|
|
224
|
-
fd = openSync(file,
|
|
268
|
+
fd = openSync(file, PRIOR_OPEN | noFollow);
|
|
225
269
|
} catch (e) {
|
|
226
|
-
if (e.code === "ENOENT" || e.code === "ENOTDIR") return { bytes: null };
|
|
270
|
+
if (e.code === "ENOENT" || e.code === "ENOTDIR" || e.code === "ELOOP") return { bytes: null };
|
|
227
271
|
if (denied(e)) return { refusal: "읽을 수 없는 파일이라 설정을 저장하지 않았습니다" };
|
|
228
272
|
throw e;
|
|
229
273
|
}
|
|
230
274
|
try {
|
|
231
|
-
|
|
232
|
-
if (!st.isFile()) return { bytes: null };
|
|
233
|
-
if (st.size > MAX_BYTES) return { refusal: "64KiB 보다 큰 파일이라 설정을 저장하지 않았습니다" };
|
|
234
|
-
return { bytes: readFileSync(fd) };
|
|
275
|
+
return openedBytes(file, fd, noFollow);
|
|
235
276
|
} finally {
|
|
236
277
|
closeSync(fd);
|
|
237
278
|
}
|
|
@@ -244,7 +285,7 @@ const OWN_KEYS = ["version", "deny", "alt", "tsvDelimiter"];
|
|
|
244
285
|
function ownSettings(bytes) {
|
|
245
286
|
try {
|
|
246
287
|
const text = bytes.toString("utf8");
|
|
247
|
-
const raw = JSON.parse(
|
|
288
|
+
const raw = JSON.parse(noBom(text));
|
|
248
289
|
const plain = Boolean(raw) && typeof raw === "object" && !Array.isArray(raw);
|
|
249
290
|
return plain && Object.keys(raw).every((k) => OWN_KEYS.includes(k)) && Boolean(checkConfig(raw).config);
|
|
250
291
|
} catch {
|
|
@@ -252,22 +293,27 @@ function ownSettings(bytes) {
|
|
|
252
293
|
}
|
|
253
294
|
}
|
|
254
295
|
|
|
255
|
-
//
|
|
256
|
-
|
|
296
|
+
// Keeps a file anti-drm did not write at bak, once, and says what it did: "kept", "earlier" (a regular file is there
|
|
297
|
+
// already and stays) or "other" (something else is there: a link, a dangling one too, or a folder; nothing is written).
|
|
298
|
+
// The note comes from the same look as the write.
|
|
299
|
+
function keepOnce(bak, bytes) {
|
|
300
|
+
let st;
|
|
257
301
|
try {
|
|
258
|
-
lstatSync(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
302
|
+
st = lstatSync(bak);
|
|
303
|
+
} catch (e) {
|
|
304
|
+
if (e.code !== "ENOENT") throw e;
|
|
305
|
+
writeFresh(bak, bytes, 0o600);
|
|
306
|
+
return "kept";
|
|
262
307
|
}
|
|
308
|
+
return st.isFile() ? "earlier" : "other";
|
|
263
309
|
}
|
|
264
310
|
|
|
265
|
-
// What a save finds at the settings path: { keep } (the bytes to put in .bak,
|
|
266
|
-
//
|
|
311
|
+
// What a save finds at the settings path: { keep } (the bytes to put in .bak, null for none: nothing there, a link
|
|
312
|
+
// or a hard link, whose file stays, or anti-drm's own settings) or { refusal }.
|
|
267
313
|
function priorSettings(file) {
|
|
268
314
|
const kind = pathKind(file);
|
|
269
|
-
if (kind === "none") return { keep: null };
|
|
270
315
|
if (kind === "other") return { refusal: "일반 파일이 아니라 설정을 저장하지 않았습니다" };
|
|
316
|
+
if (kind !== "file") return { keep: null };
|
|
271
317
|
const got = priorBytes(file);
|
|
272
318
|
if (got.refusal) return got;
|
|
273
319
|
return { keep: got.bytes === null || ownSettings(got.bytes) ? null : got.bytes };
|
|
@@ -277,12 +323,14 @@ function priorSettings(file) {
|
|
|
277
323
|
* Saves the settings: the folder 0755, the file 0644 through a temp file and a
|
|
278
324
|
* rename. keep, when given, is a file anti-drm did not write; its bytes go to
|
|
279
325
|
* <file>.bak, readable by this account only, once: a .bak already there is the
|
|
280
|
-
* first such file and stays.
|
|
326
|
+
* first such file and stays. Gives back what became of keep: null when none was
|
|
327
|
+
* given, else "kept", "earlier" or "other" (see keepOnce).
|
|
281
328
|
*/
|
|
282
329
|
export function writeConfig(file, config, keep = null) {
|
|
283
330
|
mkdirSync(dirname(file), { recursive: true, mode: 0o755 });
|
|
284
|
-
|
|
331
|
+
const kept = keep === null ? null : keepOnce(`${file}.bak`, keep);
|
|
285
332
|
writeFresh(file, `${JSON.stringify(config, null, 2)}\n`, 0o644);
|
|
333
|
+
return kept;
|
|
286
334
|
}
|
|
287
335
|
|
|
288
336
|
// ---- rules ----
|
|
@@ -337,13 +385,16 @@ const code = (ext) => `\`${ext}\``;
|
|
|
337
385
|
export function renderRules(template, config) {
|
|
338
386
|
const map = table(["금지 확장자", "대신 쓸 확장자"], config.deny.map((d) => [code(d), code(replacementFor(d, config.alt))]));
|
|
339
387
|
const alts = table(["확장자", "쓰는 곳"], config.alt.map((a) => [code(a), useOf(a, config)]));
|
|
340
|
-
|
|
388
|
+
const rules = template.replace("{{map}}", () => map).replace("{{alts}}", () => alts);
|
|
389
|
+
if (rules.includes("{{")) throw new Error("규칙을 채운 뒤에도 자리 표시({{)가 남았습니다");
|
|
390
|
+
return rules;
|
|
341
391
|
}
|
|
342
392
|
|
|
343
393
|
// ---- commands ----
|
|
344
394
|
|
|
345
395
|
const USAGE =
|
|
346
|
-
"사용법: anti-drm on | off | status | config [--runtime claude|codex] [--deny <확장자 목록> --alt <확장자 목록> [--tsv tab|comma]]"
|
|
396
|
+
"사용법: anti-drm on | off | status | config [--runtime claude|codex] [--stdin | --deny <확장자 목록> --alt <확장자 목록> [--tsv tab|comma]]\n" +
|
|
397
|
+
"--stdin 은 표준 입력이 끝날 때까지 기다립니다.";
|
|
347
398
|
|
|
348
399
|
// The settings as `status` shows them.
|
|
349
400
|
function configLines(o, found) {
|
|
@@ -385,17 +436,38 @@ export function status(opts = {}) {
|
|
|
385
436
|
return done(lines.join("\n"));
|
|
386
437
|
}
|
|
387
438
|
|
|
388
|
-
//
|
|
439
|
+
// What a save does beyond writing the settings, as lines to report: a link or a hard link at the settings path becomes
|
|
440
|
+
// a regular file (what it led to stays, and nothing of it goes to .bak), and a file anti-drm did not write goes to
|
|
441
|
+
// .bak, or is not kept when an earlier one, or something that is not a regular file, is there already. kept is what
|
|
442
|
+
// writeConfig gave back.
|
|
443
|
+
const KEPT_NOTES = {
|
|
444
|
+
kept: (bak) => `참고: anti-drm 이 쓰지 않은 원래 파일은 ${bak} 에 남겼습니다.`,
|
|
445
|
+
earlier: (bak) => `참고: anti-drm 이 쓰지 않은 원래 파일은 남기지 않았습니다. 먼저 남긴 파일이 있습니다: ${bak}`,
|
|
446
|
+
other: (bak) => `참고: anti-drm 이 쓰지 않은 원래 파일은 남기지 않았습니다. 그 자리에 일반 파일이 아닌 것이 있습니다: ${bak}`,
|
|
447
|
+
};
|
|
448
|
+
const REPLACED_NOTES = {
|
|
449
|
+
link: LINK_REPLACED,
|
|
450
|
+
hardlink: "참고: 하드 링크였던 자리를 일반 파일로 바꿨습니다. 같은 파일의 다른 이름은 건드리지 않았습니다.",
|
|
451
|
+
};
|
|
452
|
+
function saveNotes(file, replaced, kept) {
|
|
453
|
+
const notes = REPLACED_NOTES[replaced] ? [REPLACED_NOTES[replaced]] : [];
|
|
454
|
+
if (kept) notes.push(KEPT_NOTES[kept](where(`${file}.bak`)));
|
|
455
|
+
return notes;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Saves the settings, as a result to report. The look just before the write names what the rename replaces.
|
|
389
459
|
function save(o, config) {
|
|
460
|
+
let notes;
|
|
390
461
|
try {
|
|
391
462
|
const prior = priorSettings(o.configFile);
|
|
392
463
|
if (prior.refusal) return refuse(`${prior.refusal}: ${where(o.configFile)}`);
|
|
393
|
-
|
|
464
|
+
const replaced = pathKind(o.configFile);
|
|
465
|
+
notes = saveNotes(o.configFile, replaced, writeConfig(o.configFile, config, prior.keep));
|
|
394
466
|
} catch (e) {
|
|
395
467
|
if (denied(e)) return refuse(`쓸 권한이 없어 설정을 저장하지 못했습니다 (${e.code}): ${where(o.configFile)}`);
|
|
396
468
|
throw e;
|
|
397
469
|
}
|
|
398
|
-
return done(`설정을 저장했습니다: ${where(o.configFile)}
|
|
470
|
+
return done([`설정을 저장했습니다: ${where(o.configFile)}`, ...notes].join("\n"));
|
|
399
471
|
}
|
|
400
472
|
|
|
401
473
|
// New rules in each runtime that holds the rules, but `skip`. A runtime that
|
|
@@ -416,8 +488,7 @@ function refresh(o, rules, skip) {
|
|
|
416
488
|
// The settings from --deny, --alt and --tsv: { config }, { error }, or { usage } when a list is missing.
|
|
417
489
|
function configFromFlags(flags) {
|
|
418
490
|
if (flags.deny === undefined || flags.alt === undefined) return { usage: true };
|
|
419
|
-
|
|
420
|
-
return checkConfig({ deny: splitList(flags.deny), alt: splitList(flags.alt), tsvDelimiter });
|
|
491
|
+
return checkConfig({ deny: splitList(flags.deny), alt: splitList(flags.alt), tsvDelimiter: flags.tsv ?? "tab" });
|
|
421
492
|
}
|
|
422
493
|
|
|
423
494
|
const settingsGiven = (flags) => ["deny", "alt", "tsv"].some((k) => flags[k] !== undefined);
|
|
@@ -438,7 +509,7 @@ function applySettings(o, flags, skip) {
|
|
|
438
509
|
// The rules from the saved settings: { rules, lines, ok } or { refusal }.
|
|
439
510
|
function savedRules(o) {
|
|
440
511
|
const found = readConfig(o.configFile);
|
|
441
|
-
if (found === null) return { refusal: refuse("설정이 없습니다. 금지 확장자와 대체 확장자를 --deny, --alt 로 함께 주세요.") };
|
|
512
|
+
if (found === null) return { refusal: refuse("설정이 없습니다. 금지 확장자와 대체 확장자를 --stdin 이나 --deny, --alt 로 함께 주세요.") };
|
|
442
513
|
if (found.error) {
|
|
443
514
|
return { refusal: refuse(`설정 파일이 올바르지 않습니다 (${found.error}): ${where(o.configFile)}. config 로 다시 만드세요.`) };
|
|
444
515
|
}
|
|
@@ -481,17 +552,89 @@ export const configure = (opts = {}, flags = {}) => cmdConfig({ ...DEFAULTS, ...
|
|
|
481
552
|
const FLAG_NAMES = ["runtime", "deny", "alt", "tsv"];
|
|
482
553
|
const COMMANDS = ["on", "off", "status", "config"];
|
|
483
554
|
|
|
484
|
-
/**
|
|
555
|
+
/**
|
|
556
|
+
* { command, runtime, flags, extra, stdin } from the command line; each value
|
|
557
|
+
* flag takes "x" or "=x", and --stdin takes nothing.
|
|
558
|
+
*/
|
|
485
559
|
export function parseArgs(argv) {
|
|
486
560
|
const rest = [];
|
|
487
561
|
const flags = {};
|
|
562
|
+
let stdin = false;
|
|
488
563
|
for (let i = 0; i < argv.length; i++) {
|
|
489
564
|
const m = /^--([a-z]+)(?:=(.*))?$/.exec(argv[i]);
|
|
490
565
|
if (m && FLAG_NAMES.includes(m[1])) flags[m[1]] = m[2] ?? argv[++i] ?? "";
|
|
566
|
+
else if (argv[i] === "--stdin") stdin = true;
|
|
491
567
|
else rest.push(argv[i]);
|
|
492
568
|
}
|
|
493
569
|
const { runtime, ...settings } = flags;
|
|
494
|
-
|
|
570
|
+
const command = (rest[0] ?? "status").toLowerCase();
|
|
571
|
+
return { command, runtime: runtime?.toLowerCase() ?? null, flags: settings, extra: rest.slice(1), stdin };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Lines on standard input that hold the settings: the key, "=", the value.
|
|
575
|
+
const STDIN_LINE = /^(deny|alt|tsv)=(.*)$/;
|
|
576
|
+
const UTF8_STRICT = new TextDecoder("utf-8", { fatal: true });
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* The settings on standard input as flags: { flags } or { error }. At most
|
|
580
|
+
* 64 KiB of UTF-8; a leading byte order mark and CRLF line ends are fine;
|
|
581
|
+
* each of deny=, alt= and tsv= once; blank lines are skipped.
|
|
582
|
+
*/
|
|
583
|
+
export function stdinSettings(bytes) {
|
|
584
|
+
if (bytes.length > MAX_BYTES) return { error: "표준 입력이 64KiB 보다 큽니다" };
|
|
585
|
+
let text;
|
|
586
|
+
try {
|
|
587
|
+
text = UTF8_STRICT.decode(bytes);
|
|
588
|
+
} catch {
|
|
589
|
+
return { error: "표준 입력이 UTF-8 이 아닙니다" };
|
|
590
|
+
}
|
|
591
|
+
const flags = {};
|
|
592
|
+
for (const line of noBom(text).split(/\r?\n/)) {
|
|
593
|
+
if (line.trim() === "") continue;
|
|
594
|
+
const m = STDIN_LINE.exec(line);
|
|
595
|
+
if (!m || flags[m[1]] !== undefined) return { error: `표준 입력의 설정 줄을 읽을 수 없습니다: ${shown(line)}` };
|
|
596
|
+
flags[m[1]] = m[2].trim();
|
|
597
|
+
}
|
|
598
|
+
return { flags };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const PAUSE = new Int32Array(new SharedArrayBuffer(4));
|
|
602
|
+
|
|
603
|
+
// Standard input, read up to cap bytes and one more: { bytes } at the end of input, or { tooBig: true } as soon as
|
|
604
|
+
// more than cap came. A pipe left non-blocking answers EAGAIN until data comes: wait a little and read again.
|
|
605
|
+
function readCapped(fd, cap) {
|
|
606
|
+
const chunks = [];
|
|
607
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
608
|
+
let total = 0;
|
|
609
|
+
for (;;) {
|
|
610
|
+
const n = readSome(fd, buf);
|
|
611
|
+
if (n === 0) return { bytes: Buffer.concat(chunks) };
|
|
612
|
+
total += n;
|
|
613
|
+
if (total > cap) return { tooBig: true };
|
|
614
|
+
chunks.push(Buffer.from(buf.subarray(0, n)));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// One read into buf: how many bytes came, 0 at the end of input (Windows may say EOF instead).
|
|
619
|
+
function readSome(fd, buf) {
|
|
620
|
+
for (;;) {
|
|
621
|
+
try {
|
|
622
|
+
return readSync(fd, buf, 0, buf.length, null);
|
|
623
|
+
} catch (e) {
|
|
624
|
+
if (e.code === "EOF") return 0;
|
|
625
|
+
if (e.code !== "EAGAIN") throw e;
|
|
626
|
+
Atomics.wait(PAUSE, 0, 0, 10);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// The settings standard input held: an error when it was too big or unreadable, or held no settings line, which
|
|
632
|
+
// on and config both refuse rather than reach for the saved settings.
|
|
633
|
+
function stdinFrom(got) {
|
|
634
|
+
if (got.tooBig) return { error: "표준 입력이 64KiB 보다 큽니다" };
|
|
635
|
+
const read = stdinSettings(got.bytes);
|
|
636
|
+
if (read.error || Object.keys(read.flags).length > 0) return read;
|
|
637
|
+
return { error: "표준 입력에 설정 줄이 없습니다" };
|
|
495
638
|
}
|
|
496
639
|
|
|
497
640
|
// Settings flags go only with on and config, and config needs both lists.
|
|
@@ -501,9 +644,11 @@ function settingsUsage(command, flags) {
|
|
|
501
644
|
}
|
|
502
645
|
|
|
503
646
|
// Why the command line cannot run as given, or "" when it can.
|
|
504
|
-
function usageError({ command, runtime, flags, extra }) {
|
|
647
|
+
function usageError({ command, runtime, flags, extra, stdin }) {
|
|
505
648
|
if (extra.length > 0 || !COMMANDS.includes(command)) return USAGE;
|
|
506
649
|
if (runtime !== null && !RUNTIMES.includes(runtime)) return USAGE;
|
|
650
|
+
// --stdin brings all the settings, so it goes with on and config only and never with a settings flag.
|
|
651
|
+
if (stdin) return (command === "on" || command === "config") && Object.keys(flags).length === 0 ? "" : USAGE;
|
|
507
652
|
return settingsUsage(command, flags);
|
|
508
653
|
}
|
|
509
654
|
|
|
@@ -517,25 +662,32 @@ function run(command, runtime, flags) {
|
|
|
517
662
|
return command === "on" ? cmdOn(o, runtime, flags) : cmdOff(o, runtime);
|
|
518
663
|
}
|
|
519
664
|
|
|
520
|
-
|
|
665
|
+
// Writes a message line and gives back the exit status.
|
|
666
|
+
const say = (text, code) => {
|
|
667
|
+
process.stdout.write(`${text}\n`);
|
|
668
|
+
return code;
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
// Whether a command acts on one runtime and so needs to know which.
|
|
672
|
+
const needsRuntime = (command) => command === "on" || command === "off";
|
|
673
|
+
// The settings flags: from standard input with --stdin, else from the command line.
|
|
674
|
+
const settingsOf = (args, input) => (args.stdin ? stdinFrom(input()) : { flags: args.flags });
|
|
675
|
+
|
|
676
|
+
function main(argv, env = process.env, input = () => readCapped(0, MAX_BYTES)) {
|
|
521
677
|
const args = parseArgs(argv);
|
|
522
678
|
const why = usageError(args);
|
|
523
|
-
if (why)
|
|
524
|
-
process.stdout.write(`${why}\n`);
|
|
525
|
-
return 1;
|
|
526
|
-
}
|
|
679
|
+
if (why) return say(why, 1);
|
|
527
680
|
const runtime = runtimeOf(args.runtime, env);
|
|
528
|
-
if ((args.command
|
|
529
|
-
|
|
530
|
-
return 1;
|
|
681
|
+
if (needsRuntime(args.command) && !RUNTIMES.includes(runtime)) {
|
|
682
|
+
return say(`런타임을 정할 수 없습니다. --runtime claude 또는 --runtime codex 를 붙이세요.\n${USAGE}`, 1);
|
|
531
683
|
}
|
|
532
684
|
try {
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
685
|
+
const got = settingsOf(args, input);
|
|
686
|
+
if (got.error) return say(`${got.error}\n${USAGE}`, 1);
|
|
687
|
+
const { ok, message } = run(args.command, runtime, got.flags);
|
|
688
|
+
return say(message, ok ? 0 : 1);
|
|
536
689
|
} catch (e) {
|
|
537
|
-
|
|
538
|
-
return 2;
|
|
690
|
+
return say(`예기치 못한 오류로 중단했습니다: ${where(e.message)}\nstatus 로 상태를 확인하세요.`, 2);
|
|
539
691
|
}
|
|
540
692
|
}
|
|
541
693
|
|