@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,27 +1,29 @@
|
|
|
1
|
-
// pm-adr.mjs: ADR
|
|
1
|
+
// pm-adr.mjs: ADR 탭의 검사. adr.json 두 층 스키마 검사, 참조 검증과 gap 정리, docs/adr 갈라짐 경고, 밀도, 집계, check 대조 기록.
|
|
2
|
+
// 화면 렌더는 pm-adr-view.mjs.
|
|
2
3
|
// 두 층: project(검증, 흐름, 증거)는 프로젝트에 하나, decisions[](최적화 로직, 확장 대응, 포기와 얻은 것)는 결정마다.
|
|
3
4
|
// 서버 화면은 참조를 글로만 보이고 파일을 읽거나 링크하지 않는다. 참조 파일은 check 에서만 크기 상한 안의 일반 파일로 읽는다.
|
|
4
5
|
// 스키마 위반은 FAIL, 근거가 없어 비운 항목({ gap: 사유 })은 밀도 WARN, 확인 못 한 참조는 FAIL 이나 WARN(references/adr.md).
|
|
6
|
+
import crypto from 'node:crypto';
|
|
5
7
|
import fs from 'node:fs';
|
|
6
8
|
import path from 'node:path';
|
|
7
|
-
import { git, noBom, readRegular, writeAtomic } from './pm-config.mjs';
|
|
8
|
-
import { esc } from './pm-html.mjs';
|
|
9
|
+
import { git, noBom, readRegular, writeAtomic, writeNew } from './pm-config.mjs';
|
|
9
10
|
|
|
10
11
|
const ADR_FILE = 'adr.json';
|
|
12
|
+
const ADR_CHECK_FILE = 'adr-check.json';
|
|
11
13
|
const REF_READ_MAX = 4 * 1024 * 1024;
|
|
12
|
-
const STATUS = { accepted: '채택', proposed: '제안', superseded: '대체됨' };
|
|
14
|
+
export const STATUS = { accepted: '채택', proposed: '제안', superseded: '대체됨' };
|
|
13
15
|
const REF_KINDS = ['file', 'test', 'commit', 'cmd'];
|
|
14
16
|
const HASH = /^[0-9a-f]{4,40}$/i;
|
|
15
|
-
const DECISION_SLOTS = [
|
|
17
|
+
export const DECISION_SLOTS = [
|
|
16
18
|
['optimization', '최적화 로직'],
|
|
17
19
|
['scaling', '확장 대응'],
|
|
18
20
|
['tradeoffs', '포기와 얻은 것'],
|
|
19
21
|
];
|
|
20
22
|
|
|
21
|
-
const arr = (v) => (Array.isArray(v) ? v : []);
|
|
22
|
-
const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : null);
|
|
23
|
-
const str = (v) => typeof v === 'string' && v.trim().length > 0;
|
|
24
|
-
const isGap = (v) => Boolean(obj(v)) && Object.hasOwn(v, 'gap');
|
|
23
|
+
export const arr = (v) => (Array.isArray(v) ? v : []);
|
|
24
|
+
export const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : null);
|
|
25
|
+
export const str = (v) => typeof v === 'string' && v.trim().length > 0;
|
|
26
|
+
export const isGap = (v) => Boolean(obj(v)) && Object.hasOwn(v, 'gap');
|
|
25
27
|
// json 경로 조각(['decisions', 0, 'scaling']) 을 글(decisions[0].scaling)로.
|
|
26
28
|
const jp = (parts) => parts.reduce((a, k) => (typeof k === 'number' ? `${a}[${k}]` : a ? `${a}.${k}` : k), '') || '$';
|
|
27
29
|
|
|
@@ -338,7 +340,10 @@ function divergence(ctx, v) {
|
|
|
338
340
|
return names.sort().filter((n) => !pointed.has(`docs/adr/${n}`)).map((n) => `docs/adr/${n} 를 가리키는 결정 없음`);
|
|
339
341
|
}
|
|
340
342
|
|
|
341
|
-
// adr.json
|
|
343
|
+
// adr.json 글(BOM 을 뗀 뒤)의 sha256. check 대조 기록과 화면이 같은 adr.json 을 보았는지 가린다.
|
|
344
|
+
export const adrSha = (text) => crypto.createHash('sha256').update(text, 'utf8').digest('hex');
|
|
345
|
+
|
|
346
|
+
// adr.json 읽기. 돌려줌: { missing } | { error } | { value, sha }
|
|
342
347
|
function readAdr(dataDir) {
|
|
343
348
|
let text;
|
|
344
349
|
try {
|
|
@@ -346,32 +351,43 @@ function readAdr(dataDir) {
|
|
|
346
351
|
} catch (e) {
|
|
347
352
|
return e.code === 'ENOENT' ? { missing: true } : { error: `읽기 실패(${e.code || 'error'})` };
|
|
348
353
|
}
|
|
354
|
+
const body = noBom(text);
|
|
349
355
|
try {
|
|
350
|
-
return { value: JSON.parse(
|
|
356
|
+
return { value: JSON.parse(body), sha: adrSha(body) };
|
|
351
357
|
} catch (e) {
|
|
352
358
|
return { error: `JSON 아님: ${e.message}` };
|
|
353
359
|
}
|
|
354
360
|
}
|
|
355
361
|
|
|
356
|
-
// 검사 전체(check 와 정리가 같이 씀). 돌려줌: { missing } | { error } | { value, S, diverge }
|
|
362
|
+
// 검사 전체(check 와 정리가 같이 씀). 돌려줌: { missing } | { error } | { value, sha, S, diverge }
|
|
357
363
|
function inspectAdr(ctx, config) {
|
|
358
364
|
const got = readAdr(ctx.dataDir);
|
|
359
365
|
if (!Object.hasOwn(got, 'value')) return got;
|
|
360
366
|
const S = examine(got.value, ctx, config);
|
|
361
367
|
const diverge = S.schema.length ? [] : divergence(ctx, got.value);
|
|
362
|
-
return { value: got.value, S, diverge };
|
|
368
|
+
return { value: got.value, sha: got.sha, S, diverge };
|
|
363
369
|
}
|
|
364
370
|
|
|
365
|
-
// check 의 ADR 부분. r: pm-check 의 sink. 돌려줌: { value: 밀도에 쓸 값, bare: 참조 없는 서술
|
|
371
|
+
// check 의 ADR 부분. r: pm-check 의 sink. 돌려줌: { value: 밀도에 쓸 값, bare: 참조 없는 서술 칸, sha: adr.json sha256 }.
|
|
372
|
+
// adr.json 이 없거나 깨졌으면 { value: null, bare: [], sha: null }.
|
|
373
|
+
const NO_ADR = () => ({ value: null, bare: [], sha: null });
|
|
366
374
|
export function checkAdr(ctx, config, r) {
|
|
367
375
|
const got = inspectAdr(ctx, config);
|
|
368
|
-
if (got.missing) return
|
|
369
|
-
if (got.error) return r.fail(ADR_FILE, '$', got.error),
|
|
376
|
+
if (got.missing) return NO_ADR();
|
|
377
|
+
if (got.error) return r.fail(ADR_FILE, '$', got.error), NO_ADR();
|
|
370
378
|
for (const x of [...got.S.schema, ...got.S.claim]) r.fail(ADR_FILE, x.p, x.msg);
|
|
371
379
|
for (const x of got.S.refFail) r.fail(ADR_FILE, jp(x.parts), `참조: ${x.msg}`);
|
|
372
380
|
for (const x of got.S.refWarn) r.warn(ADR_FILE, jp(x.parts), `참조: ${x.msg}`);
|
|
373
381
|
for (const msg of got.diverge) r.warn(ADR_FILE, 'sources:', msg);
|
|
374
|
-
return { value: got.value, bare: bareSlots(got.value, (p) => got.S.good.has(p)) };
|
|
382
|
+
return { value: got.value, bare: bareSlots(got.value, (p) => got.S.good.has(p)), sha: got.sha };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// check 대조 기록(adr-check.json): check 가 확인을 마친 참조로 센 집계 글과 그때 adr.json 의 sha256.
|
|
386
|
+
// 화면은 파일을 읽지 않고 적힌 참조로 세므로, 이 기록과 대조해 두 수가 다르거나 check 뒤 adr.json 이 바뀌었음을 알린다.
|
|
387
|
+
// rec: { sha, tally }. 쓰기 실패는 UsageError(writeAtomic).
|
|
388
|
+
export async function writeAdrCheck(dataDir, rec, now = Date.now()) {
|
|
389
|
+
const body = { sha: rec.sha, tally: rec.tally, at: new Date(now).toISOString() };
|
|
390
|
+
await writeAtomic(path.join(dataDir, ADR_CHECK_FILE), `${JSON.stringify(body, null, 2)}\n`);
|
|
375
391
|
}
|
|
376
392
|
|
|
377
393
|
// json 경로 조각 자리에 값을 넣는다.
|
|
@@ -388,13 +404,16 @@ const stamp = (ms) => {
|
|
|
388
404
|
};
|
|
389
405
|
|
|
390
406
|
// 스키마 FAIL 이 남은 adr.json 을 같은 폴더의 adr.json.rejected-<시각>(있으면 -2, -3 ...)으로 옮긴다. 결정 기록을 잃지 않게
|
|
391
|
-
// 지우지 않는다. 새 이름은
|
|
407
|
+
// 지우지 않는다. 새 이름은 writeNew 로 만들어 있는 것(끊어진 링크 포함)을 덮거나 따라가지 않고, 사본을 만든 뒤에만 원본을
|
|
408
|
+
// 지운다. 돌려줌: 옮긴 경로.
|
|
392
409
|
function reject(file, now) {
|
|
393
410
|
const base = `${file}.rejected-${stamp(now)}`;
|
|
411
|
+
const bytes = fs.readFileSync(file);
|
|
412
|
+
const mode = fs.statSync(file).mode & 0o777; // as COPYFILE_EXCL kept it
|
|
394
413
|
for (let i = 1; ; i++) {
|
|
395
414
|
const dest = i === 1 ? base : `${base}-${i}`;
|
|
396
415
|
try {
|
|
397
|
-
|
|
416
|
+
writeNew(dest, bytes, { mode });
|
|
398
417
|
} catch (e) {
|
|
399
418
|
if (e.code === 'EEXIST') continue;
|
|
400
419
|
throw e;
|
|
@@ -497,89 +516,3 @@ export function adrTally(v, bare = bareSlots(v)) {
|
|
|
497
516
|
const gaps = gapCount(v);
|
|
498
517
|
return { text: `근거 없음 ${gaps}칸, 미검증 ${unverified}, 구현 못 함 ${notImplemented}, 참조 없는 서술 ${bare.length}`, any: gaps + unverified + notImplemented + bare.length > 0 };
|
|
499
518
|
}
|
|
500
|
-
|
|
501
|
-
// ---------- 렌더 ----------
|
|
502
|
-
|
|
503
|
-
const gapHtml = (v) => `<span class="b miss">근거 없음: ${esc(v.gap)}</span>`;
|
|
504
|
-
|
|
505
|
-
// 참조 한 줄 글. 파일을 읽거나 링크하지 않는다.
|
|
506
|
-
export function refText(x) {
|
|
507
|
-
if (!obj(x)) return '';
|
|
508
|
-
if (isGap(x)) return gapHtml(x);
|
|
509
|
-
const note = typeof x.note === 'string' && x.note ? ` <small>${esc(x.note)}</small>` : '';
|
|
510
|
-
const line = Number.isInteger(x.line) ? `:${x.line}` : '';
|
|
511
|
-
const out = {
|
|
512
|
-
file: () => `파일 <code>${esc(x.path)}${line}</code>`,
|
|
513
|
-
test: () => `시험 <code>${esc(x.path)}</code> 의 ${esc(x.name)}`,
|
|
514
|
-
commit: () => `커밋 <code>${esc(String(x.hash || '').slice(0, 7))}</code>`,
|
|
515
|
-
cmd: () => `명령 <code>${esc(x.cmd)}</code>, 출력 ${x.output ? `<code>${esc(x.output)}</code>` : '없음'}`,
|
|
516
|
-
}[x.kind];
|
|
517
|
-
return out ? out() + note : esc(JSON.stringify(x)) + note;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const refsHtml = (list) => arr(list).map(refText).filter(Boolean).join('<br>') || '<span class="n">참조 없음</span>';
|
|
521
|
-
const table = (head, rows) => `<table><thead><tr>${head.map((h) => `<th>${esc(h)}</th>`).join('')}</tr></thead><tbody>${rows.join('') || `<tr><td colspan="${head.length}" class="n">없음</td></tr>`}</tbody></table>`;
|
|
522
|
-
const td = (h, cls = '') => `<td${cls ? ` class="${cls}"` : ''}>${h}</td>`;
|
|
523
|
-
const sec = (title, body) => `<h3>${esc(title)}</h3>${body}`;
|
|
524
|
-
// 칸이 gap 이면 근거 없음, 아니면 body(값).
|
|
525
|
-
const slotHtml = (v, body) => (isGap(v) ? `<div>${gapHtml(v)}</div>` : obj(v) || Array.isArray(v) ? body(v) : `<div>${gapHtml({ gap: '칸 없음' })}</div>`);
|
|
526
|
-
|
|
527
|
-
const evidenceHtml = (x) => (x.status === 'unverified' ? `<span class="b unv">미검증: ${esc(x.why)}</span>${arr(x.evidence).length ? `<br>${refsHtml(x.evidence)}` : ''}` : refsHtml(x.evidence));
|
|
528
|
-
|
|
529
|
-
function verificationHtml(v) {
|
|
530
|
-
return table(['id', '단위 기능', '결과', 'evidence'], arr(v).filter(obj).map((x) => `<tr>${td(esc(x.id), 'h')}${td(esc(x.module))}${td(esc(x.result || '-'))}${td(evidenceHtml(x), 'n')}</tr>`));
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
function flowHtml(f) {
|
|
534
|
-
const rows = [['입력', f.input], ['처리', f.logic], ['출력', f.output]].map(([k, t]) => `<tr>${td(k, 't')}${td(str(t) ? esc(t) : gapHtml({ gap: '빈 칸' }))}</tr>`);
|
|
535
|
-
return `${table(['구분', '내용'], rows)}<div class="meta">참조: ${refsHtml(f.refs)}</div>`;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
function proofHtml(p) {
|
|
539
|
-
const impl = arr(p.implemented).filter(obj).map((x) => `<tr>${td(esc(x.feature))}${td(esc(arr(x.modules).join(', ')), 'h')}${td(refsHtml(x.proofs), 'n')}</tr>`);
|
|
540
|
-
const not = arr(p.notImplemented).filter(obj).map((x) => `<tr>${td(esc(x.feature))}${td(esc(x.why))}</tr>`);
|
|
541
|
-
return `<div class="meta">구현된 기능과 증명 위치</div>${table(['구현된 기능', '모듈', '증명 위치'], impl)}<div class="meta">구현 못 한 기능</div>${table(['구현 못 한 기능', '사유'], not)}`;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
const filled = (d) => DECISION_SLOTS.filter(([k]) => obj(d[k]) && !isGap(d[k])).length;
|
|
545
|
-
const emptyNames = (d) => DECISION_SLOTS.filter(([k]) => !(obj(d[k]) && !isGap(d[k]))).map(([, n]) => n);
|
|
546
|
-
|
|
547
|
-
function decisionRow(d) {
|
|
548
|
-
const miss = emptyNames(d);
|
|
549
|
-
const st = STATUS[d.status] || d.status;
|
|
550
|
-
return `<tr>${td(esc(d.id), 'h')}${td(esc(d.title))}${td(esc(st))}${td(esc(d.date || '-'), 't')}${td(`${filled(d)}/3`, 'd')}${td(miss.length ? `<span class="b miss">${esc(miss.join(', '))}</span>` : '-', 'n')}</tr>`;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
function tradeoffsHtml(t) {
|
|
554
|
-
const gave = arr(t.gaveUp).filter(obj).map((x) => `<tr>${td(esc(x.what))}${td(esc(x.why))}</tr>`);
|
|
555
|
-
const got = arr(t.gained).filter(obj).map((x) => `<tr>${td(esc(x.what))}${td(esc(x.purpose))}</tr>`);
|
|
556
|
-
return `${table(['포기한 것', '이유'], gave)}${table(['얻은 것', '목적'], got)}`;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function decisionHtml(d) {
|
|
560
|
-
const opts = arr(d.options).filter(obj).map((x) => `<tr>${td(esc(x.name))}${td(esc(x.pros))}${td(esc(x.cons))}</tr>`);
|
|
561
|
-
const rejected = str(d.rejected) ? `<div class="meta">버린 선택지: ${esc(d.rejected)}</div>` : '';
|
|
562
|
-
const optim = (o) => `<div>${esc(o.process)}</div><div class="meta">참조한 모듈: ${esc(arr(o.modules).join(', ') || '없음')}</div><div class="meta">참조: ${refsHtml(o.refs)}</div>`;
|
|
563
|
-
const scale = (s) => `<div>${esc(s.argument)}</div>${str(s.limits) ? `<div class="meta">한계: ${esc(s.limits)}</div>` : ''}<div class="meta">참조: ${refsHtml(s.refs)}</div>`;
|
|
564
|
-
return `<h2>${esc(d.id)} ${esc(d.title)} <small>${esc(STATUS[d.status] || d.status)}</small></h2>
|
|
565
|
-
<div class="meta">맥락: ${esc(d.context)}</div>${table(['선택지', '장점', '단점'], opts)}${rejected}
|
|
566
|
-
<div><b>결론</b>: ${esc(d.decision)}</div><div class="meta">이유: ${esc(d.why)}</div>
|
|
567
|
-
${sec('최적화 로직', slotHtml(d.optimization, optim))}${sec('확장 대응', slotHtml(d.scaling, scale))}${sec('포기와 얻은 것', slotHtml(d.tradeoffs, tradeoffsHtml))}
|
|
568
|
-
<div class="meta">근거 출처: ${refsHtml(d.sources)}</div>`;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// ADR 탭 본문. 자료가 없거나 깨졌으면 다른 탭처럼 안내와 스키마 문서.
|
|
572
|
-
export function adrBody(m, emptyNotice) {
|
|
573
|
-
const v = m.adr && obj(m.adr.value);
|
|
574
|
-
if (!v) return emptyNotice('references/adr.md');
|
|
575
|
-
const p = obj(v.project) || {};
|
|
576
|
-
const ds = arr(v.decisions).filter(obj);
|
|
577
|
-
const tally = adrTally(v);
|
|
578
|
-
return `<div class="meta">의사 결정 기록. 위는 프로젝트 전체(검증, 흐름, 증거), 아래는 결정마다(최적화 로직, 확장 대응, 포기와 얻은 것). 자료: <code>adr.json</code>. 참조는 위치만 보이고 이 화면은 파일을 열지 않음. 참조 확인은 check.</div>
|
|
579
|
-
<div class="meta"><span class="b ${tally.any ? 'miss' : 'done'}">${esc(tally.text)}</span></div>
|
|
580
|
-
<h2>프로젝트 요약</h2>
|
|
581
|
-
${sec('검증', slotHtml(p.verification, verificationHtml))}${sec('흐름', slotHtml(p.flow, flowHtml))}${sec('증거', slotHtml(p.proof, proofHtml))}
|
|
582
|
-
<h2>결정 목록 <small>${ds.length}건</small></h2>
|
|
583
|
-
${table(['id', '제목', '상태', '날짜', '결정별 3항목', '빈 항목'], ds.map(decisionRow))}
|
|
584
|
-
${ds.map(decisionHtml).join('\n')}`;
|
|
585
|
-
}
|
|
@@ -14,7 +14,7 @@ import { ITEM_STATUS, STAGE_STATUS } from './pm-data.mjs';
|
|
|
14
14
|
import { archLayout, archFlaws, flowLayout, nameFits, FLOW } from './pm-render.mjs';
|
|
15
15
|
import { color, finite, textW, wrap, TABS } from './pm-html.mjs';
|
|
16
16
|
import { memoryWarnings } from './pm-memory.mjs';
|
|
17
|
-
import { checkAdr, adrRows, cleanAdr, adrTally } from './pm-adr.mjs';
|
|
17
|
+
import { checkAdr, adrRows, cleanAdr, adrTally, writeAdrCheck } from './pm-adr.mjs';
|
|
18
18
|
|
|
19
19
|
const arr = (v) => (Array.isArray(v) ? v : []);
|
|
20
20
|
const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : null);
|
|
@@ -531,7 +531,7 @@ function memoryInto(r, memory, project, env) {
|
|
|
531
531
|
}
|
|
532
532
|
}
|
|
533
533
|
|
|
534
|
-
// 검사 전체. 돌려줌: { lines: [출력 줄], fails }
|
|
534
|
+
// 검사 전체. 자료를 바꾸지 않는다. 돌려줌: { lines: [출력 줄], fails, adr: check 대조 기록 { sha, tally } (adr.json 이 없거나 깨졌으면 null) }
|
|
535
535
|
export function runCheck(ctx, env = process.env) {
|
|
536
536
|
const r = sink();
|
|
537
537
|
const config = checkConfig(ctx, r);
|
|
@@ -543,8 +543,8 @@ export function runCheck(ctx, env = process.env) {
|
|
|
543
543
|
const flow = checkFlow(ctx.dataDir, r);
|
|
544
544
|
const arch = checkArch(ctx.dataDir, r, config.text.plannedPrefix);
|
|
545
545
|
const got = checkAdr(ctx, config, r);
|
|
546
|
-
const adr = got
|
|
547
|
-
const adrBare = got
|
|
546
|
+
const adr = got.value;
|
|
547
|
+
const adrBare = got.bare;
|
|
548
548
|
const inv = checkInventory(ctx, r);
|
|
549
549
|
const memory = memoryOf(ctx);
|
|
550
550
|
memoryInto(r, memory, ctx.project, env);
|
|
@@ -553,7 +553,8 @@ export function runCheck(ctx, env = process.env) {
|
|
|
553
553
|
// 밀도는 init 기록이나 inventory.json 이 있을 때만. 다만 ADR 밀도는 adr.json 이 있으면 늘 낸다
|
|
554
554
|
const dens = inv || memory ? density({ arch, flow, req, items: items.items, inv, storyCount: ids.length, adr, adrBare }) : null;
|
|
555
555
|
const rows = dens ? dens.rows : adr ? adrRows(adr, adrBare) : [];
|
|
556
|
-
|
|
556
|
+
const tally = adrTally(adr, adrBare).text;
|
|
557
|
+
return { ...finish(out, rows, dens, tally), adr: got.sha ? { sha: got.sha, tally } : null };
|
|
557
558
|
}
|
|
558
559
|
|
|
559
560
|
// 밀도 미달 줄, 합계 줄, 탭별 밀도 요약, ADR 집계(근거 없음 칸, 미검증, 구현 못 함, 참조 없는 서술. 늘 한 줄)를 붙인다. 돌려줌: { lines, fails }
|
|
@@ -566,12 +567,20 @@ function finish(out, rows, dens, tally) {
|
|
|
566
567
|
return { lines: out, fails };
|
|
567
568
|
}
|
|
568
569
|
|
|
569
|
-
// --adr-cleanup: 검사 전에 init 정리 단계(pm-adr.mjs cleanAdr)를 적용한다. 그 밖에는 자료를 바꾸지
|
|
570
|
+
// --adr-cleanup: 검사 전에 init 정리 단계(pm-adr.mjs cleanAdr)를 적용한다. 그 밖에는 자료를 바꾸지 않고,
|
|
571
|
+
// 화면 대조용 adr-check.json 만 쓴다. 쓰지 못하면 stderr 경고(검사 결과와 종료 코드는 그대로).
|
|
570
572
|
export async function command(cmd, ctx, o, io) {
|
|
571
573
|
if (cmd !== 'check') throw new UsageError(`${cmd} 는 pm-check.mjs 가 맡지 않음`);
|
|
572
574
|
if (o._.length > 1) throw new UsageError(`check 는 위치 인자를 받지 않음: ${o._.slice(1).join(' ')}`);
|
|
573
575
|
if (o['adr-cleanup'] && ctx.exists) for (const l of await cleanAdr(ctx, loadConfig(ctx).config)) io.out(l);
|
|
574
|
-
const { lines, fails } = runCheck(ctx);
|
|
576
|
+
const { lines, fails, adr } = runCheck(ctx);
|
|
575
577
|
for (const l of lines) io.out(l);
|
|
578
|
+
if (adr) {
|
|
579
|
+
try {
|
|
580
|
+
await writeAdrCheck(ctx.dataDir, adr);
|
|
581
|
+
} catch (e) {
|
|
582
|
+
io.err(`경고: ${e.message}. 화면의 check 대조가 낡을 수 있음`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
576
585
|
return fails ? 1 : 0;
|
|
577
586
|
}
|
|
@@ -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
|
}
|
|
@@ -6,7 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import { esc, color, finite, textW, wrap, FAVICON, CSS, TABS, tabsHtml } from './pm-html.mjs';
|
|
7
7
|
import { configOrFail, loadConfig, UsageError, DEFAULT_REQ_RULE, STORYLESS_REQ_RULE } from './pm-config.mjs';
|
|
8
8
|
import { collect, parseTime, agentJudge } from './pm-sources.mjs';
|
|
9
|
-
import { adrBody } from './pm-adr.mjs';
|
|
9
|
+
import { adrBody } from './pm-adr-view.mjs';
|
|
10
10
|
|
|
11
11
|
const two = (n) => String(n).padStart(2, '0');
|
|
12
12
|
const hm = (ms) => (ms == null ? '-' : Math.floor(ms / 3600e3) + ':' + two(Math.floor((ms % 3600e3) / 60000)));
|