@kaydash9999/banker-plugins 0.24.0 → 0.25.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +49 -0
- package/README.md +5 -4
- package/bin/lib/codex-hooks.mjs +5 -1
- package/codex/hooks.json +4 -4
- package/codex/manifest.json +2 -1
- package/codex/skills/teamwork/scripts/teamwork.mjs +21 -11
- package/codex/transform-matrix.md +1 -0
- package/hooks/telemetry-count-codex.mjs +3 -2
- package/package.json +1 -1
- package/skills/anti-drm/SKILL.md +2 -2
- package/skills/anti-drm/scripts/anti-drm.mjs +8 -5
- package/skills/anti-drm/scripts/instruction-toggle.mjs +6 -3
- package/skills/build-testbox/scripts/browser-cell.mjs +10 -2
- package/skills/build-testbox/scripts/containers.mjs +8 -3
- package/skills/build-testbox/scripts/proc.mjs +32 -8
- package/skills/build-testbox/scripts/testbox.mjs +10 -2
- package/skills/git-init/scripts/git-init.mjs +53 -5
- package/skills/git-init/scripts/marker-block.mjs +144 -22
- package/skills/init-dir/SKILL.md +1 -1
- package/skills/init-dir/scripts/init-dir.mjs +29 -2
- package/skills/init-dir/scripts/marker-block.mjs +144 -22
- package/skills/init-dir/templates/scripts/README.md +4 -1
- package/skills/init-dir/templates/scripts/lib/service.mjs +140 -33
- package/skills/lineage/SKILL.md +1 -1
- package/skills/lineage/lineage.py +11 -11
- package/skills/motion-sites/SKILL.md +79 -0
- package/skills/motion-sites/scripts/motion-sites.mjs +331 -0
- package/skills/omc-patch/scripts/omc-patch.mjs +2 -1
- package/skills/payload-mon/scripts/payload-mon.mjs +2 -1
- package/skills/payload-mon/scripts/payload-size.mjs +2 -2
- package/skills/progress-mon/README.md +4 -3
- package/skills/progress-mon/SKILL.md +1 -1
- package/skills/progress-mon/references/adr.md +7 -0
- package/skills/progress-mon/scripts/pm-adr-view.mjs +101 -0
- package/skills/progress-mon/scripts/pm-adr.mjs +38 -105
- package/skills/progress-mon/scripts/pm-check.mjs +16 -7
- package/skills/progress-mon/scripts/pm-config.mjs +133 -7
- package/skills/progress-mon/scripts/pm-data.mjs +35 -29
- package/skills/progress-mon/scripts/pm-memory.mjs +14 -7
- package/skills/progress-mon/scripts/pm-render.mjs +1 -1
- package/skills/progress-mon/scripts/pm-server.mjs +3 -3
- package/skills/progress-mon/scripts/pm-sources.mjs +12 -2
- package/skills/remains/SKILL.md +30 -3
- package/skills/remains/scripts/boxes.mjs +14 -4
- package/skills/setup-bypass-permissions/scripts/bypass-permissions.mjs +2 -1
- package/skills/setup-bypass-permissions/scripts/codex-config.mjs +12 -7
- package/skills/setup-omc-hud/assets/apply-hud.mjs +2 -1
- package/skills/setup-omc-hud/assets/codex-config.mjs +12 -7
- package/skills/setup-omc-hud/scripts/claude-update-last.mjs +2 -1
- package/skills/smart-compact/scripts/codex-gate.mjs +25 -4
- package/skills/tone-compact/scripts/instruction-toggle.mjs +6 -3
- package/skills/tune-tmux/scripts/marker-block.mjs +144 -22
- package/skills/tune-tmux/scripts/tmux-config.mjs +35 -3
- package/skills/tune-tmux/scripts/tune-tmux.mjs +15 -9
- package/skills/ultra-chain/references/chain-ledger.mjs +8 -1
- package/skills/vertical-pptx/verify_a4p.mjs +11 -4
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Child processes for build-testbox: PATH lookup (PATHEXT on Windows), a promise-returning runner with a time limit
|
|
2
2
|
// and a set of live children, so a signal handler can stop them all. No shell: arguments go to the program as given.
|
|
3
3
|
// On Windows a .cmd or .bat goes through cmd.exe with each argument quoted (Node refuses to spawn them directly).
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
5
5
|
import { accessSync, constants, statSync } from "node:fs";
|
|
6
|
-
import { delimiter, extname, isAbsolute, join } from "node:path";
|
|
6
|
+
import { delimiter, extname, isAbsolute, join, win32 } from "node:path";
|
|
7
7
|
|
|
8
8
|
const LIVE = new Set();
|
|
9
9
|
const MAX_OUT = 8 * 1024 * 1024;
|
|
@@ -51,6 +51,21 @@ function collector() {
|
|
|
51
51
|
return { parts, add };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// Ends a child and everything it started: on POSIX its process group (the child leads one, spawned detached), on
|
|
55
|
+
// Windows the tree through System32's taskkill /T /F (not a taskkill found in the working folder).
|
|
56
|
+
function killTree(child) {
|
|
57
|
+
if (process.platform === "win32") {
|
|
58
|
+
const taskkill = win32.join(process.env.SystemRoot || "C:\\Windows", "System32", "taskkill.exe");
|
|
59
|
+
spawnSync(taskkill, ["/T", "/F", "/PID", String(child.pid)], { stdio: "ignore", windowsHide: true });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
process.kill(-child.pid, "SIGKILL");
|
|
64
|
+
} catch {
|
|
65
|
+
try { child.kill("SIGKILL"); } catch { /* already gone */ }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
// Runs a program and resolves { code, signal, stdout, stderr, out, timedOut, error }. Never rejects: a missing
|
|
55
70
|
// program is { code: null, error: "ENOENT" }. out holds stdout and stderr in arrival order.
|
|
56
71
|
export function runProc(cmd, args = [], opts = {}) {
|
|
@@ -61,13 +76,19 @@ export function runProc(cmd, args = [], opts = {}) {
|
|
|
61
76
|
let timedOut = false;
|
|
62
77
|
let child;
|
|
63
78
|
try {
|
|
64
|
-
|
|
79
|
+
const detached = process.platform !== "win32"; // its own process group, so a time-out ends what it started too
|
|
80
|
+
child = spawn(bin, argv, { cwd: opts.cwd, env: opts.env || process.env, stdio: ["ignore", "pipe", "pipe"], detached, ...extra });
|
|
65
81
|
} catch (e) {
|
|
66
82
|
resolveRun({ code: null, signal: null, ...parts, timedOut, error: e.code || String(e.message) });
|
|
67
83
|
return;
|
|
68
84
|
}
|
|
69
85
|
LIVE.add(child);
|
|
70
|
-
const timer = opts.timeoutMs ? setTimeout(() => { timedOut = true; child
|
|
86
|
+
const timer = opts.timeoutMs ? setTimeout(() => { timedOut = true; killTree(child); }, opts.timeoutMs) : null;
|
|
87
|
+
// After a time-out, a process that left the tree may still hold the pipes: they are closed a second after the exit,
|
|
88
|
+
// so the run ends instead of waiting for it.
|
|
89
|
+
child.on("exit", () => {
|
|
90
|
+
if (timedOut) setTimeout(() => { child.stdout.destroy(); child.stderr.destroy(); }, 1000).unref();
|
|
91
|
+
});
|
|
71
92
|
child.stdout.on("data", add("stdout"));
|
|
72
93
|
child.stderr.on("data", add("stderr"));
|
|
73
94
|
let error = null;
|
|
@@ -80,12 +101,15 @@ export function runProc(cmd, args = [], opts = {}) {
|
|
|
80
101
|
});
|
|
81
102
|
}
|
|
82
103
|
|
|
83
|
-
// Stops every child still running (used by the SIGINT and SIGTERM handlers).
|
|
104
|
+
// Stops every child still running (used by the SIGINT and SIGTERM handlers, and when this process exits). On POSIX a
|
|
105
|
+
// child leads its own process group, so it no longer gets the terminal's signals: an entry point that uses runProc
|
|
106
|
+
// keeps signal handlers that call killAll (testbox.mjs does).
|
|
84
107
|
export function killAll() {
|
|
85
|
-
for (const child of LIVE)
|
|
86
|
-
try { child.kill("SIGKILL"); } catch { /* already gone */ }
|
|
87
|
-
}
|
|
108
|
+
for (const child of LIVE) killTree(child);
|
|
88
109
|
LIVE.clear();
|
|
89
110
|
}
|
|
90
111
|
|
|
91
112
|
export const firstLine = (s) => String(s || "").split(/\r?\n/).map((l) => l.trim()).find(Boolean) || "";
|
|
113
|
+
|
|
114
|
+
// A child group left when this process ends some other way (an uncaught error) is ended too.
|
|
115
|
+
process.on("exit", killAll);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// build-testbox: a test bed each PC builds for itself. Browser matrix (Playwright Chromium and WebKit, desktop and
|
|
3
3
|
// mobile emulation) and container OS (Rocky Linux; macOS only as a Tart VM on Apple Silicon) run in parallel.
|
|
4
4
|
// run downloads nothing; setup downloads only after --yes. Pure Node 18+, no dependency.
|
|
5
|
-
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdirSync, realpathSync, rmSync } from "node:fs";
|
|
6
6
|
import { join, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { UsageError, parseArgs } from "./args.mjs";
|
|
@@ -144,5 +144,13 @@ export async function main(argv) {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
// Run directly, also through a linked path or another spelling: both sides as real paths.
|
|
148
|
+
const realOr = (p) => {
|
|
149
|
+
try {
|
|
150
|
+
return realpathSync(p);
|
|
151
|
+
} catch {
|
|
152
|
+
return resolve(p);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const isMain = process.argv[1] && realOr(process.argv[1]) === realOr(fileURLToPath(import.meta.url));
|
|
148
156
|
if (isMain) main(process.argv.slice(2)).then((code) => { process.exitCode = code; });
|
|
@@ -24,13 +24,17 @@
|
|
|
24
24
|
* repository, a .git below the root, no commit identity). No network is used.
|
|
25
25
|
*/
|
|
26
26
|
import { spawnSync } from "node:child_process";
|
|
27
|
-
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
27
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
28
28
|
import { homedir, tmpdir } from "node:os";
|
|
29
29
|
import { join, parse, resolve, sep } from "node:path";
|
|
30
30
|
import { pathToFileURL } from "node:url";
|
|
31
31
|
import { HASH, commitBlock, findBlock, markers, planBlock } from "./marker-block.mjs";
|
|
32
32
|
import { scan } from "./payload-scan.mjs";
|
|
33
33
|
|
|
34
|
+
// The folder programs start in: the home folder (Windows looks for a program in the working folder first, so a
|
|
35
|
+
// program planted in the folder worked on never runs), or the temp folder when there is no home folder.
|
|
36
|
+
const runDir = () => (existsSync(homedir()) ? homedir() : tmpdir());
|
|
37
|
+
|
|
34
38
|
const USAGE = [
|
|
35
39
|
"git-init: 민감 정보를 빼는 .gitignore 를 쓰고 비밀값을 스캔한 뒤 git init, git add ., git commit -m \"init\" 으로 첫 커밋을 만듦(check 는 계획만)",
|
|
36
40
|
"사용법: node git-init.mjs [check|run] [--root <dir>] [--branch <이름>] [--local-identity \"<이름> <<메일>>\"] [--nested]",
|
|
@@ -120,9 +124,29 @@ function gitEnv(extra) {
|
|
|
120
124
|
return env;
|
|
121
125
|
}
|
|
122
126
|
|
|
123
|
-
//
|
|
127
|
+
// On Windows git is a wrapper (cmd\git.exe) that starts the real git: a time limit on the wrapper alone leaves the real
|
|
128
|
+
// git running, which can still make the commit after the failure was reported. With a time limit, git runs under this
|
|
129
|
+
// small Node supervisor instead, which ends the whole tree with System32's taskkill /T /F and exits 124.
|
|
130
|
+
const TREE_SUPERVISOR = [
|
|
131
|
+
"const {spawn,spawnSync}=require('child_process');const p=require('path');",
|
|
132
|
+
"const [ms,...a]=process.argv.slice(1);",
|
|
133
|
+
"const c=spawn('git',a,{stdio:'inherit',windowsHide:true});",
|
|
134
|
+
"const t=setTimeout(()=>{spawnSync(p.win32.join(process.env.SystemRoot||'C:\\\\Windows','System32','taskkill.exe'),['/T','/F','/PID',String(c.pid)],{stdio:'ignore',windowsHide:true});process.exit(124)},Number(ms));",
|
|
135
|
+
"c.on('error',()=>process.exit(127));c.on('exit',(code)=>{clearTimeout(t);process.exit(code==null?1:code)});",
|
|
136
|
+
].join("");
|
|
137
|
+
|
|
138
|
+
// Runs git on the folder cwd (git -C), started from the home folder: Windows looks for a program in the working folder
|
|
139
|
+
// first, so a git.exe inside the folder being initialized never runs. Never throws. ok is false when git could not
|
|
140
|
+
// start or exited non-zero; a time-out is error.code ETIMEDOUT.
|
|
124
141
|
function git(args, cwd, { env, timeout } = {}) {
|
|
125
|
-
const
|
|
142
|
+
const opts = { cwd: runDir(), encoding: "utf8", env: gitEnv(env), killSignal: "SIGKILL", maxBuffer: 256 * 1024 * 1024, windowsHide: true };
|
|
143
|
+
const argv = ["-C", cwd, ...args];
|
|
144
|
+
if (timeout && process.platform === "win32") {
|
|
145
|
+
const r = spawnSync(process.execPath, ["-e", TREE_SUPERVISOR, String(timeout), ...argv], { ...opts, timeout: timeout + 30000 });
|
|
146
|
+
const error = r.status === 124 ? Object.assign(new Error("git timed out"), { code: "ETIMEDOUT" }) : r.error;
|
|
147
|
+
return { ok: !error && r.status === 0, out: r.stdout ?? "", err: (r.stderr ?? "").trim(), error };
|
|
148
|
+
}
|
|
149
|
+
const r = spawnSync("git", argv, { ...opts, timeout });
|
|
126
150
|
return { ok: !r.error && r.status === 0, out: r.stdout ?? "", err: (r.stderr ?? "").trim(), error: r.error };
|
|
127
151
|
}
|
|
128
152
|
|
|
@@ -136,12 +160,35 @@ const real = (p) => {
|
|
|
136
160
|
}
|
|
137
161
|
};
|
|
138
162
|
|
|
163
|
+
// Whether a and b are one folder: device and inode (bigint, as NTFS file ids pass 2^53), or where the file system has
|
|
164
|
+
// no inode numbers, the path as the file system spells it (realpathSync.native gives one spelling for names that differ
|
|
165
|
+
// only in case, an 8.3 short name or a linked parent), case folded on Windows and macOS. A string compare of the JS
|
|
166
|
+
// realpath let `--root c:\users\me` (or /users/me on macOS) pass the HOME refusal.
|
|
167
|
+
function sameDir(a, b) {
|
|
168
|
+
try {
|
|
169
|
+
const x = statSync(a, { bigint: true });
|
|
170
|
+
const y = statSync(b, { bigint: true });
|
|
171
|
+
if (x.ino !== 0n && y.ino !== 0n) return x.dev === y.dev && x.ino === y.ino;
|
|
172
|
+
} catch {
|
|
173
|
+
// one is gone: the spellings below decide
|
|
174
|
+
}
|
|
175
|
+
const spell = (p) => {
|
|
176
|
+
try {
|
|
177
|
+
return realpathSync.native(p);
|
|
178
|
+
} catch {
|
|
179
|
+
return real(p);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const fold = (p) => (process.platform === "win32" || process.platform === "darwin" ? p.toLowerCase() : p);
|
|
183
|
+
return fold(spell(a)) === fold(spell(b));
|
|
184
|
+
}
|
|
185
|
+
|
|
139
186
|
// The root folder, or a failure: missing (2), HOME or a file system root (3).
|
|
140
187
|
function checkRoot(rootArg) {
|
|
141
188
|
const root = resolve(rootArg);
|
|
142
189
|
if (!statSync(root, { throwIfNoEntry: false })?.isDirectory()) return fail(2, `루트 폴더가 없음: ${root}`);
|
|
143
190
|
const r = real(root);
|
|
144
|
-
if (
|
|
191
|
+
if (sameDir(root, homedir())) return fail(3, `루트가 HOME 이라 거부: ${root}. 프로젝트 폴더에서 실행하거나 --root 로 지정`);
|
|
145
192
|
if (parse(r).root === r) return fail(3, `루트가 파일 시스템 루트라 거부: ${root}`);
|
|
146
193
|
return { root };
|
|
147
194
|
}
|
|
@@ -212,7 +259,7 @@ export function missingPatterns(text) {
|
|
|
212
259
|
function gitignorePlan(root, follow) {
|
|
213
260
|
const path = join(root, ".gitignore");
|
|
214
261
|
const opts = { comment: HASH, id: ID, skill: SKILL, followSymlink: follow };
|
|
215
|
-
const probe = planBlock(path, { ...opts, body: null });
|
|
262
|
+
const probe = planBlock(path, { ...opts, body: null, readOnly: true }); // read only: never written
|
|
216
263
|
if (!probe.ok) return { refuse: probe };
|
|
217
264
|
const missing = missingPatterns(probe.before === null ? "" : probe.before.toString("utf8"));
|
|
218
265
|
const plan = planBlock(path, { ...opts, body: missing.length > 0 ? [BLOCK_HEAD, ...missing].join("\n") : null });
|
|
@@ -325,6 +372,7 @@ function commitInit(root) {
|
|
|
325
372
|
|
|
326
373
|
function runSteps(o, ctx) {
|
|
327
374
|
const done = commitBlock(ctx.gi.plan, SKILL);
|
|
375
|
+
if (!done.ok) return fail(1, `.gitignore 거부: ${done.message}. git init 하지 않음`, { ...ctx.report, ok: false });
|
|
328
376
|
const report = { ...ctx.report, gitignore: { ...ctx.report.gitignore, status: done.changed ? (done.created ? "created" : "updated") : "unchanged", backup: done.backup ?? null } };
|
|
329
377
|
const list = listCandidates(ctx.tmp, ctx.root, []);
|
|
330
378
|
const nested = list.filter((p) => p.endsWith("/"));
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* file. Windows has no owner or permission bits to check.
|
|
21
21
|
* - Writes go to a temp file in the same folder and a rename. A failed write removes the temp file.
|
|
22
22
|
* - When the bytes change, the first original is kept in `<file>.<skill>.bak`; an existing backup is never
|
|
23
|
-
* overwritten.
|
|
23
|
+
* overwritten. Something else at that name (a link, a folder) refuses the change (backup), so no original is lost.
|
|
24
24
|
*
|
|
25
25
|
* Results are { ok, changed, refused?, message, path, file }. Callers map refused to exit status 1.
|
|
26
26
|
*/
|
|
@@ -67,6 +67,13 @@ const readlinkOr = (path) => {
|
|
|
67
67
|
return "?";
|
|
68
68
|
}
|
|
69
69
|
};
|
|
70
|
+
const nativeReal = (path) => {
|
|
71
|
+
try {
|
|
72
|
+
return realpathSync.native(path);
|
|
73
|
+
} catch {
|
|
74
|
+
return resolve(path);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
70
77
|
const realOrNull = (path) => {
|
|
71
78
|
try {
|
|
72
79
|
return realpathSync(path);
|
|
@@ -96,7 +103,8 @@ function fileProblem(st, file, uid) {
|
|
|
96
103
|
return null;
|
|
97
104
|
}
|
|
98
105
|
|
|
99
|
-
// The file's stat and bytes, opened without blocking so a FIFO planted after the look never waits.
|
|
106
|
+
// The file's stat and bytes, opened without blocking so a FIFO planted after the look never waits. id: the same
|
|
107
|
+
// file's bigint stat for telling files apart (an NTFS file id passes 2^53, so as a Number two files can share one).
|
|
100
108
|
function readChecked(file, uid) {
|
|
101
109
|
const first = statSync(file);
|
|
102
110
|
const problem = fileProblem(first, file, uid);
|
|
@@ -105,7 +113,7 @@ function readChecked(file, uid) {
|
|
|
105
113
|
try {
|
|
106
114
|
const st = fstatSync(fd);
|
|
107
115
|
const late = fileProblem(st, file, uid);
|
|
108
|
-
return late ? { refuse: late } : { st, bytes: readFileSync(fd) };
|
|
116
|
+
return late ? { refuse: late } : { st, id: fstatSync(fd, { bigint: true }), bytes: readFileSync(fd) };
|
|
109
117
|
} finally {
|
|
110
118
|
closeSync(fd);
|
|
111
119
|
}
|
|
@@ -212,9 +220,25 @@ const options = (opts) => ({
|
|
|
212
220
|
wrap: opts.wrap ?? null,
|
|
213
221
|
skill: opts.skill,
|
|
214
222
|
follow: opts.followSymlink === true,
|
|
223
|
+
readOnly: opts.readOnly === true, // a plan only read (never committed): no backup spot to check
|
|
215
224
|
uid: "uid" in opts ? opts.uid : process.getuid?.(),
|
|
216
225
|
});
|
|
217
226
|
|
|
227
|
+
// A file not there yet is named by its folder as the file system spells it (one key for a linked parent, an 8.3 name
|
|
228
|
+
// or the folder in another case) and its own name as given. Two names differing only in case still get two keys:
|
|
229
|
+
// where the file system takes them for one file, applyBlocks meets that when the second create finds the first.
|
|
230
|
+
const newFileKey = (file) => `path:${join(nativeReal(dirname(resolve(file))), basename(file))}`;
|
|
231
|
+
|
|
232
|
+
// The key of a file that is there now, as planBlock gives it, or null when it cannot be read.
|
|
233
|
+
function keyNow(file) {
|
|
234
|
+
try {
|
|
235
|
+
const id = statSync(file, { bigint: true });
|
|
236
|
+
return id.ino === 0n ? `path:${nativeReal(file)}` : `ino:${id.dev}:${id.ino}`;
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
218
242
|
/**
|
|
219
243
|
* Works out what applyBlock would write, without writing: { ok, changed, refused?, message, path, file, key,
|
|
220
244
|
* before, after, mode }. key names the file (device and inode, or the path of a file not there yet).
|
|
@@ -226,14 +250,20 @@ export function planBlock(path, opts) {
|
|
|
226
250
|
if (at.refuse) return at.refuse;
|
|
227
251
|
if (!at.exists) {
|
|
228
252
|
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:
|
|
253
|
+
return { ok: true, changed: after !== null, path, file: at.file, key: newFileKey(at.file), before: null, after, mode: NEW_FILE_MODE };
|
|
230
254
|
}
|
|
231
255
|
const read = readChecked(at.file, o.uid);
|
|
232
256
|
if (read.refuse) return read.refuse;
|
|
233
257
|
const next = nextBytes(read.bytes, o, at.file);
|
|
234
258
|
if (next.refuse) return next.refuse;
|
|
235
259
|
const changed = !next.bytes.equals(read.bytes);
|
|
236
|
-
|
|
260
|
+
// A change keeps the first original in <file>.<skill>.bak: something else there (a link, a folder) is refused now,
|
|
261
|
+
// in the plan, so a dry run, a status and a check say so before anything is written.
|
|
262
|
+
const bakWhy = changed && !o.readOnly ? backupSpotWhy(`${at.file}.${o.skill}.bak`) : null;
|
|
263
|
+
if (bakWhy) return refusal("backup", `${bakWhy}: ${at.file}.${o.skill}.bak`, path);
|
|
264
|
+
// ino 0 (a file system without file ids) tells nothing apart: then the path as the file system spells it is the
|
|
265
|
+
// key (realpathSync.native gives one spelling for names that differ only in case where case is ignored).
|
|
266
|
+
const key = read.id.ino === 0n ? `path:${nativeReal(at.file)}` : `ino:${read.id.dev}:${read.id.ino}`;
|
|
237
267
|
return { ok: true, changed, path, file: at.file, key, before: read.bytes, after: next.bytes, mode: read.st.mode & 0o7777 };
|
|
238
268
|
}
|
|
239
269
|
|
|
@@ -254,36 +284,111 @@ function writeAtomic(file, data, mode) {
|
|
|
254
284
|
}
|
|
255
285
|
}
|
|
256
286
|
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
287
|
+
// Opens a file not there yet, as "wx" does, and makes sure the name is the file made. Anything at the name, a link to
|
|
288
|
+
// nothing among them, is EEXIST: Windows opens "wx" through a link to nothing and makes the file it leads to (macOS
|
|
289
|
+
// and Linux answer EEXIST), so the name is looked at first, and a link planted between that look and the open is
|
|
290
|
+
// caught after it by comparing the opened file with the name; the empty file made through it is taken back.
|
|
291
|
+
function openFresh(path, mode) {
|
|
292
|
+
if (lstatSync(path, { throwIfNoEntry: false })) throw eexist(path);
|
|
293
|
+
const fd = openSync(path, "wx", mode);
|
|
294
|
+
let made;
|
|
295
|
+
let st;
|
|
296
|
+
try {
|
|
297
|
+
made = fstatSync(fd, { bigint: true });
|
|
298
|
+
st = lstatSync(path, { bigint: true, throwIfNoEntry: false });
|
|
299
|
+
} catch {
|
|
300
|
+
return fd; // cannot look (ESTALE, EPERM): the file is the caller's, as a plain "wx" leaves it
|
|
301
|
+
}
|
|
302
|
+
if (st && !st.isSymbolicLink() && (made.ino === 0n || (st.dev === made.dev && st.ino === made.ino))) return fd;
|
|
303
|
+
closeSync(fd);
|
|
304
|
+
if (st?.isSymbolicLink()) takeBack(path, made);
|
|
305
|
+
throw eexist(path);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const eexist = (path) => Object.assign(new Error(`EEXIST: file already exists, open '${path}'`), { code: "EEXIST", path });
|
|
309
|
+
|
|
310
|
+
// Removes the file an open made through the link at path, while it is still that empty file.
|
|
311
|
+
function takeBack(path, made) {
|
|
312
|
+
try {
|
|
313
|
+
const target = realpathSync(path);
|
|
314
|
+
const st = statSync(target, { bigint: true });
|
|
315
|
+
if (st.dev === made.dev && st.ino === made.ino && st.size === 0n) rmSync(target, { force: true });
|
|
316
|
+
} catch {
|
|
317
|
+
// the link changed again: nothing of ours to find
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Writes bytes to a file not there yet (openFresh) and closes it. A write that fails takes the empty file away again
|
|
322
|
+
// while the name is still that file, so no empty file stands for a first original later.
|
|
323
|
+
function writeFresh(path, bytes, mode) {
|
|
324
|
+
const fd = openFresh(path, mode);
|
|
262
325
|
try {
|
|
263
326
|
writeFileSync(fd, bytes);
|
|
327
|
+
} catch (e) {
|
|
328
|
+
try {
|
|
329
|
+
const made = fstatSync(fd, { bigint: true });
|
|
330
|
+
const st = lstatSync(path, { bigint: true, throwIfNoEntry: false });
|
|
331
|
+
if (st && st.isFile() && st.dev === made.dev && st.ino === made.ino) rmSync(path, { force: true });
|
|
332
|
+
} catch {
|
|
333
|
+
// cannot tell the name is still that file: it stays, and the write's own error is what is reported
|
|
334
|
+
}
|
|
335
|
+
throw e;
|
|
264
336
|
} finally {
|
|
265
337
|
closeSync(fd);
|
|
266
338
|
}
|
|
267
|
-
return bak;
|
|
268
339
|
}
|
|
269
340
|
|
|
270
|
-
|
|
341
|
+
// Why the backup spot cannot take the first original, or null: something that is not a regular file there, or a look
|
|
342
|
+
// that fails (EPERM on Windows for a .bak being deleted, and the like).
|
|
343
|
+
function backupSpotWhy(bak) {
|
|
344
|
+
try {
|
|
345
|
+
const st = lstatSync(bak, { throwIfNoEntry: false });
|
|
346
|
+
return st && !st.isFile() ? "백업 자리에 일반 파일이 아닌 것이 있어 그대로 둠" : null;
|
|
347
|
+
} catch (e) {
|
|
348
|
+
return `백업 자리를 확인하지 못해 그대로 둠(${e.code || e.message})`;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Keeps the first original: writes <file>.<skill>.bak only when nothing is at that name yet. { bak } (null when a
|
|
353
|
+
// regular file there is the first original already), or { refuse } when something else is there (a link, a folder):
|
|
354
|
+
// then the file is not changed, as its original would have nowhere to go.
|
|
355
|
+
function backup(file, skill, bytes) {
|
|
356
|
+
const bak = `${file}.${skill}.bak`;
|
|
357
|
+
const there = (st) => (st.isFile() ? { bak: null } : { refuse: refusal("backup", `백업 자리에 일반 파일이 아닌 것이 있어 그대로 둠: ${bak}`, file) });
|
|
358
|
+
const st = lstatSync(bak, { throwIfNoEntry: false });
|
|
359
|
+
if (st) return there(st);
|
|
360
|
+
try {
|
|
361
|
+
writeFresh(bak, bytes, BACKUP_MODE);
|
|
362
|
+
} catch (e) {
|
|
363
|
+
if (e?.code !== "EEXIST") throw e;
|
|
364
|
+
const now = lstatSync(bak, { throwIfNoEntry: false }); // made since the look
|
|
365
|
+
return now ? there(now) : { refuse: refusal("backup", `백업을 만들지 못해 그대로 둠: ${bak}`, file) };
|
|
366
|
+
}
|
|
367
|
+
return { bak };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Writes what planBlock worked out. A plan that changes nothing or was refused is returned as a result. A file planned
|
|
372
|
+
* as new that is there by the write (made or linked since the plan) is not touched: the result is a refusal with
|
|
373
|
+
* raced: true, which applyBlocks answers by planning that file again.
|
|
374
|
+
*/
|
|
271
375
|
export function commitBlock(plan, skill) {
|
|
272
376
|
if (!plan.ok) return plan;
|
|
273
377
|
const result = { ok: true, changed: plan.changed, path: plan.path, file: plan.file };
|
|
274
378
|
if (!plan.changed) return { ...result, message: `변경 없음: ${plan.path}` };
|
|
275
379
|
if (plan.before === null && plan.after !== null) {
|
|
276
|
-
const fd = openSync(plan.file, "wx", plan.mode);
|
|
277
380
|
try {
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
|
|
381
|
+
writeFresh(plan.file, plan.after, plan.mode);
|
|
382
|
+
} catch (e) {
|
|
383
|
+
if (e?.code !== "EEXIST") throw e;
|
|
384
|
+
return { ...refusal("raced", `계획한 뒤 그 이름에 파일이나 링크가 생겨 그대로 둠: ${plan.path}`, plan.path), raced: true };
|
|
281
385
|
}
|
|
282
386
|
return { ...result, created: true, message: `새로 만듦: ${plan.path}` };
|
|
283
387
|
}
|
|
284
|
-
const
|
|
388
|
+
const kept = backup(plan.file, skill, plan.before);
|
|
389
|
+
if (kept.refuse) return kept.refuse;
|
|
285
390
|
writeAtomic(plan.file, plan.after, plan.mode);
|
|
286
|
-
return { ...result, backup: bak, message: `바꿈: ${plan.path}` };
|
|
391
|
+
return { ...result, backup: kept.bak, message: `바꿈: ${plan.path}` };
|
|
287
392
|
}
|
|
288
393
|
|
|
289
394
|
/** Puts in, replaces or (body null) takes out the block in one file. dryRun plans only. */
|
|
@@ -296,16 +401,33 @@ export function applyBlock(path, opts) {
|
|
|
296
401
|
/**
|
|
297
402
|
* Applies blocks to several files, each entry { path, ...opts }. Every file is planned first: one refusal and
|
|
298
403
|
* none is written. Two entries naming one file (after links are followed) write it once, by the first entry.
|
|
404
|
+
* A file that changes between the plans and the writes (raced) is planned again; a refusal then comes back as
|
|
405
|
+
* { ok: false, partial: true }, the entries before it being written already.
|
|
299
406
|
*/
|
|
300
407
|
export function applyBlocks(entries, { dryRun = false } = {}) {
|
|
301
408
|
const plans = entries.map((e) => ({ entry: e, plan: planBlock(e.path, e) }));
|
|
302
409
|
const refused = plans.filter((p) => !p.plan.ok).map((p) => p.plan);
|
|
303
410
|
if (refused.length > 0) return { ok: false, refused, results: plans.map((p) => p.plan) };
|
|
304
411
|
const seen = new Map();
|
|
305
|
-
const
|
|
306
|
-
|
|
412
|
+
const once = (plan) => ({ ok: true, changed: false, path: plan.path, file: plan.file, sameAs: seen.get(plan.key), message: `같은 파일이라 한 번만 씀: ${plan.path}` });
|
|
413
|
+
const write = (entry, plan) => {
|
|
414
|
+
const done = dryRun ? plan : commitBlock(plan, entry.skill);
|
|
415
|
+
if (!done.ok) return done;
|
|
307
416
|
seen.set(plan.key, plan.path);
|
|
308
|
-
|
|
417
|
+
const now = done.created ? keyNow(plan.file) : null; // a file made now is that file to later spellings too
|
|
418
|
+
if (now && !seen.has(now)) seen.set(now, plan.path);
|
|
419
|
+
return done;
|
|
420
|
+
};
|
|
421
|
+
const results = plans.map(({ entry, plan }) => {
|
|
422
|
+
if (seen.has(plan.key)) return once(plan);
|
|
423
|
+
const done = write(entry, plan);
|
|
424
|
+
if (!done.raced) return done;
|
|
425
|
+
// Made since the plan: by an earlier entry under another spelling (a case-insensitive file system), or by another
|
|
426
|
+
// program. Plan it again as what is there now.
|
|
427
|
+
const again = planBlock(entry.path, entry);
|
|
428
|
+
if (!again.ok) return again;
|
|
429
|
+
return seen.has(again.key) ? once(again) : write(entry, again);
|
|
309
430
|
});
|
|
310
|
-
|
|
431
|
+
const late = results.filter((r) => r.ok === false);
|
|
432
|
+
return late.length > 0 ? { ok: false, partial: true, refused: late, results } : { ok: true, refused: [], results };
|
|
311
433
|
}
|
package/skills/init-dir/SKILL.md
CHANGED
|
@@ -111,7 +111,7 @@ argument-hint: "[apply|status] [--only=<폴더,...>] [--follow-symlink] [--dry-r
|
|
|
111
111
|
| 로테이션 | 쓰기 전에 "현재 크기 + 쓸 양 > 100MB(104857600 바이트)" 면 돌림. `.log` 가 `.1.log` 로 밀리고 `.6.log` 는 지움. 현재 파일 포함 7개, 스트림당 최대 700MB |
|
|
112
112
|
| stop | 감독에 SIGTERM. 감독이 자식의 프로세스 그룹에 넘기고 10초 뒤 SIGKILL. 감독이 없으면 자식에 직접. Windows 는 `taskkill /T`, 10초 뒤 `/F` |
|
|
113
113
|
| status | 감독과 자식의 실행 여부를 따로, 로그 파일 수와 크기 |
|
|
114
|
-
| 시작 잠금 | `start` 는 `logs/<이름>.start.lock` 을 O_EXCL 로 만들어 잡고 끝나면 지움. 동시에 여러 번 실행해도 감독은 하나, 나중 실행은 기다린 뒤 `이미 실행 중`. 죽은 PID, 다른 프로그램, 30초 넘은 잠금은
|
|
114
|
+
| 시작 잠금 | `start` 는 `logs/<이름>.start.lock` 을 O_EXCL 로 만들어 잡고 끝나면 지움. 동시에 여러 번 실행해도 감독은 하나, 나중 실행은 기다린 뒤 `이미 실행 중`. 죽은 PID, 다른 프로그램, 30초 넘은 잠금은 `.start.lock.break` 를 잡은 `start` 하나만 다시 읽어 확인한 뒤 지움(동시 정리에도 감독은 하나). 링크나 폴더 자리는 거부 |
|
|
115
115
|
| 종료 코드 | 0 성공(status 는 둘 다 실행 중), 1 실패(시작 2초 안에 멈춤, 멈추지 못함, status 에서 실행 중 아님), 2 설정 없음(`command` 비어 있음), 3 Node 18 이상 없음(래퍼) |
|
|
116
116
|
|
|
117
117
|
- 대상 프로젝트에 Node 18 이상이 필요하다. 다른 Node 는 환경 변수 `NODE` 에 경로
|
|
@@ -93,12 +93,35 @@ const real = (p) => {
|
|
|
93
93
|
}
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
+
// Whether a and b are one folder: device and inode (bigint, as NTFS file ids pass 2^53), or where the file system has
|
|
97
|
+
// no inode numbers, the path as the file system spells it (realpathSync.native gives one spelling for names that differ
|
|
98
|
+
// only in case, an 8.3 short name or a linked parent), case folded on Windows and macOS. A string compare of the JS
|
|
99
|
+
// realpath let `--root c:\users\me` (or /users/me on macOS) pass the HOME refusal.
|
|
100
|
+
function sameDir(a, b) {
|
|
101
|
+
try {
|
|
102
|
+
const x = statSync(a, { bigint: true });
|
|
103
|
+
const y = statSync(b, { bigint: true });
|
|
104
|
+
if (x.ino !== 0n && y.ino !== 0n) return x.dev === y.dev && x.ino === y.ino;
|
|
105
|
+
} catch {
|
|
106
|
+
// one is gone: the spellings below decide
|
|
107
|
+
}
|
|
108
|
+
const spell = (p) => {
|
|
109
|
+
try {
|
|
110
|
+
return realpathSync.native(p);
|
|
111
|
+
} catch {
|
|
112
|
+
return real(p);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const fold = (p) => (process.platform === "win32" || process.platform === "darwin" ? p.toLowerCase() : p);
|
|
116
|
+
return fold(spell(a)) === fold(spell(b));
|
|
117
|
+
}
|
|
118
|
+
|
|
96
119
|
// The root folder, or a failure: missing (2), HOME or a file system root (3).
|
|
97
120
|
function checkRoot(rootArg) {
|
|
98
121
|
const root = resolve(rootArg);
|
|
99
122
|
if (!statSync(root, { throwIfNoEntry: false })?.isDirectory()) return fail(2, `루트 폴더가 없음: ${root}`);
|
|
100
123
|
const r = real(root);
|
|
101
|
-
if (
|
|
124
|
+
if (sameDir(root, homedir())) return fail(3, `루트가 HOME 이라 거부: ${root}. 프로젝트 폴더에서 실행하거나 --root 로 지정`);
|
|
102
125
|
if (parse(r).root === r) return fail(3, `루트가 파일 시스템 루트라 거부: ${root}`);
|
|
103
126
|
return { root };
|
|
104
127
|
}
|
|
@@ -165,6 +188,7 @@ const blockEntries = (root, files, present, follow) =>
|
|
|
165
188
|
}));
|
|
166
189
|
|
|
167
190
|
function instructionStatus(r) {
|
|
191
|
+
if (r.ok === false) return `refused:${r.refused}`;
|
|
168
192
|
if (r.sameAs) return "same-file";
|
|
169
193
|
if (!r.changed) return "unchanged";
|
|
170
194
|
return r.created || r.before === null ? "created" : "updated";
|
|
@@ -227,7 +251,10 @@ function apply(o) {
|
|
|
227
251
|
if (p.error) return p;
|
|
228
252
|
const folders = o.dryRun ? plannedFolders(p.folders) : makeFolders(p.root, p.folders);
|
|
229
253
|
const done = o.dryRun ? p.plan : applyBlocks(p.entries);
|
|
230
|
-
if (!done.ok)
|
|
254
|
+
if (!done.ok) {
|
|
255
|
+
const wrote = done.partial ? done.results.filter((r) => r.ok && r.changed).map((r) => r.path) : [];
|
|
256
|
+
return fail(1, `지시 파일 거부: ${done.refused.map((r) => r.message).join("; ")}. 폴더는 만들었음${wrote.length ? `. 이미 쓴 파일: ${wrote.join(", ")}` : ""}`);
|
|
257
|
+
}
|
|
231
258
|
const instructions = done.results.map((r, i) => ({ file: p.entries[i].name, status: instructionStatus(r) }));
|
|
232
259
|
const warnings = [...p.warnings, ...leftoverWarnings(p.root)];
|
|
233
260
|
return { code: 0, report: { ok: true, command: "apply", root: p.root, runtime: o.runtime, dryRun: o.dryRun, folders, instructions, warnings, present: p.present } };
|