@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// pm-config.mjs: 프로젝트 루트와 자료 폴더 결정, 런타임 판별, config.json 읽기와 기본값, 형식 검사, 원자적 쓰기.
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import fs from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import net from 'node:net';
|
|
@@ -52,13 +53,64 @@ export function readRegular(file, encoding = 'utf8', fsx = fs, max = READ_MAX) {
|
|
|
52
53
|
return fsx.readFileSync(file, encoding);
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
// 실제 경로. native 먼저(Windows 의 8.3 짧은 이름, macOS 와 Windows 의 디스크 대소문자로 푼다). 아직 없는 꼬리는 있는
|
|
57
|
+
// 조상까지 풀고 붙인다(macOS 의 /var 는 /private/var 로 가는 링크).
|
|
58
|
+
export function realNative(p) {
|
|
59
|
+
try {
|
|
60
|
+
return fs.realpathSync.native(p);
|
|
61
|
+
} catch {
|
|
62
|
+
try {
|
|
63
|
+
return fs.realpathSync(p);
|
|
64
|
+
} catch {
|
|
65
|
+
const up = path.dirname(p);
|
|
66
|
+
return up === p ? p : path.join(realNative(up), path.basename(p));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 두 경로가 같은 폴더인지: 장치와 inode(bigint), inode 가 없는 파일 시스템은 realNative 철자(Windows, macOS 는 접어서).
|
|
72
|
+
// 문자열 비교는 대소문자, 8.3 이름, 링크인 부모 폴더처럼 철자만 다른 같은 폴더를 다른 폴더로 본다.
|
|
73
|
+
export function sameFolder(a, b) {
|
|
74
|
+
try {
|
|
75
|
+
const x = fs.statSync(a, { bigint: true });
|
|
76
|
+
const y = fs.statSync(b, { bigint: true });
|
|
77
|
+
if (x.ino !== 0n && y.ino !== 0n) return x.dev === y.dev && x.ino === y.ino;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const fold = (p) => (process.platform === 'win32' || process.platform === 'darwin' ? p.toLowerCase() : p);
|
|
82
|
+
return fold(realNative(a)) === fold(realNative(b));
|
|
83
|
+
}
|
|
84
|
+
|
|
55
85
|
// 프로젝트 안 자료 폴더의 경로 가운데 링크인 첫 폴더(.omc, progress-mon 등). 없으면 null.
|
|
56
|
-
// 프로젝트 밖(--data)이면 자료 폴더 자체만 본다. 아직 없는 폴더에서 멈춘다.
|
|
86
|
+
// 프로젝트 밖(--data)이면 자료 폴더 자체만 본다. 아직 없는 폴더에서 멈춘다. 안팎은 자료 폴더에서 위로 올라가며
|
|
87
|
+
// 프로젝트 루트와 같은 폴더(sameFolder)를 만나는지로 본다: --data 를 다른 대소문자나 8.3 이름으로 적어도 안으로 본다.
|
|
57
88
|
function linkedDir(root, dataDir) {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
89
|
+
const isLink = (d) => {
|
|
90
|
+
try {
|
|
91
|
+
return fs.lstatSync(d).isSymbolicLink();
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
// 위로 올라가며 루트를 찾는다. 링크가 아닌 루트를 만나면 그 아래가 안이다(.omc -> . 같은 안의 링크도 목록에 남아
|
|
97
|
+
// 거부된다). 끝까지 못 만나면 루트를 가리키는 첫 링크(루트를 링크 철자로 적은 --data)를 루트로 본다.
|
|
98
|
+
const up = [];
|
|
99
|
+
let rootAt = -1;
|
|
100
|
+
let linkRootAt = -1;
|
|
101
|
+
for (let d = dataDir; ; d = path.dirname(d)) {
|
|
102
|
+
if (sameFolder(d, root)) {
|
|
103
|
+
if (!isLink(d)) {
|
|
104
|
+
rootAt = up.length;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
if (linkRootAt < 0) linkRootAt = up.length;
|
|
108
|
+
}
|
|
109
|
+
up.push(d);
|
|
110
|
+
if (path.dirname(d) === d) break;
|
|
111
|
+
}
|
|
112
|
+
const cut = rootAt >= 0 ? rootAt : linkRootAt;
|
|
113
|
+
const dirs = cut > 0 ? up.slice(0, cut).reverse() : [dataDir];
|
|
62
114
|
for (const d of dirs) {
|
|
63
115
|
let st;
|
|
64
116
|
try {
|
|
@@ -85,8 +137,11 @@ export function privateIPv4(ip) {
|
|
|
85
137
|
return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168);
|
|
86
138
|
}
|
|
87
139
|
|
|
88
|
-
// git 출력(앞뒤 공백 제거). git 이 없거나 실패하면 예외.
|
|
89
|
-
|
|
140
|
+
// git 출력(앞뒤 공백 제거). git 이 없거나 실패하면 예외. 홈 폴더에서 git -C 로 부른다: Windows 는 일하는 폴더의
|
|
141
|
+
// 프로그램을 먼저 찾으므로, 프로젝트 안의 git.exe 가 실행되지 않게.
|
|
142
|
+
// 프로그램을 띄울 폴더: 홈 폴더(없으면 임시 폴더).
|
|
143
|
+
export const runDir = () => (fs.existsSync(os.homedir()) ? os.homedir() : os.tmpdir());
|
|
144
|
+
export const git = (cwd, args) => execFileSync('git', ['-C', path.resolve(cwd), ...args], { cwd: runDir(), encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
90
145
|
|
|
91
146
|
// 프로젝트 루트: --project, 아니면 cwd 의 git 최상위, 실패면 cwd.
|
|
92
147
|
export function projectRoot(cwd, project) {
|
|
@@ -123,6 +178,77 @@ function pickContext({ cwd = process.cwd(), project, runtime, data } = {}) {
|
|
|
123
178
|
// 사용법 오류(종료 1).
|
|
124
179
|
export class UsageError extends Error {}
|
|
125
180
|
|
|
181
|
+
// 고정 이름(잠금, 백업, 처음 만드는 설정)에 새 파일을 만든다(wx). 그 이름에 무엇이든 있으면 끊어진 링크까지 EEXIST.
|
|
182
|
+
// 까닭: Windows 의 wx 는 끊어진 링크를 따라가 그 너머에 파일을 만든다(macOS, Linux 는 EEXIST). 그래서 먼저 lstat 로
|
|
183
|
+
// 보고, 연 뒤 쓰기 전에 연 파일(fstat)과 이름(lstat)을 대조한다. 이름이 없거나 링크이거나 다른 파일이면 아무것도 쓰지
|
|
184
|
+
// 않고 EEXIST(링크를 따라 만든 빈 파일은 지움). 볼 수 없으면(ESTALE, EPERM 등) 보통의 wx 처럼 그대로 쓴다. 쓰기가
|
|
185
|
+
// 실패하면 이름이 아직 그 파일일 때 지운다(빈 잠금이나 빈 백업이 남지 않게). 처음 lstat 의 EPERM 등(Windows 에서 지우는
|
|
186
|
+
// 중인 파일)은 그대로 던진다. 부른 쪽은 wx 의 같은 코드처럼 기다린다.
|
|
187
|
+
export function writeNew(file, data, opts = {}, fsx = fs) {
|
|
188
|
+
if (fsx.lstatSync(file, { throwIfNoEntry: false }) !== undefined) throw eexist(file);
|
|
189
|
+
const fd = fsx.openSync(file, 'wx', opts.mode);
|
|
190
|
+
try {
|
|
191
|
+
const made = fstatOr(fd, fsx);
|
|
192
|
+
if (namesOpened(file, made, fsx) === false) throw eexist(file);
|
|
193
|
+
try {
|
|
194
|
+
fsx.writeFileSync(fd, data);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
if (namesOpened(file, made, fsx) === true) fsx.rmSync(file, { force: true });
|
|
197
|
+
throw e;
|
|
198
|
+
}
|
|
199
|
+
} finally {
|
|
200
|
+
fsx.closeSync(fd);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const fstatOr = (fd, fsx) => {
|
|
205
|
+
try {
|
|
206
|
+
return fsx.fstatSync(fd, { bigint: true });
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// 이름이 연 파일(made)을 가리키는지: 참, 거짓(없음, 링크, 다른 파일), 볼 수 없으면 null. 링크면 그 링크로 만든 빈 파일을 지움.
|
|
213
|
+
function namesOpened(file, made, fsx) {
|
|
214
|
+
let st;
|
|
215
|
+
try {
|
|
216
|
+
st = fsx.lstatSync(file, { bigint: true, throwIfNoEntry: false });
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
if (!st) return false;
|
|
221
|
+
if (st.isSymbolicLink()) {
|
|
222
|
+
if (made) dropFollowed(file, made, fsx);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (!made || made.ino === 0n) return null;
|
|
226
|
+
return st.dev === made.dev && st.ino === made.ino;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 링크를 따라 연 파일이 아직 그 빈 파일이면 지운다.
|
|
230
|
+
function dropFollowed(file, made, fsx) {
|
|
231
|
+
try {
|
|
232
|
+
const target = fsx.realpathSync(file);
|
|
233
|
+
const at = fsx.statSync(target, { bigint: true });
|
|
234
|
+
if (at.dev === made.dev && at.ino === made.ino && at.size === 0n) fsx.rmSync(target, { force: true });
|
|
235
|
+
} catch {
|
|
236
|
+
// 링크가 또 바뀜: 되돌릴 것을 찾지 못함
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const eexist = (file) => Object.assign(new Error(`EEXIST: file already exists, open '${file}'`), { code: 'EEXIST', syscall: 'open', path: file });
|
|
241
|
+
|
|
242
|
+
// 그 자리에 일반 파일이 아닌 것(링크, 폴더 등)이 있는지. 없거나 볼 수 없으면 거짓.
|
|
243
|
+
export function notPlainFile(file, fsx = fs) {
|
|
244
|
+
try {
|
|
245
|
+
const st = fsx.lstatSync(file, { throwIfNoEntry: false });
|
|
246
|
+
return st !== undefined && !st.isFile();
|
|
247
|
+
} catch {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
126
252
|
const RENAME_RETRIES = 5;
|
|
127
253
|
const RENAME_STEP_MS = 50;
|
|
128
254
|
const RENAME_RETRY_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import crypto from 'node:crypto';
|
|
7
|
-
import { dataLabel, defaults, loadConfig, eventsPath, writeAtomic, git, noBom, readRegular, UsageError } from './pm-config.mjs';
|
|
7
|
+
import { dataLabel, defaults, loadConfig, eventsPath, writeAtomic, writeNew, notPlainFile, git, noBom, readRegular, realNative, UsageError } from './pm-config.mjs';
|
|
8
8
|
import { stageList } from './pm-sources.mjs';
|
|
9
9
|
import { planMemory, applyMemory } from './pm-memory.mjs';
|
|
10
10
|
|
|
@@ -58,7 +58,7 @@ function readLock(file, fsx) {
|
|
|
58
58
|
function breakStale(file, seen, fsx) {
|
|
59
59
|
const brk = `${file}.break`;
|
|
60
60
|
try {
|
|
61
|
-
|
|
61
|
+
writeNew(brk, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }) + '\n', {}, fsx);
|
|
62
62
|
} catch (e) {
|
|
63
63
|
if (!BUSY_CODES.has(e.code)) return false;
|
|
64
64
|
try {
|
|
@@ -97,6 +97,27 @@ function breakStale(file, seen, fsx) {
|
|
|
97
97
|
return removed;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// 잠금을 한 번 만들어 본다. 돌려줌: 만들었으면 null, 아니면 기다릴 까닭 코드(EEXIST, Windows 의 EPERM 등).
|
|
101
|
+
// 링크나 폴더가 잠금 자리에 있으면 풀릴 잠금이 아니므로 기다리지 않고 거부한다(끊어진 링크면 읽기가 없음으로 보여 헛돈다).
|
|
102
|
+
function tryLockOnce(file, body, fsx, label) {
|
|
103
|
+
try {
|
|
104
|
+
writeNew(file, body, {}, fsx);
|
|
105
|
+
return null;
|
|
106
|
+
} catch (e) {
|
|
107
|
+
if (!BUSY_CODES.has(e.code)) throw new UsageError(`잠금 파일을 만들지 못함(${e.code || e.message})`);
|
|
108
|
+
if (notPlainFile(file, fsx)) throw new UsageError(`잠금 자리에 일반 파일이 아닌 것(링크나 폴더)이 있어 거부: ${label}items.json.lock`);
|
|
109
|
+
return e.code;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 기다림이 끝났을 때의 오류. 잠금 파일이 끝내 없었으면 다른 쓰기가 아니라 만들지 못한 것(권한 등).
|
|
114
|
+
function lockTimeout(code, seenLock, last, label) {
|
|
115
|
+
if (!seenLock && code !== 'EEXIST') return new UsageError(`잠금 파일을 만들지 못함(${code})`);
|
|
116
|
+
return new UsageError(
|
|
117
|
+
`다른 쓰기가 끝나지 않음(잠금 ${label}items.json.lock, pid ${last.pid ?? '알 수 없음'}, createdAt ${last.createdAt ?? '알 수 없음'}). 그 프로세스가 끝났는데도 남아 있으면 잠금 파일을 지운 뒤 다시 실행`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
100
121
|
// items.json.lock 을 wx 로 만든다. 있으면 50 ms 간격으로 최대 waitMs 기다린다.
|
|
101
122
|
// Windows 에서 지우는 중인 파일에 wx 하면 EPERM(또는 EACCES, EBUSY)이라 EEXIST 처럼 기다린다.
|
|
102
123
|
// 낡은 잠금은 breakStale 로만 정리한다.
|
|
@@ -109,27 +130,15 @@ export async function acquireLock(dataDir, { fsx = fs, waitMs = LOCK_WAIT_MS, la
|
|
|
109
130
|
let last = {};
|
|
110
131
|
let seenLock = false; // 기다리는 동안 잠금 파일이 한 번이라도 보였는지
|
|
111
132
|
for (;;) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
fsx.writeFileSync(file, body, { flag: 'wx' });
|
|
115
|
-
return { file, mine };
|
|
116
|
-
} catch (e) {
|
|
117
|
-
code = e.code;
|
|
118
|
-
if (!BUSY_CODES.has(code)) throw new UsageError(`잠금 파일을 만들지 못함(${code || e.message})`);
|
|
119
|
-
}
|
|
133
|
+
const code = tryLockOnce(file, body, fsx, label);
|
|
134
|
+
if (code === null) return { file, mine };
|
|
120
135
|
const info = readLock(file, fsx);
|
|
121
136
|
// 그 사이 풀림: 바로 다시. 지우는 중(EPERM 등)이면 아래에서 기다린다
|
|
122
137
|
if (info === null && code === 'EEXIST' && Date.now() < until) continue;
|
|
123
138
|
if (info) seenLock = true;
|
|
124
139
|
if (info && info.pid !== undefined) last = info;
|
|
125
140
|
if (staleLock(info, Date.now()) && breakStale(file, info, fsx)) continue;
|
|
126
|
-
if (Date.now() >= until)
|
|
127
|
-
// 잠금 파일이 끝내 없었으면 다른 쓰기가 아니라 만들지 못한 것(권한 등)
|
|
128
|
-
if (!seenLock && code !== 'EEXIST') throw new UsageError(`잠금 파일을 만들지 못함(${code})`);
|
|
129
|
-
throw new UsageError(
|
|
130
|
-
`다른 쓰기가 끝나지 않음(잠금 ${label}items.json.lock, pid ${last.pid ?? '알 수 없음'}, createdAt ${last.createdAt ?? '알 수 없음'}). 그 프로세스가 끝났는데도 남아 있으면 잠금 파일을 지운 뒤 다시 실행`,
|
|
131
|
-
);
|
|
132
|
-
}
|
|
141
|
+
if (Date.now() >= until) throw lockTimeout(code, seenLock, last, label);
|
|
133
142
|
await sleep(STEP_MS);
|
|
134
143
|
}
|
|
135
144
|
}
|
|
@@ -287,13 +296,6 @@ function storyChange(o, now, configIds) {
|
|
|
287
296
|
const NEXT = ['다음 할 일', ' 1. references/authoring.md 를 읽고 req-spec.json, reqs.json, flow.json, arch.json, adr.json 작성', ' 2. check 로 검사. FAIL 0 까지 최대 3회', ' 3. start 로 서버 시작'];
|
|
288
297
|
|
|
289
298
|
const slash = (p) => p.split(path.sep).join('/');
|
|
290
|
-
const realOr = (p) => {
|
|
291
|
-
try {
|
|
292
|
-
return fs.realpathSync(p);
|
|
293
|
-
} catch {
|
|
294
|
-
return p;
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
299
|
const inside = (rel) => rel && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
298
300
|
|
|
299
301
|
// 자료 폴더를 git 제외 목록(git rev-parse --git-path info/exclude)에 한 줄 추가. 추적 파일은 바꾸지 않는다. 실패는 알림만.
|
|
@@ -306,8 +308,9 @@ function gitExclude(ctx) {
|
|
|
306
308
|
} catch {
|
|
307
309
|
return 'git 제외 목록: git 저장소 아님, 건너뜀';
|
|
308
310
|
}
|
|
309
|
-
// 링크 경로(macOS /var 와 /private/var, Windows junction)
|
|
310
|
-
|
|
311
|
+
// 링크 경로(macOS /var 와 /private/var, Windows junction), 다른 대소문자, 8.3 이름으로 부른 --project 나 --data 도
|
|
312
|
+
// git 최상위와 같은 철자로 푼다(realNative: 디스크의 철자)
|
|
313
|
+
const rel = path.relative(realNative(top), realNative(ctx.dataDir));
|
|
311
314
|
if (!inside(rel)) return 'git 제외 목록: 자료 폴더가 저장소 밖이라 건너뜀';
|
|
312
315
|
const line = `/${slash(rel)}/`;
|
|
313
316
|
const shown = inside(path.relative(ctx.project, excl)) ? slash(path.relative(ctx.project, excl)) : 'info/exclude';
|
|
@@ -334,10 +337,13 @@ function gitExclude(ctx) {
|
|
|
334
337
|
return `git 제외 목록 추가: ${line} (${shown})`;
|
|
335
338
|
}
|
|
336
339
|
|
|
337
|
-
// 없을 때만 만든다. 돌려줌: 출력 줄.
|
|
340
|
+
// 없을 때만 만든다. 돌려줌: 출력 줄. 가리키는 것이 없는 링크는 거부한다(만들면 링크 너머에 파일이 생긴다).
|
|
338
341
|
function makeOnce(file, label, text) {
|
|
339
|
-
if (fs.
|
|
340
|
-
|
|
342
|
+
if (fs.lstatSync(file, { throwIfNoEntry: false }) !== undefined) {
|
|
343
|
+
if (fs.existsSync(file)) return `그대로 둠: ${label}`;
|
|
344
|
+
throw new UsageError(`${label} 자리에 가리키는 것이 없는 링크가 있어 거부`);
|
|
345
|
+
}
|
|
346
|
+
writeNew(file, text);
|
|
341
347
|
return `새로 만듦: ${label}`;
|
|
342
348
|
}
|
|
343
349
|
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
// 예외: 표식이 붙은 우리 줄(앞머리로 가린 규칙 줄과 색인 줄)이 지금 판과 다르면 그 줄만 제자리에서 바꾼다(첫 원본은 .progress-mon.bak).
|
|
3
3
|
// 대상 파일은 에이전트가 넘긴다(--memory, --memory-dir). 스크립트는 HOME 아래 쓰기 경로를 스스로 만들지 않는다.
|
|
4
4
|
// 덧붙인 뒤 규칙 로드 한도를 재고 경고 줄을 낸다(종료 코드는 그대로). status, check 도 같은 측정을 다시 한다.
|
|
5
|
+
import crypto from 'node:crypto';
|
|
5
6
|
import fs from 'node:fs';
|
|
6
7
|
import os from 'node:os';
|
|
7
8
|
import path from 'node:path';
|
|
8
|
-
import { git, readRegular, UsageError } from './pm-config.mjs';
|
|
9
|
+
import { git, readRegular, realNative, UsageError, writeNew } from './pm-config.mjs';
|
|
9
10
|
|
|
10
11
|
export const MARKER = '<!-- progress-mon:rule -->';
|
|
11
12
|
export const OMX_OPEN = '<!-- USER:OMX:POLICY:START -->';
|
|
@@ -159,8 +160,10 @@ const gitTop = (dir) => {
|
|
|
159
160
|
// Codex 프로젝트 지시 합계: 저장소 루트에서 프로젝트 루트까지 각 폴더에서 Codex 가 읽는 파일 크기의 합.
|
|
160
161
|
export function measureCodex(project, env = process.env) {
|
|
161
162
|
const limit = codexLimit(env);
|
|
162
|
-
|
|
163
|
-
const
|
|
163
|
+
// 둘 다 디스크의 철자로 푼다: 링크나 다른 대소문자로 준 --project 가 저장소 밖으로 보여 지시 파일을 빼먹지 않게
|
|
164
|
+
const proj = realNative(path.resolve(project));
|
|
165
|
+
const top0 = gitTop(proj);
|
|
166
|
+
const top = top0 && realNative(top0);
|
|
164
167
|
const rel = top ? path.relative(top, proj) : '';
|
|
165
168
|
const dirs = [top && !rel.startsWith('..') && !path.isAbsolute(rel) ? top : proj];
|
|
166
169
|
if (dirs[0] !== proj) for (const part of rel.split(path.sep).filter(Boolean)) dirs.push(path.join(dirs[dirs.length - 1], part));
|
|
@@ -349,7 +352,8 @@ function rewrite(file, body) {
|
|
|
349
352
|
const real = fs.realpathSync(file);
|
|
350
353
|
const st = fs.statSync(real);
|
|
351
354
|
if (!st.isFile() || st.nlink > 1) throw changedError(file);
|
|
352
|
-
|
|
355
|
+
// 임시 이름은 난수: Windows 의 wx 는 끊어진 링크를 따라가므로, 미리 맞힐 수 있는 이름이면 링크를 심어 둘 수 있다.
|
|
356
|
+
const tmp = `${real}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
|
|
353
357
|
try {
|
|
354
358
|
fs.writeFileSync(tmp, body, { flag: 'wx', mode: st.mode & 0o777 });
|
|
355
359
|
fs.chmodSync(tmp, st.mode & 0o7777);
|
|
@@ -368,13 +372,16 @@ function rewrite(file, body) {
|
|
|
368
372
|
}
|
|
369
373
|
}
|
|
370
374
|
|
|
371
|
-
// 첫 원본 백업. 이미 있으면 덮지 않는다. 만들지
|
|
375
|
+
// 첫 원본 백업. 이미 있으면 덮지 않는다. 만들지 못하거나 그 자리에 일반 파일이 아닌 것(링크 등)이 있으면 바꾸지 않고 거부.
|
|
372
376
|
function backup(file, buf) {
|
|
373
377
|
const bak = `${file}.progress-mon.bak`;
|
|
374
378
|
try {
|
|
375
|
-
|
|
379
|
+
writeNew(bak, buf);
|
|
376
380
|
} catch (e) {
|
|
377
|
-
|
|
381
|
+
// EEXIST 는 그 자리에 일반 파일(먼저 남긴 첫 원본)이 있을 때만 통과. 링크, 폴더, 그 사이 사라진 이름은 거부
|
|
382
|
+
const st = e.code === 'EEXIST' ? fs.lstatSync(bak, { throwIfNoEntry: false }) : null;
|
|
383
|
+
const why = e.code !== 'EEXIST' ? e.code || e.message : !st ? '만든 뒤 이름이 사라짐' : !st.isFile() ? '일반 파일이 아닌 것(링크나 폴더)이 있음' : null;
|
|
384
|
+
if (why) throw new UsageError(`${path.basename(bak)} 백업을 만들지 못해 규칙 줄을 바꾸지 않음(${why})`);
|
|
378
385
|
}
|
|
379
386
|
return bak;
|
|
380
387
|
}
|
|
@@ -4,7 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import http from 'node:http';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
|
-
import { privateIPv4, dataLabel, loadConfig, writeAtomic, readRegular, UsageError } from './pm-config.mjs';
|
|
7
|
+
import { privateIPv4, dataLabel, loadConfig, writeAtomic, writeNew, readRegular, UsageError } from './pm-config.mjs';
|
|
8
8
|
import { workflowRoots } from './pm-sources.mjs';
|
|
9
9
|
import { memoryWarnings } from './pm-memory.mjs';
|
|
10
10
|
|
|
@@ -521,7 +521,7 @@ export async function saveHost(ctx, ip, io, fsx = fs) {
|
|
|
521
521
|
|
|
522
522
|
// ---------- start 잠금 ----------
|
|
523
523
|
// 동시에 부른 start 가 서버를 둘 띄우지 않게 start 전체를 server.json.lock 으로 감싼다(wx: 둘 가운데 하나만 만듦,
|
|
524
|
-
// 그 자리의 링크는 따라가지 않고 EEXIST). 못 잡은 쪽은 기다렸다가 잡고, 그 안에서 떠 있는 서버를 다시 보아 재사용한다.
|
|
524
|
+
// 그 자리의 링크는 따라가지 않고 EEXIST. Windows 의 wx 는 끊어진 링크를 따라가므로 writeNew 가 먼저 lstat 로 본다). 못 잡은 쪽은 기다렸다가 잡고, 그 안에서 떠 있는 서버를 다시 보아 재사용한다.
|
|
525
525
|
// 낡은 잠금(쥔 프로세스가 죽음, 또는 START_LOCK_STALE_MS 넘게 남음, 링크나 일반 파일 아님)은 넘겨받기 표지
|
|
526
526
|
// server.json.lock.taking 을 wx 로 잡은 쪽만, 그 안에서 다시 판정한 뒤 지우고 새로 만든다.
|
|
527
527
|
const START_LOCK = 'server.json.lock';
|
|
@@ -535,7 +535,7 @@ const LOCK_BUSY = new Set(['EEXIST', 'EPERM', 'EACCES', 'EBUSY']);
|
|
|
535
535
|
// 돌려줌: 만들었으면 null, 아니면 그 까닭 코드. 그 밖 오류는 UsageError.
|
|
536
536
|
function makeLockFile(file) {
|
|
537
537
|
try {
|
|
538
|
-
|
|
538
|
+
writeNew(file, `${process.pid} ${iso(Date.now())}\n`, { mode: 0o600 });
|
|
539
539
|
return null;
|
|
540
540
|
} catch (e) {
|
|
541
541
|
if (LOCK_BUSY.has(e.code)) return e.code;
|
|
@@ -186,7 +186,8 @@ export function stageStates(records) {
|
|
|
186
186
|
// ---------- git ----------
|
|
187
187
|
|
|
188
188
|
const gitCache = new Map();
|
|
189
|
-
|
|
189
|
+
// 홈 폴더에서 부른다(Windows 는 일하는 폴더의 git.exe 를 먼저 찾는다)
|
|
190
|
+
const git = (cwd, args) => execFileSync('git', ['-C', path.resolve(cwd), ...args], { cwd: fs.existsSync(os.homedir()) ? os.homedir() : os.tmpdir(), encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 64 * 1024 * 1024 });
|
|
190
191
|
|
|
191
192
|
const readText = (p) => {
|
|
192
193
|
try {
|
package/skills/remains/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: remains
|
|
3
|
-
description: "남은 버그, 하자, 미검증 항목을 찾아 표로 보여 줌. 테스트박스가 등록돼 있으면 각 OS 에서 시험을 돌려 확인함. 읽기와 시험 실행만 하고 고치지 않음. 'remains'/'남은 버그'/'남은 하자'/'미검증 항목'/'뭐가 남았어' 시 사용."
|
|
3
|
+
description: "남은 버그, 하자, 미검증 항목을 찾아 표로 보여 줌. 찾은 결함과 원인이 같은 자리는 저장소 전체에서 찾음. 테스트박스가 등록돼 있으면 각 OS 에서 시험을 돌려 확인함. 읽기와 시험 실행만 하고 고치지 않음. 'remains'/'남은 버그'/'남은 하자'/'미검증 항목'/'뭐가 남았어' 시 사용."
|
|
4
4
|
argument-hint: "[기준 커밋] [--no-boxes]"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -28,10 +28,34 @@ argument-hint: "[기준 커밋] [--no-boxes]"
|
|
|
28
28
|
|---|---|
|
|
29
29
|
| 이번 대화 | 미검증이나 보류로 남긴 것, 검토의 비차단 지적, 사용자가 미룬 결정, 보고했지만 확인하지 않은 주장 |
|
|
30
30
|
| 저장소 기록 | 인계 문서(예: `.omc/handoffs/`), CHANGELOG `[Unreleased]` 의 `UNVALIDATED` 나 미검증 표기, 문서의 미검증 절 |
|
|
31
|
-
| 코드 표시 | 범위 안에서 바뀐 파일의 `TODO`, `FIXME`, `XXX`, `HACK`, 시험의 `skip`, `only`, `todo
|
|
31
|
+
| 코드 표시 | 범위 안에서 바뀐 파일의 `TODO`, `FIXME`, `XXX`, `HACK`, 시험의 `skip`, `only`, `todo`. 같은 종류는 2-1 에서 저장소 전체로 넓힌다 |
|
|
32
32
|
| git 상태 | 커밋하지 않은 변경, push 하지 않은 커밋, 추적하지 않는 새 파일 |
|
|
33
33
|
| 메모리와 노트 | 있으면 그 안의 남은 지적 |
|
|
34
34
|
|
|
35
|
+
## 2-1. 같은 종류 전체 찾기 (읽기만)
|
|
36
|
+
|
|
37
|
+
지적을 그 자리에만 적으면 같은 원인의 결함이 다른 파일에 남는다. 그러면 고칠 때마다 다음 검토에서 같은 종류가 또 나온다. 그래서 모은 결함 항목(버그와 하자)마다 같은 종류를 저장소 전체에서 찾는다.
|
|
38
|
+
|
|
39
|
+
1. 항목마다 결함 종류를 원인으로 한 문장에 쓴다. 자리 이름으로 쓰지 않는다.
|
|
40
|
+
- 예: "Windows 의 wx 는 끊어진 링크를 따라가므로, 추측 가능한 이름에 새 파일을 만들면 링크 너머에 파일이 생긴다"
|
|
41
|
+
- 예: "경로 문자열로 같은 파일인지 판정하면 대소문자, 8.3 짧은 이름, 링크인 부모 폴더만 다른 같은 파일을 놓친다"
|
|
42
|
+
2. 같은 원인을 내는 코드 꼴을 모두 적는다. API 하나만 보지 않는다.
|
|
43
|
+
- 예: `wx` 만이 아니라 `O_EXCL`, `COPYFILE_EXCL`, `wx` 없이 추측 가능한 임시 이름에 쓰는 것까지
|
|
44
|
+
- 예: `===` 비교만이 아니라 `Map`, `Set` 키, `startsWith`, `path.relative` 의 `..` 판정, `includes` 까지
|
|
45
|
+
3. 범위는 바뀐 파일이 아니라 배포되는 파일 전부다.
|
|
46
|
+
- 같은 파일의 사본(다른 스킬 폴더, 런타임별 폴더)도 각각 센다.
|
|
47
|
+
- 시험과 개발 도구에서 찾은 것은 따로 표시한다.
|
|
48
|
+
4. 찾은 명령과 건수를 확인 범위에 적는다.
|
|
49
|
+
- 찾기가 0건이면 패턴을 의심한다. 더 단순한 패턴이나 다른 도구(`rg`, `git grep`)로 다시 찾는다. 셸의 `grep` 이 다른 도구로 바뀌어 있을 수 있다.
|
|
50
|
+
5. 찾은 자리마다 판정한다.
|
|
51
|
+
- 고칠 것: 결과(거짓 성공, 예외, 경계 우회, 밖 파일 생성 등)를 적는다.
|
|
52
|
+
- 해당 없음: 까닭(방어가 이미 있음, 닿는 길이 없음, 이름을 미리 알 수 없음 등)을 적는다.
|
|
53
|
+
6. 프로젝트에 기록된, 이미 고친 결함도 종류로 삼는다.
|
|
54
|
+
- 출처: CHANGELOG 의 고친 항목, 위키와 메모리의 함정 기록
|
|
55
|
+
- 비슷한 항목은 종류 하나로 묶어 중복을 줄인다.
|
|
56
|
+
- 예전에 한 곳만 고친 종류가 다른 곳에 남았는지 1~5 와 같은 방법으로 본다.
|
|
57
|
+
7. 자리가 많으면 런타임이 지원하는 읽기 전용 하위 에이전트에게 종류별로 나눠 맡겨도 된다. 맡길 때는 결함 종류, 찾을 꼴, 이미 고친 본보기를 함께 준다. 하위 에이전트도 고치지 않는다.
|
|
58
|
+
|
|
35
59
|
## 3. 이 머신에서 시험
|
|
36
60
|
|
|
37
61
|
- 프로젝트의 시험 명령을 찾는다. `package.json` 의 `scripts.test`, CI 워크플로의 시험 단계, `Makefile`, pytest 설정, `*.test.mjs`(node:test) 순으로 본다. 찾은 명령과 그 근거를 확인 범위에 적는다.
|
|
@@ -90,7 +114,7 @@ argument-hint: "[기준 커밋] [--no-boxes]"
|
|
|
90
114
|
| `name` | 박스 이름(영문이나 숫자로 시작, 영문, 숫자, `.`, `_`, `-`) |
|
|
91
115
|
| `os` | `linux`, `darwin`, `win32` 중 하나 |
|
|
92
116
|
| `ssh` | `user@host`. 암호 없이 SSH 키로 접속돼야 한다 |
|
|
93
|
-
| `port`, `identity` | 선택. SSH 포트와 키 파일 경로 |
|
|
117
|
+
| `port`, `identity` | 선택. SSH 포트와 키 파일 경로(상대 경로는 boxes.mjs 를 실행한 폴더 기준) |
|
|
94
118
|
| `path` | 선택. Linux, macOS 박스에서 PATH 앞에 붙일 절대 경로(비대화형 SSH 의 PATH 에 node 가 없을 때) |
|
|
95
119
|
| `runAs` | 선택. root 로 접속하는 Linux 박스에서 시험을 대신 돌릴 계정(예: `nobody`) |
|
|
96
120
|
|
|
@@ -123,5 +147,8 @@ argument-hint: "[기준 커밋] [--no-boxes]"
|
|
|
123
147
|
|
|
124
148
|
- 근거는 다시 확인할 수 있게 쓴다(파일과 줄, 명령, 시험 이름, 로그 경로).
|
|
125
149
|
- 심각도가 높은 것부터 놓는다.
|
|
150
|
+
- 2-1 의 결함 종류는 종류마다 한 행으로 쓴다. 항목 칸에 종류를, 근거 칸에 고칠 자리 수와 해당 없음 수를 적는다.
|
|
151
|
+
- 고칠 자리가 20곳 이하면 표 아래에 `파일:줄 | 판정 | 까닭` 목록을 붙인다.
|
|
152
|
+
- 20곳을 넘으면 자리 목록을 시스템 임시 폴더의 파일에 쓰고 그 경로를 근거에 적는다. 저장소 안에는 쓰지 않는다.
|
|
126
153
|
- 남은 항목이 없으면 표 대신 "남은 항목 없음" 과 확인 범위를 쓴다.
|
|
127
154
|
- 마지막 줄에는 사용자가 고를 다음 행동을 쓴다(예: 고칠 항목 고르기). 고치는 일은 요청을 받은 뒤에 한다.
|
|
@@ -15,9 +15,13 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
15
15
|
import { randomBytes } from "node:crypto";
|
|
16
16
|
import { copyFileSync, existsSync, linkSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { homedir, tmpdir } from "node:os";
|
|
18
|
-
import { join } from "node:path";
|
|
18
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
|
|
21
|
+
// The folder programs start in: the home folder (Windows looks for a program in the working folder first, so a
|
|
22
|
+
// program planted in the folder worked on never runs), or the temp folder when there is no home folder.
|
|
23
|
+
const runDir = () => (existsSync(homedir()) ? homedir() : tmpdir());
|
|
24
|
+
|
|
21
25
|
const OSES = new Set(["linux", "darwin", "win32"]);
|
|
22
26
|
const FIELDS = new Set(["name", "os", "ssh", "port", "identity", "path", "runAs"]);
|
|
23
27
|
const SECRETISH = /pass|secret|token|credential/i;
|
|
@@ -102,7 +106,10 @@ export function loadBoxes(file = configFile()) {
|
|
|
102
106
|
|
|
103
107
|
// ── commands for a box ───────────────────────────────────────────────────────
|
|
104
108
|
|
|
105
|
-
|
|
109
|
+
// ssh and scp start in the home folder, so a relative key path is made absolute against this run's folder first,
|
|
110
|
+
// where it was meant. A path from ~ stays as it is: OpenSSH expands it itself.
|
|
111
|
+
const keyPath = (p) => (/^~([\\/]|$)/.test(p) || isAbsolute(p) ? p : resolve(p));
|
|
112
|
+
const extras = (b, portFlag) => [...(b.port ? [portFlag, String(b.port)] : []), ...(b.identity ? ["-i", keyPath(b.identity)] : [])];
|
|
106
113
|
export const sshBase = (b) => [...BASE, ...extras(b, "-p")];
|
|
107
114
|
export const scpBase = (b) => [...BASE, ...extras(b, "-P")];
|
|
108
115
|
|
|
@@ -256,8 +263,10 @@ export async function runOnBox(b, plan, exec) {
|
|
|
256
263
|
return { ...rest, ms: Date.now() - t0 };
|
|
257
264
|
}
|
|
258
265
|
|
|
266
|
+
// Started from the home folder with git -C: Windows looks for a program in the working folder first, so a git.exe
|
|
267
|
+
// inside the folder worked on never runs.
|
|
259
268
|
function git(cwd, args) {
|
|
260
|
-
const r = spawnSync("git", args, { cwd, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
269
|
+
const r = spawnSync("git", ["-C", resolve(cwd), ...args], { cwd: runDir(), encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
261
270
|
if (r.status !== 0) throw new Error(`git ${args.filter((a) => !a.includes("=")).join(" ")} 실패: ${lastLine(r.stderr) || r.error?.message || ""}`);
|
|
262
271
|
return r.stdout;
|
|
263
272
|
}
|
|
@@ -423,7 +432,8 @@ export async function main(argv, deps = {}) {
|
|
|
423
432
|
// even if a grandchild still holds its pipes. Output past MAX_OUTPUT is dropped.
|
|
424
433
|
export function spawnExec(file, args, { timeoutMs = UPLOAD_MS } = {}) {
|
|
425
434
|
return new Promise((resolve) => {
|
|
426
|
-
|
|
435
|
+
// from the home folder: Windows looks for ssh.exe and scp.exe in the working folder (the repository) first
|
|
436
|
+
const child = spawn(file, args, { cwd: runDir(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
427
437
|
children.add(child);
|
|
428
438
|
const buf = { stdout: "", stderr: "" };
|
|
429
439
|
for (const k of ["stdout", "stderr"]) {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
* folder (a sandbox, a read-only mount, a file another account owns): the settings are unchanged.
|
|
39
39
|
* Every output ends with the line `설정 파일: <path>`.
|
|
40
40
|
*/
|
|
41
|
+
import { randomBytes } from "node:crypto";
|
|
41
42
|
import { accessSync, chmodSync, constants, existsSync, lstatSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
42
43
|
import { homedir } from "node:os";
|
|
43
44
|
import { dirname, join } from "node:path";
|
|
@@ -187,7 +188,7 @@ function writeFresh(file, data, mode) {
|
|
|
187
188
|
if (lstatSync(file, { throwIfNoEntry: false })?.isDirectory()) {
|
|
188
189
|
throw Object.assign(new Error(`쓸 자리에 폴더가 있습니다: ${file}`), { code: "EISDIR" });
|
|
189
190
|
}
|
|
190
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
191
|
+
const tmp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
191
192
|
rmSync(tmp, { force: true });
|
|
192
193
|
try {
|
|
193
194
|
writeFileSync(tmp, data, { flag: "wx", mode });
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - 파일이 링크이거나 일반 파일이 아니면 고치지 않는다. 쓰기 전 원본을 백업(이 계정만 읽음)하고, 새 파일에 쓴 뒤
|
|
10
10
|
* 바꿔 넣으며 파일 모드를 지킨다.
|
|
11
11
|
*/
|
|
12
|
+
import crypto from 'node:crypto';
|
|
12
13
|
import fs from 'node:fs';
|
|
13
14
|
import os from 'node:os';
|
|
14
15
|
import path from 'node:path';
|
|
@@ -34,15 +35,19 @@ export function readToml(file) {
|
|
|
34
35
|
return { text: fs.readFileSync(file, 'utf8'), mode: st.mode & 0o7777, exists: true };
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
// 원본을 backup 에 복사한 뒤(원본이 있을 때) 새 파일에 써서 바꿔 넣는다.
|
|
38
|
+
// 원본을 backup 에 복사한 뒤(원본이 있을 때) 새 파일에 써서 바꿔 넣는다. 백업도 같은 방식이라 백업 자리의 링크는
|
|
39
|
+
// 따라가지 않고 바뀐다.
|
|
38
40
|
export function writeToml(file, text, { mode = null, backup = null } = {}) {
|
|
39
|
-
if (backup && fs.existsSync(file))
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
if (backup && fs.existsSync(file)) replaceFile(backup, fs.readFileSync(file), 0o600); // 원본에 비밀 값이 있을 수 있다
|
|
42
|
+
replaceFile(file, text, mode);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 난수 이름의 새 파일에 쓴 뒤 file 자리로 바꿔 넣는다. 이름을 미리 맞힐 수 없어, 그 자리에 심어 둔 링크를 wx 가
|
|
46
|
+
// 따라가는 일(Windows 는 끊어진 링크를 따라가 대상 파일을 만든다)이 없다. 실패하면 새 파일을 지운다.
|
|
47
|
+
function replaceFile(file, data, mode) {
|
|
48
|
+
const tmp = `${file}.${crypto.randomBytes(8).toString('hex')}.tmp`;
|
|
44
49
|
try {
|
|
45
|
-
fs.writeFileSync(tmp,
|
|
50
|
+
fs.writeFileSync(tmp, data, { flag: 'wx', mode: mode ?? 0o600 });
|
|
46
51
|
if (mode !== null) fs.chmodSync(tmp, mode);
|
|
47
52
|
fs.renameSync(tmp, file);
|
|
48
53
|
} catch (e) {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* changes the exit code. APPLY_HUD_CHECK_TIMEOUT_MS (tests) shortens its 25s limit.
|
|
35
35
|
* Exit 1 means nothing changed; exit 2 means an unexpected error stopped it partway.
|
|
36
36
|
*/
|
|
37
|
+
import { randomBytes } from "node:crypto";
|
|
37
38
|
import { spawnSync } from "node:child_process";
|
|
38
39
|
import {
|
|
39
40
|
accessSync,
|
|
@@ -405,7 +406,7 @@ function writeSettings(file, text) {
|
|
|
405
406
|
} catch {
|
|
406
407
|
if (isLink(file)) return writeFileSync(file, text);
|
|
407
408
|
}
|
|
408
|
-
const tmp = `${target}.${process.pid}.tmp`;
|
|
409
|
+
const tmp = `${target}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
409
410
|
try {
|
|
410
411
|
rmSync(tmp, { force: true });
|
|
411
412
|
writeFileSync(tmp, text, { flag: "wx", mode });
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - 파일이 링크이거나 일반 파일이 아니면 고치지 않는다. 쓰기 전 원본을 백업(이 계정만 읽음)하고, 새 파일에 쓴 뒤
|
|
10
10
|
* 바꿔 넣으며 파일 모드를 지킨다.
|
|
11
11
|
*/
|
|
12
|
+
import crypto from 'node:crypto';
|
|
12
13
|
import fs from 'node:fs';
|
|
13
14
|
import os from 'node:os';
|
|
14
15
|
import path from 'node:path';
|
|
@@ -34,15 +35,19 @@ export function readToml(file) {
|
|
|
34
35
|
return { text: fs.readFileSync(file, 'utf8'), mode: st.mode & 0o7777, exists: true };
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
// 원본을 backup 에 복사한 뒤(원본이 있을 때) 새 파일에 써서 바꿔 넣는다.
|
|
38
|
+
// 원본을 backup 에 복사한 뒤(원본이 있을 때) 새 파일에 써서 바꿔 넣는다. 백업도 같은 방식이라 백업 자리의 링크는
|
|
39
|
+
// 따라가지 않고 바뀐다.
|
|
38
40
|
export function writeToml(file, text, { mode = null, backup = null } = {}) {
|
|
39
|
-
if (backup && fs.existsSync(file))
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
if (backup && fs.existsSync(file)) replaceFile(backup, fs.readFileSync(file), 0o600); // 원본에 비밀 값이 있을 수 있다
|
|
42
|
+
replaceFile(file, text, mode);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 난수 이름의 새 파일에 쓴 뒤 file 자리로 바꿔 넣는다. 이름을 미리 맞힐 수 없어, 그 자리에 심어 둔 링크를 wx 가
|
|
46
|
+
// 따라가는 일(Windows 는 끊어진 링크를 따라가 대상 파일을 만든다)이 없다. 실패하면 새 파일을 지운다.
|
|
47
|
+
function replaceFile(file, data, mode) {
|
|
48
|
+
const tmp = `${file}.${crypto.randomBytes(8).toString('hex')}.tmp`;
|
|
44
49
|
try {
|
|
45
|
-
fs.writeFileSync(tmp,
|
|
50
|
+
fs.writeFileSync(tmp, data, { flag: 'wx', mode: mode ?? 0o600 });
|
|
46
51
|
if (mode !== null) fs.chmodSync(tmp, mode);
|
|
47
52
|
fs.renameSync(tmp, file);
|
|
48
53
|
} catch (e) {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* `node --check`. Exit status 1 means nothing was changed; 2 means an unexpected
|
|
26
26
|
* error stopped the command partway, so check `status`.
|
|
27
27
|
*/
|
|
28
|
+
import { randomBytes } from "node:crypto";
|
|
28
29
|
import { spawnSync } from "node:child_process";
|
|
29
30
|
import { accessSync, constants, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
30
31
|
import { homedir, tmpdir } from "node:os";
|
|
@@ -139,7 +140,7 @@ function insertBlock(base) {
|
|
|
139
140
|
// either name is replaced rather than written through, and a write that fails
|
|
140
141
|
// leaves the old file as it was.
|
|
141
142
|
function writeFresh(file, data) {
|
|
142
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
143
|
+
const tmp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
143
144
|
rmSync(tmp, { force: true });
|
|
144
145
|
try {
|
|
145
146
|
writeFileSync(tmp, data, { flag: "wx" });
|