@kaydash9999/banker-plugins 0.25.1 → 0.26.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 +24 -0
- package/README.md +4 -3
- package/codex/manifest.json +1 -0
- package/codex/transform-matrix.md +1 -0
- package/package.json +1 -1
- package/skills/make-evidence/SKILL.md +258 -0
- package/skills/make-evidence/scripts/evidence.mjs +162 -0
- package/skills/make-evidence/scripts/me-check.mjs +125 -0
- package/skills/make-evidence/scripts/me-commit.mjs +48 -0
- package/skills/make-evidence/scripts/me-git.mjs +63 -0
- package/skills/make-evidence/scripts/me-oracle.mjs +244 -0
- package/skills/make-evidence/scripts/me-precheck.mjs +122 -0
- package/skills/make-evidence/scripts/me-run.mjs +214 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// make-evidence: runs a command into a log (secrets masked, exit code kept, time limit over the process tree),
|
|
2
|
+
// and starts or stops a server. Pure Node, no network beyond the ready check on the URL it is given.
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
+
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync, writeSync } from "node:fs";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { EXCLUDE, Usage, git, lines, runDir, stamp } from "./me-git.mjs";
|
|
7
|
+
|
|
8
|
+
// ---- secrets --------------------------------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
const KEYWORD = "(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|private[_-]?key|client[_-]?secret|credential)";
|
|
11
|
+
const SECRETS = [
|
|
12
|
+
[/(authorization:\s*(?:bearer|basic|token)\s+)[^\s"']+/gi, "$1***"],
|
|
13
|
+
[new RegExp(`(\\b[A-Za-z0-9_]*${KEYWORD}[A-Za-z0-9_]*["']?\\s*[:=]\\s*["']?)[^\\s"'&,;]+`, "gi"), "$1***"],
|
|
14
|
+
[new RegExp(`(--?${KEYWORD}[A-Za-z0-9_-]*(?:=|\\s+))[^\\s"'-][^\\s"']*`, "gi"), "$1***"],
|
|
15
|
+
[/(\bmysql(?:dump)?\b[^\n]*?\s-p)[^\s"']+/gi, "$1***"],
|
|
16
|
+
[/(\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:)[^\s@/]+@/gi, "$1***@"],
|
|
17
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "***"],
|
|
18
|
+
[/\b(?:sk-[A-Za-z0-9_-]{16,}|sk_(?:live|test)_[A-Za-z0-9]{10,}|gh[pousr]_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|xox[abpr]-[A-Za-z0-9-]{10,}|eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,})/g, "***"],
|
|
19
|
+
[/(\b(?:cookie|set-cookie):\s*)\S[^\r\n]*/gi, "$1***"],
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export function redact(text) {
|
|
23
|
+
let n = 0;
|
|
24
|
+
for (const [re, to] of SECRETS) {
|
|
25
|
+
for (const m of text.matchAll(re)) if (!m[0].endsWith("***") && !m[0].endsWith("***@")) n++;
|
|
26
|
+
text = text.replace(re, (...m) => (m[0].endsWith("***") || m[0].endsWith("***@") ? m[0] : m[0].replace(new RegExp(re.source, re.flags.replace("g", "")), to)));
|
|
27
|
+
}
|
|
28
|
+
return { text, n };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Places in a text that still look like a secret (already masked ones do not count): line numbers only.
|
|
32
|
+
export function secretLines(text) {
|
|
33
|
+
const hits = new Set();
|
|
34
|
+
const all = text.split("\n");
|
|
35
|
+
all.forEach((line, i) => { if (redact(line).n) hits.add(i + 1); });
|
|
36
|
+
if (redact(text).n && !hits.size) hits.add(1); // a block over several lines
|
|
37
|
+
return [...hits];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---- logs -----------------------------------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
const quote = (a) => (/^[\w@%+=:,./-]+$/.test(a) ? a : `'${a.replace(/'/g, "'\\''")}'`);
|
|
43
|
+
|
|
44
|
+
// The log file to write: {ts} (or @TS@, which PowerShell leaves alone unquoted) filled in, the folder made, an
|
|
45
|
+
// existing file refused.
|
|
46
|
+
export function claimLog(name) {
|
|
47
|
+
const file = resolve(name.replace(/\{ts\}|@TS@/g, stamp()));
|
|
48
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
49
|
+
if (existsSync(file)) throw new Error(`로그가 이미 있음(덮어쓰지 않음): ${file}`);
|
|
50
|
+
return file;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function headInfo(cwd) {
|
|
54
|
+
const r = git(cwd, ["rev-parse", "HEAD"]);
|
|
55
|
+
if (r.status !== 0) return { sha: "none", dirty: "?" };
|
|
56
|
+
const s = git(cwd, ["--no-optional-locks", "status", "--porcelain", "--", ".", ...EXCLUDE]);
|
|
57
|
+
return { sha: r.stdout.trim(), dirty: s.status === 0 ? lines(s.stdout).length : "?" };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function header(argv, shell, cwd) {
|
|
61
|
+
const shown = redact(shell ? argv.join(" ") : argv.map(quote).join(" ")).text;
|
|
62
|
+
const h = headInfo(cwd);
|
|
63
|
+
return `$ ${shown}\n# head=${h.sha} dirty=${h.dirty} at=${new Date().toISOString()} node=${process.version} os=${process.platform} cwd=${cwd}\n`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function spawnArgs(argv, shell) {
|
|
67
|
+
if (!shell) return [argv[0], argv.slice(1)];
|
|
68
|
+
const line = argv.join(" ");
|
|
69
|
+
return process.platform === "win32" ? ["powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", line]] : ["/bin/sh", ["-c", line]];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function killTree(pid) {
|
|
73
|
+
if (!pid) return;
|
|
74
|
+
if (process.platform === "win32") {
|
|
75
|
+
spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { cwd: runDir(), stdio: "ignore", windowsHide: true });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
try { process.kill(-pid, "SIGKILL"); } catch { try { process.kill(pid, "SIGKILL"); } catch { /* already gone */ } }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Writes whole lines, masked, in the order they arrive; a partial line waits for its end.
|
|
82
|
+
function lineSink(fd, count) {
|
|
83
|
+
let part = "";
|
|
84
|
+
const put = (s) => { const r = redact(s); count.n += r.n; writeSync(fd, r.text); };
|
|
85
|
+
return {
|
|
86
|
+
push(chunk) {
|
|
87
|
+
part += chunk;
|
|
88
|
+
const i = part.lastIndexOf("\n");
|
|
89
|
+
if (i >= 0) { put(part.slice(0, i + 1)); part = part.slice(i + 1); }
|
|
90
|
+
},
|
|
91
|
+
end() { if (part) put(`${part}\n`); part = ""; },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const GRACE_MS = 3000;
|
|
96
|
+
const SIGNALS = ["SIGTERM", "SIGINT", "SIGHUP"];
|
|
97
|
+
|
|
98
|
+
function exitText(state, code, signal) {
|
|
99
|
+
if (state.timedOut) return `timeout(${state.timeoutS}s)`;
|
|
100
|
+
if (state.error) return `error(${state.error})`;
|
|
101
|
+
return code === null || code === undefined ? `signal(${signal})` : String(code);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Closes the log once: on close, a little after exit when a descendant still holds the pipes, or on a signal
|
|
105
|
+
// to this helper (the child's tree is ended first).
|
|
106
|
+
function closer(state, fd, sinks, child, done) {
|
|
107
|
+
return (code, signal, how) => {
|
|
108
|
+
if (state.ended) return;
|
|
109
|
+
state.ended = true;
|
|
110
|
+
clearTimeout(state.timer); clearTimeout(state.grace);
|
|
111
|
+
for (const s of SIGNALS) process.removeListener(s, state.onSignal);
|
|
112
|
+
sinks.forEach((s) => s.end());
|
|
113
|
+
if (how === "orphan") { killTree(child.pid); child.stdout.destroy(); child.stderr.destroy(); }
|
|
114
|
+
const exit = how === "interrupted" ? `interrupted(${signal})` : exitText(state, code, signal);
|
|
115
|
+
writeSync(fd, `${how === "orphan" ? "# orphan-output=yes\n" : ""}# redacted=${state.count.n}\nexit=${exit}\n`);
|
|
116
|
+
closeSync(fd);
|
|
117
|
+
appendFileSync(state.file, runMetrics(readFileSync(state.file, "utf8"), exit).map((m) => `METRIC ${m}\n`).join(""));
|
|
118
|
+
done({ file: state.file, code: state.timedOut ? 124 : typeof code === "number" ? code : 1, exit });
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function runLogged({ log, argv, cwd = process.cwd(), timeoutS = 300, shell = false }) {
|
|
123
|
+
if (!argv.length) throw new Usage("실행할 명령이 없음(-- 뒤에 씀)");
|
|
124
|
+
const file = claimLog(log);
|
|
125
|
+
const fd = openSync(file, "wx");
|
|
126
|
+
writeSync(fd, header(argv, shell, cwd));
|
|
127
|
+
const [cmd, args] = spawnArgs(argv, shell);
|
|
128
|
+
return new Promise((done) => {
|
|
129
|
+
const state = { file, timeoutS, count: { n: 0 }, ended: false, timedOut: false, error: null, timer: null, grace: null, onSignal: null };
|
|
130
|
+
const sinks = [lineSink(fd, state.count), lineSink(fd, state.count)];
|
|
131
|
+
const child = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"], detached: process.platform !== "win32", windowsHide: true });
|
|
132
|
+
const finish = closer(state, fd, sinks, child, done);
|
|
133
|
+
state.onSignal = (sig) => { killTree(child.pid); finish(null, sig, "interrupted"); process.exit(130); };
|
|
134
|
+
for (const s of SIGNALS) process.once(s, state.onSignal);
|
|
135
|
+
state.timer = setTimeout(() => {
|
|
136
|
+
state.timedOut = true;
|
|
137
|
+
killTree(child.pid);
|
|
138
|
+
state.grace = setTimeout(() => finish(null, "SIGKILL", "orphan"), GRACE_MS);
|
|
139
|
+
}, timeoutS * 1000);
|
|
140
|
+
child.stdout.setEncoding("utf8").on("data", (c) => sinks[0].push(c));
|
|
141
|
+
child.stderr.setEncoding("utf8").on("data", (c) => sinks[1].push(c));
|
|
142
|
+
child.on("error", (e) => { state.error = e.code || e.message; finish(null, null); });
|
|
143
|
+
child.on("exit", (code, signal) => { state.grace = setTimeout(() => finish(code, signal, "orphan"), GRACE_MS); });
|
|
144
|
+
child.on("close", (code, signal) => finish(code, signal));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Test counts the common runners print, so a document can cite them: node --test (spec and TAP), pytest, jest.
|
|
149
|
+
const SUMMARIES = [
|
|
150
|
+
[/^\S*\s*(?:ℹ|#) (tests|pass|fail|skipped|todo|cancelled) (\d+)\s*$/gm, (m) => [[m[1], m[2]]]],
|
|
151
|
+
[/^=+ (.*\b(?:passed|failed)\b.*) in [\d.]+s/gm, (m) => [...m[1].matchAll(/(\d+) (passed|failed|skipped|errors?|xfailed|xpassed)/g)].map((x) => [x[2], x[1]])],
|
|
152
|
+
[/^Tests:\s+(.*\btotal)\s*$/gm, (m) => [...m[1].matchAll(/(\d+) (passed|failed|skipped|todo|total)/g)].map((x) => [x[2], x[1]])],
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
export function runMetrics(text, exit) {
|
|
156
|
+
const out = new Map([["run.exit", exit]]);
|
|
157
|
+
for (const [re, pick] of SUMMARIES) for (const m of text.matchAll(re)) for (const [k, v] of pick(m)) out.set(`run.tests.${k}`, v);
|
|
158
|
+
return [...out].map(([k, v]) => `${k}=${v}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---- long-running servers -------------------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
async function reachable(url) {
|
|
164
|
+
try {
|
|
165
|
+
await fetch(url, { signal: AbortSignal.timeout(2000) });
|
|
166
|
+
return true;
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function waitReady(child, ready, waitS) {
|
|
173
|
+
const end = Date.now() + waitS * 1000;
|
|
174
|
+
while (Date.now() < end) {
|
|
175
|
+
if (child.exitCode !== null || child.signalCode !== null) return `not-ready(exited ${child.exitCode ?? child.signalCode})`;
|
|
176
|
+
if (await reachable(ready)) return null;
|
|
177
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
178
|
+
}
|
|
179
|
+
return `not-ready(${waitS}s)`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function serve({ log, pidFile, ready, waitS = 60, argv, cwd = process.cwd(), shell = false }) {
|
|
183
|
+
if (!argv.length) throw new Usage("실행할 명령이 없음(-- 뒤에 씀)");
|
|
184
|
+
if (existsSync(pidFile)) throw new Error(`pid 파일이 이미 있음: ${pidFile} (먼저 stop)`);
|
|
185
|
+
if (await reachable(ready)) throw new Error(`이미 답하는 서버가 있음: ${ready} (띄우지 않음. 이미 떠 있던 서버로 다룸)`);
|
|
186
|
+
const file = claimLog(log);
|
|
187
|
+
const fd = openSync(file, "wx");
|
|
188
|
+
writeSync(fd, header(argv, shell, cwd));
|
|
189
|
+
const [cmd, args] = spawnArgs(argv, shell);
|
|
190
|
+
const child = spawn(cmd, args, { cwd, stdio: ["ignore", fd, fd], detached: true, windowsHide: true });
|
|
191
|
+
closeSync(fd);
|
|
192
|
+
writeFileSync(pidFile, `${child.pid}\n${file}\n`);
|
|
193
|
+
const why = await waitReady(child, ready, waitS);
|
|
194
|
+
child.unref();
|
|
195
|
+
if (!why) return { file, pid: child.pid, ready: true };
|
|
196
|
+
stop({ pidFile, why });
|
|
197
|
+
return { file, pid: child.pid, ready: false, why };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Ends the server's process tree, masks the secrets its log may hold and closes the log with its exit line.
|
|
201
|
+
export function stop({ pidFile, why: given = "stopped" }) {
|
|
202
|
+
let why = given;
|
|
203
|
+
const [pid, file] = lines(readFileSync(pidFile, "utf8"));
|
|
204
|
+
let alive = true;
|
|
205
|
+
try { process.kill(Number(pid), 0); } catch (e) { alive = e.code === "EPERM"; }
|
|
206
|
+
if (!alive && why === "stopped") why = "gone-before-stop";
|
|
207
|
+
killTree(Number(pid));
|
|
208
|
+
if (file && existsSync(file)) {
|
|
209
|
+
const r = redact(readFileSync(file, "utf8"));
|
|
210
|
+
writeFileSync(file, `${r.text}${r.text.endsWith("\n") ? "" : "\n"}# redacted=${r.n}\nexit=${why}\n`);
|
|
211
|
+
}
|
|
212
|
+
rmSync(pidFile, { force: true });
|
|
213
|
+
return { pid: Number(pid), file };
|
|
214
|
+
}
|