@kaydash9999/banker-plugins 0.24.1 → 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 +32 -0
- package/README.md +5 -4
- package/bin/lib/codex-hooks.mjs +5 -1
- package/codex/hooks.json +4 -4
- package/codex/manifest.json +1 -0
- package/codex/skills/teamwork/scripts/teamwork.mjs +18 -8
- 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 +131 -19
- package/skills/init-dir/scripts/init-dir.mjs +29 -2
- package/skills/init-dir/scripts/marker-block.mjs +131 -19
- package/skills/init-dir/templates/scripts/lib/service.mjs +57 -6
- 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/scripts/pm-adr.mjs +6 -3
- 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-server.mjs +3 -3
- package/skills/progress-mon/scripts/pm-sources.mjs +2 -1
- 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 +131 -19
- 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
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
chmodSync, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, realpathSync,
|
|
28
28
|
renameSync, rmSync, statSync, writeFileSync,
|
|
29
29
|
} from "node:fs";
|
|
30
|
+
import { randomBytes } from "node:crypto";
|
|
30
31
|
import { homedir, tmpdir } from "node:os";
|
|
31
32
|
import { join, posix, win32 } from "node:path";
|
|
32
33
|
import { fileURLToPath } from "node:url";
|
|
@@ -60,10 +61,11 @@ function readJson(file) {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
// A new file beside the target, given the target's mode when there is one, then renamed over it. The temp name
|
|
64
|
-
//
|
|
64
|
+
// A new file beside the target, given the target's mode when there is one, then renamed over it. The temp name ends
|
|
65
|
+
// in random bytes, so no one can plant a link at it first (Windows opens "wx" through a link to nothing and makes the
|
|
66
|
+
// file it leads to), and a link in the target's place is replaced, not followed. The temp file goes on failure.
|
|
65
67
|
function writeAtomic(file, data) {
|
|
66
|
-
const tmp = `${file}.${
|
|
68
|
+
const tmp = `${file}.${randomBytes(8).toString("hex")}.tmp`;
|
|
67
69
|
try {
|
|
68
70
|
writeFileSync(tmp, data, { flag: "wx" });
|
|
69
71
|
const mode = modeOf(file);
|
|
@@ -214,10 +216,29 @@ export function hookCommand(node, script, opts = {}) {
|
|
|
214
216
|
const r = P.relative(dir, f);
|
|
215
217
|
return r === "" || (!r.startsWith("..") && !P.isAbsolute(r));
|
|
216
218
|
};
|
|
217
|
-
|
|
219
|
+
// Each path as given and as the file system spells it: a link, macOS /var against /private/var, a Windows 8.3 name
|
|
220
|
+
// or another case would otherwise slip a node in the temp folder past the check.
|
|
221
|
+
const both = (p) => [p, realOf(p)];
|
|
222
|
+
const temps = [tmpdir(), ...(win ? [] : ["/tmp"])].flatMap(both);
|
|
223
|
+
const inTemp = both(node).some((f) => temps.some((d) => under(f, d)));
|
|
224
|
+
const inWork = codex && [node, SELF].flatMap(both).some((f) => both(cwd).some((d) => under(f, d)));
|
|
225
|
+
if (inTemp || inWork) return null;
|
|
218
226
|
return formatCommand(node, script, win);
|
|
219
227
|
}
|
|
220
228
|
|
|
229
|
+
// The path as the file system spells it (native first: it gives the long name and the case on disk), or as given.
|
|
230
|
+
function realOf(p) {
|
|
231
|
+
try {
|
|
232
|
+
return realpathSync.native(p);
|
|
233
|
+
} catch {
|
|
234
|
+
try {
|
|
235
|
+
return realpathSync(p);
|
|
236
|
+
} catch {
|
|
237
|
+
return p;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
221
242
|
const COPY_RE = new RegExp(`[/\\\\]${COPY_NAME.replace(/\./g, "\\.")}["']? hook$`);
|
|
222
243
|
const isOurs = (h) => isObject(h) && typeof h.command === "string" && COPY_RE.test(h.command);
|
|
223
244
|
|
|
@@ -198,7 +198,8 @@ export function writeFresh(file, data, mode = 0o644) {
|
|
|
198
198
|
// any name that is there, a link to nothing among them (EEXIST). Windows follows a link to nothing and makes the file
|
|
199
199
|
// it leads to: then that file is removed again when it is still the empty one made, and the open is refused with
|
|
200
200
|
// EEXIST as elsewhere. A look before the open catches such a link; this catches one planted between the look and the
|
|
201
|
-
// open. A
|
|
201
|
+
// open. A name gone after the open is EEXIST too. A look that cannot be made (ESTALE, EPERM) leaves the file to the
|
|
202
|
+
// caller, as a plain "wx" would.
|
|
202
203
|
export function openNew(path, mode) {
|
|
203
204
|
const fd = openSync(path, "wx", mode);
|
|
204
205
|
let made = null;
|
|
@@ -206,8 +207,10 @@ export function openNew(path, mode) {
|
|
|
206
207
|
try {
|
|
207
208
|
made = fstatSync(fd, { bigint: true });
|
|
208
209
|
st = lstatSync(path, { bigint: true });
|
|
209
|
-
} catch {
|
|
210
|
-
return fd;
|
|
210
|
+
} catch (e) {
|
|
211
|
+
if (e?.code !== "ENOENT") return fd; // cannot look (ESTALE, EPERM): the file is the caller's, as a plain "wx" leaves it
|
|
212
|
+
closeSync(fd); // the name is gone: nothing is written through a name not proved
|
|
213
|
+
throw Object.assign(new Error(`EEXIST: file already exists, open '${path}'`), { code: "EEXIST", syscall: "open", path });
|
|
211
214
|
}
|
|
212
215
|
if (!st.isSymbolicLink() && (made.ino === 0n || sameInode(made, st))) return fd;
|
|
213
216
|
closeSync(fd);
|
|
@@ -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
|
*/
|
|
@@ -220,9 +220,25 @@ const options = (opts) => ({
|
|
|
220
220
|
wrap: opts.wrap ?? null,
|
|
221
221
|
skill: opts.skill,
|
|
222
222
|
follow: opts.followSymlink === true,
|
|
223
|
+
readOnly: opts.readOnly === true, // a plan only read (never committed): no backup spot to check
|
|
223
224
|
uid: "uid" in opts ? opts.uid : process.getuid?.(),
|
|
224
225
|
});
|
|
225
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
|
+
|
|
226
242
|
/**
|
|
227
243
|
* Works out what applyBlock would write, without writing: { ok, changed, refused?, message, path, file, key,
|
|
228
244
|
* before, after, mode }. key names the file (device and inode, or the path of a file not there yet).
|
|
@@ -234,13 +250,17 @@ export function planBlock(path, opts) {
|
|
|
234
250
|
if (at.refuse) return at.refuse;
|
|
235
251
|
if (!at.exists) {
|
|
236
252
|
const after = o.body === null ? null : Buffer.from(blockText(o.mk, o.body, o.wrap, "\n"), "utf8");
|
|
237
|
-
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 };
|
|
238
254
|
}
|
|
239
255
|
const read = readChecked(at.file, o.uid);
|
|
240
256
|
if (read.refuse) return read.refuse;
|
|
241
257
|
const next = nextBytes(read.bytes, o, at.file);
|
|
242
258
|
if (next.refuse) return next.refuse;
|
|
243
259
|
const changed = !next.bytes.equals(read.bytes);
|
|
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);
|
|
244
264
|
// ino 0 (a file system without file ids) tells nothing apart: then the path as the file system spells it is the
|
|
245
265
|
// key (realpathSync.native gives one spelling for names that differ only in case where case is ignored).
|
|
246
266
|
const key = read.id.ino === 0n ? `path:${nativeReal(at.file)}` : `ino:${read.id.dev}:${read.id.ino}`;
|
|
@@ -264,36 +284,111 @@ function writeAtomic(file, data, mode) {
|
|
|
264
284
|
}
|
|
265
285
|
}
|
|
266
286
|
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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);
|
|
272
325
|
try {
|
|
273
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;
|
|
274
336
|
} finally {
|
|
275
337
|
closeSync(fd);
|
|
276
338
|
}
|
|
277
|
-
return bak;
|
|
278
339
|
}
|
|
279
340
|
|
|
280
|
-
|
|
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
|
+
*/
|
|
281
375
|
export function commitBlock(plan, skill) {
|
|
282
376
|
if (!plan.ok) return plan;
|
|
283
377
|
const result = { ok: true, changed: plan.changed, path: plan.path, file: plan.file };
|
|
284
378
|
if (!plan.changed) return { ...result, message: `변경 없음: ${plan.path}` };
|
|
285
379
|
if (plan.before === null && plan.after !== null) {
|
|
286
|
-
const fd = openSync(plan.file, "wx", plan.mode);
|
|
287
380
|
try {
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
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 };
|
|
291
385
|
}
|
|
292
386
|
return { ...result, created: true, message: `새로 만듦: ${plan.path}` };
|
|
293
387
|
}
|
|
294
|
-
const
|
|
388
|
+
const kept = backup(plan.file, skill, plan.before);
|
|
389
|
+
if (kept.refuse) return kept.refuse;
|
|
295
390
|
writeAtomic(plan.file, plan.after, plan.mode);
|
|
296
|
-
return { ...result, backup: bak, message: `바꿈: ${plan.path}` };
|
|
391
|
+
return { ...result, backup: kept.bak, message: `바꿈: ${plan.path}` };
|
|
297
392
|
}
|
|
298
393
|
|
|
299
394
|
/** Puts in, replaces or (body null) takes out the block in one file. dryRun plans only. */
|
|
@@ -306,16 +401,33 @@ export function applyBlock(path, opts) {
|
|
|
306
401
|
/**
|
|
307
402
|
* Applies blocks to several files, each entry { path, ...opts }. Every file is planned first: one refusal and
|
|
308
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.
|
|
309
406
|
*/
|
|
310
407
|
export function applyBlocks(entries, { dryRun = false } = {}) {
|
|
311
408
|
const plans = entries.map((e) => ({ entry: e, plan: planBlock(e.path, e) }));
|
|
312
409
|
const refused = plans.filter((p) => !p.plan.ok).map((p) => p.plan);
|
|
313
410
|
if (refused.length > 0) return { ok: false, refused, results: plans.map((p) => p.plan) };
|
|
314
411
|
const seen = new Map();
|
|
315
|
-
const
|
|
316
|
-
|
|
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;
|
|
317
416
|
seen.set(plan.key, plan.path);
|
|
318
|
-
|
|
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);
|
|
319
430
|
});
|
|
320
|
-
|
|
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 };
|
|
321
433
|
}
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* Every function that runs a program takes `exec(cmd, args)` returning { status, stdout, stderr, error }, so the
|
|
7
7
|
* tests can stand in for tmux on every OS. No network is used.
|
|
8
8
|
*/
|
|
9
|
-
import { accessSync, constants, lstatSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
9
|
+
import { accessSync, constants, lstatSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
10
10
|
import { devNull, homedir } from "node:os";
|
|
11
|
-
import { basename, delimiter, join, resolve } from "node:path";
|
|
11
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
12
12
|
|
|
13
13
|
/** The tool and version in `tmux -V` output. psmux says "tmux 3.3.8" on line 1 and "psmux" on line 2. */
|
|
14
14
|
export function parseVersion(text) {
|
|
@@ -76,13 +76,45 @@ export function pickClipboard({ platform, env, has }) {
|
|
|
76
76
|
|
|
77
77
|
const exists = (p) => lstatSync(p, { throwIfNoEntry: false }) !== undefined;
|
|
78
78
|
|
|
79
|
+
// The path as the file system spells it (native first: the long name and the case on disk), a missing tail joined to
|
|
80
|
+
// its nearest existing folder so spelled.
|
|
81
|
+
function spelled(p) {
|
|
82
|
+
try {
|
|
83
|
+
return realpathSync.native(p);
|
|
84
|
+
} catch {
|
|
85
|
+
try {
|
|
86
|
+
return realpathSync(p);
|
|
87
|
+
} catch {
|
|
88
|
+
const up = dirname(p);
|
|
89
|
+
return up === p ? p : join(spelled(up), basename(p));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Whether a and b name one file: device and inode (bigint) when both are there, else the spelled paths, case folded on
|
|
96
|
+
* Windows and macOS. A string compare took a --conf in another case, through a linked XDG_CONFIG_HOME or an 8.3 name
|
|
97
|
+
* for another file and warned wrongly.
|
|
98
|
+
*/
|
|
99
|
+
export function sameFile(a, b) {
|
|
100
|
+
try {
|
|
101
|
+
const x = statSync(a, { bigint: true });
|
|
102
|
+
const y = statSync(b, { bigint: true });
|
|
103
|
+
if (x.ino !== 0n && y.ino !== 0n) return x.dev === y.dev && x.ino === y.ino;
|
|
104
|
+
} catch {
|
|
105
|
+
// one is not there (yet): the spellings decide
|
|
106
|
+
}
|
|
107
|
+
const fold = (p) => (process.platform === "win32" || process.platform === "darwin" ? p.toLowerCase() : p);
|
|
108
|
+
return fold(spelled(resolve(a))) === fold(spelled(resolve(b)));
|
|
109
|
+
}
|
|
110
|
+
|
|
79
111
|
/** The config files tmux reads by itself, in order. Measured: 3.6a reads every one there; 2.7 only ~/.tmux.conf. */
|
|
80
112
|
export function tmuxReads(v, env, home) {
|
|
81
113
|
const files = [join(home, ".tmux.conf")];
|
|
82
114
|
if (v && !atLeast(v, 3, 1)) return files;
|
|
83
115
|
if (env.XDG_CONFIG_HOME) files.push(join(env.XDG_CONFIG_HOME, "tmux", "tmux.conf"));
|
|
84
116
|
files.push(join(home, ".config", "tmux", "tmux.conf"));
|
|
85
|
-
return
|
|
117
|
+
return files.map((f) => resolve(f)).filter((f, i, all) => !all.slice(0, i).some((g) => sameFile(g, f)));
|
|
86
118
|
}
|
|
87
119
|
|
|
88
120
|
/** The tmux file for the block: --conf, else the first of tmuxReads that is there, else ~/.tmux.conf. */
|
|
@@ -19,15 +19,19 @@
|
|
|
19
19
|
* Exit status: 0 done (nothing to change included), 1 refused, write or reload failed, 2 usage error,
|
|
20
20
|
* 3 environment (on or off with neither tmux nor psmux). No network is used.
|
|
21
21
|
*/
|
|
22
|
-
import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
23
23
|
import { spawnSync } from "node:child_process";
|
|
24
|
-
import { tmpdir } from "node:os";
|
|
24
|
+
import { homedir, tmpdir } from "node:os";
|
|
25
25
|
import { join, resolve } from "node:path";
|
|
26
26
|
import { pathToFileURL } from "node:url";
|
|
27
27
|
import { HASH, commitBlock, markers, planBlock } from "./marker-block.mjs";
|
|
28
28
|
import * as tc from "./tmux-config.mjs";
|
|
29
29
|
import { appRows } from "./app-notes.mjs";
|
|
30
30
|
|
|
31
|
+
// The folder programs start in: the home folder (Windows looks for a program in the working folder first, so a
|
|
32
|
+
// program planted in the folder worked on never runs), or the temp folder when there is no home folder.
|
|
33
|
+
const runDir = () => (existsSync(homedir()) ? homedir() : tmpdir());
|
|
34
|
+
|
|
31
35
|
const USAGE = [
|
|
32
36
|
"tune-tmux: tmux, psmux 설정에 표지 블록을 넣고 빼서 마우스 드래그 복사와 PageUp/PageDown 스크롤을 맞춤",
|
|
33
37
|
"사용법: node tune-tmux.mjs [status|on|off] [--target auto|tmux|psmux] [--conf <경로>] [--follow-symlink] [--force-drag] [--reload] [--socket <이름>] [--dry-run] [--json]",
|
|
@@ -68,7 +72,8 @@ const loadCaps = () => JSON.parse(readFileSync(new URL("./psmux-caps.json", impo
|
|
|
68
72
|
|
|
69
73
|
function defaults(deps) {
|
|
70
74
|
const env = deps.env ?? process.env;
|
|
71
|
-
|
|
75
|
+
// from the home folder: Windows looks for tmux or psmux in the working folder first
|
|
76
|
+
const exec = (cmd, args) => spawnSync(cmd, args, { cwd: runDir(), encoding: "utf8", env, timeout: 15000, windowsHide: true });
|
|
72
77
|
return { env, platform: process.platform, exec, caps: null, ...deps };
|
|
73
78
|
}
|
|
74
79
|
|
|
@@ -109,8 +114,8 @@ function context(o, d) {
|
|
|
109
114
|
}
|
|
110
115
|
const conf = tc.chooseTmuxConf(t.tool, d.env, home, o.conf);
|
|
111
116
|
const reads = tc.tmuxReads(t.tool, d.env, home);
|
|
112
|
-
if (o.conf && !reads.
|
|
113
|
-
ctx.warnings.push(...tc.conflictsIn(reads.filter((f) => f
|
|
117
|
+
if (o.conf && !reads.some((f) => tc.sameFile(f, conf))) ctx.warnings.push(`경고: tmux 가 스스로 읽지 않는 파일: ${conf}. tmux -f 나 source-file 로 불러야 적용됨`);
|
|
118
|
+
ctx.warnings.push(...tc.conflictsIn(reads.filter((f) => !tc.sameFile(f, conf))));
|
|
114
119
|
const body = t.tool ? tmuxWanted(t.tool, o, d) : { lines: [], omitted: [], clipboard: null };
|
|
115
120
|
return { ...ctx, conf, ...body };
|
|
116
121
|
}
|
|
@@ -155,7 +160,9 @@ function change(ctx, o, d) {
|
|
|
155
160
|
const plan = planOf(ctx, o.cmd === "off" || none ? null : ctx.lines.join("\n"), o);
|
|
156
161
|
if (!plan.ok) return fail(1, `${plan.message}. 아무것도 바꾸지 않음`);
|
|
157
162
|
if (none && !plan.changed) return { code: 0, report: { ...reportOf(ctx, o.cmd, o), changed: false, ...noLines(ctx) } };
|
|
158
|
-
|
|
163
|
+
const done = o.dryRun ? plan : commitBlock(plan, SKILL);
|
|
164
|
+
if (!done.ok) return fail(1, `${done.message}. 아무것도 바꾸지 않음`);
|
|
165
|
+
return reloaded(ctx, plan, o, d, none ? removedOld(written(ctx, o, plan, done), ctx) : written(ctx, o, plan, done));
|
|
159
166
|
}
|
|
160
167
|
|
|
161
168
|
// The written report, applied to a running server when --reload asks (not on a dry run).
|
|
@@ -166,9 +173,8 @@ function reloaded(ctx, plan, o, d, out) {
|
|
|
166
173
|
return { code: 0, report: { ...out, reload } };
|
|
167
174
|
}
|
|
168
175
|
|
|
169
|
-
//
|
|
170
|
-
function written(ctx, o, plan) {
|
|
171
|
-
const done = o.dryRun ? plan : commitBlock(plan, SKILL);
|
|
176
|
+
// What the write (or the dry-run plan) did, as the report says it.
|
|
177
|
+
function written(ctx, o, plan, done) {
|
|
172
178
|
const empty = (o.cmd === "off" || ctx.lines.length === 0) && plan.changed && plan.after?.length === 0;
|
|
173
179
|
const note = empty ? "블록만 있던 파일이라 빈 파일로 남김" : null;
|
|
174
180
|
return { ...reportOf(ctx, o.cmd, o), changed: plan.changed, created: done.created === true, backup: done.backup ?? null, note };
|
|
@@ -18,8 +18,13 @@ import { spawnSync } from "node:child_process";
|
|
|
18
18
|
import { createHash, randomBytes } from "node:crypto";
|
|
19
19
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
20
20
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
21
|
+
import { homedir, tmpdir } from "node:os";
|
|
21
22
|
import { fileURLToPath } from "node:url";
|
|
22
23
|
|
|
24
|
+
// The folder programs start in: the home folder (Windows looks for a program in the working folder first, so a
|
|
25
|
+
// program planted in the folder worked on never runs), or the temp folder when there is no home folder.
|
|
26
|
+
const startDir = () => (existsSync(homedir()) ? homedir() : tmpdir());
|
|
27
|
+
|
|
23
28
|
export const STAGES = [
|
|
24
29
|
{ id: "preflight", label: "사전 점검" },
|
|
25
30
|
{ id: "research", label: "자료 조사" },
|
|
@@ -219,8 +224,10 @@ function checkQuorum(event, meta) {
|
|
|
219
224
|
|
|
220
225
|
// ── run folder ───────────────────────────────────────────────────────────────
|
|
221
226
|
|
|
227
|
+
// Started from the home folder with git -C: Windows looks for a program in the working folder first, so a git.exe
|
|
228
|
+
// inside the folder worked on never runs.
|
|
222
229
|
function git(cwd, args) {
|
|
223
|
-
const r = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
230
|
+
const r = spawnSync("git", ["-C", resolve(cwd), ...args], { cwd: startDir(), encoding: "utf8" });
|
|
224
231
|
return r.status === 0 ? r.stdout.trim() : null;
|
|
225
232
|
}
|
|
226
233
|
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
* Usage: node verify_a4p.mjs <file.pptx> [--json] [--strict] [--font-file <path>] [--deck <deck.json>]
|
|
42
42
|
*/
|
|
43
43
|
import { spawnSync } from 'node:child_process';
|
|
44
|
+
import { randomBytes } from 'node:crypto';
|
|
44
45
|
import fs from 'node:fs';
|
|
45
46
|
import { createRequire } from 'node:module';
|
|
46
47
|
import os from 'node:os';
|
|
@@ -2101,10 +2102,16 @@ export function main(argv) {
|
|
|
2101
2102
|
console.error(`verify_a4p: 재압축 산출물이 자기 무결성 검사를 통과하지 못해 쓰지 않았다:\n ${integrity.problems.join('\n ')}`);
|
|
2102
2103
|
return EXIT.FAIL;
|
|
2103
2104
|
}
|
|
2104
|
-
// Stage then rename, so an interrupted --in-place cannot leave a half-written deck.
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2105
|
+
// Stage then rename, so an interrupted --in-place cannot leave a half-written deck. The stage name is random
|
|
2106
|
+
// and made exclusively, so a link someone left at a fixed name is never written through.
|
|
2107
|
+
const tmp = `${target}.${randomBytes(6).toString('hex')}.a4p-tmp`;
|
|
2108
|
+
try {
|
|
2109
|
+
fs.writeFileSync(tmp, rep.out, { flag: 'wx' });
|
|
2110
|
+
fs.renameSync(tmp, target);
|
|
2111
|
+
} catch (e) {
|
|
2112
|
+
fs.rmSync(tmp, { force: true });
|
|
2113
|
+
throw e;
|
|
2114
|
+
}
|
|
2108
2115
|
|
|
2109
2116
|
report.write = {
|
|
2110
2117
|
mode: opts.convert ? 'convert-a4p' : 'repair',
|