@kaydash9999/banker-plugins 0.23.3 → 0.24.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 +39 -0
- package/README.md +14 -6
- package/codex/manifest.json +6 -2
- package/codex/transform-matrix.md +6 -2
- package/package.json +1 -1
- package/skills/build-testbox/SKILL.md +213 -0
- package/skills/build-testbox/scripts/args.mjs +156 -0
- package/skills/build-testbox/scripts/browser-cell.mjs +181 -0
- package/skills/build-testbox/scripts/browsers.mjs +164 -0
- package/skills/build-testbox/scripts/containers.mjs +237 -0
- package/skills/build-testbox/scripts/detect.mjs +166 -0
- package/skills/build-testbox/scripts/proc.mjs +91 -0
- package/skills/build-testbox/scripts/report.mjs +109 -0
- package/skills/build-testbox/scripts/setup.mjs +144 -0
- package/skills/build-testbox/scripts/testbox.mjs +148 -0
- package/skills/git-init/SKILL.md +152 -0
- package/skills/git-init/scripts/git-init.mjs +440 -0
- package/skills/git-init/scripts/marker-block.mjs +311 -0
- package/skills/git-init/scripts/payload-scan.mjs +186 -0
- package/skills/init-dir/SKILL.md +172 -0
- package/skills/init-dir/scripts/init-dir.mjs +308 -0
- package/skills/init-dir/scripts/marker-block.mjs +311 -0
- package/skills/init-dir/scripts/templates.mjs +100 -0
- package/skills/init-dir/templates/KB/README.md +15 -0
- package/skills/init-dir/templates/docs/README.md +14 -0
- package/skills/init-dir/templates/logs/README.md +14 -0
- package/skills/init-dir/templates/logs/gitignore +3 -0
- package/skills/init-dir/templates/mockup/README.md +7 -0
- package/skills/init-dir/templates/scripts/README.md +36 -0
- package/skills/init-dir/templates/scripts/lib/service.mjs +540 -0
- package/skills/init-dir/templates/scripts/restart.cmd +13 -0
- package/skills/init-dir/templates/scripts/restart.sh +10 -0
- package/skills/init-dir/templates/scripts/service.conf +5 -0
- package/skills/init-dir/templates/scripts/start.cmd +13 -0
- package/skills/init-dir/templates/scripts/start.sh +10 -0
- package/skills/init-dir/templates/scripts/status.cmd +13 -0
- package/skills/init-dir/templates/scripts/status.sh +10 -0
- package/skills/init-dir/templates/scripts/stop.cmd +13 -0
- package/skills/init-dir/templates/scripts/stop.sh +10 -0
- package/skills/init-dir/templates/tests/README.md +15 -0
- package/skills/init-dir/templates/tests/run.cmd +13 -0
- package/skills/init-dir/templates/tests/run.mjs +160 -0
- package/skills/init-dir/templates/tests/run.sh +10 -0
- package/skills/init-dir/templates/tests/tests.conf +7 -0
- package/skills/init-dir/templates/work/README.md +8 -0
- package/skills/progress-mon/README.md +10 -8
- package/skills/progress-mon/SKILL.md +53 -16
- package/skills/progress-mon/assets/fixture/README.md +1 -1
- package/skills/progress-mon/assets/fixture/adr.json +35 -0
- package/skills/progress-mon/assets/fixture/arch.json +1 -1
- package/skills/progress-mon/assets/fixture/flow.json +2 -2
- package/skills/progress-mon/assets/fixture/variants/arch-auto/arch.json +1 -1
- package/skills/progress-mon/assets/fixture/variants/legacy-init/rules.json +6 -0
- package/skills/progress-mon/assets/fixture/variants/legacy-init/state.json +5 -0
- package/skills/progress-mon/references/adr.md +177 -0
- package/skills/progress-mon/references/authoring.md +13 -5
- package/skills/progress-mon/scripts/pm-adr.mjs +585 -0
- package/skills/progress-mon/scripts/pm-check.mjs +48 -20
- package/skills/progress-mon/scripts/pm-data.mjs +1 -1
- package/skills/progress-mon/scripts/pm-html.mjs +1 -0
- package/skills/progress-mon/scripts/pm-memory.mjs +113 -28
- package/skills/progress-mon/scripts/pm-render.mjs +6 -4
- package/skills/progress-mon/scripts/pm-server.mjs +2 -1
- package/skills/progress-mon/scripts/pm-sources.mjs +1 -0
- package/skills/progress-mon/scripts/progress-mon.mjs +3 -2
- package/skills/tune-tmux/SKILL.md +235 -0
- package/skills/tune-tmux/scripts/app-notes.mjs +38 -0
- package/skills/tune-tmux/scripts/marker-block.mjs +311 -0
- package/skills/tune-tmux/scripts/psmux-caps.json +71 -0
- package/skills/tune-tmux/scripts/tmux-config.mjs +251 -0
- package/skills/tune-tmux/scripts/tune-tmux.mjs +284 -0
- package/skills/ultra-chain/SKILL.md +52 -22
- package/skills/ultra-chain/references/chain-ledger.mjs +93 -11
- package/skills/ultra-chain/references/chain-report.mjs +13 -2
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
// 구성도와 workflow 기하는 렌더와 같은 배치 계산(archLayout, archFlaws, flowLayout)에 검사한다.
|
|
4
4
|
// 밀도(4.9절)는 init 이 만든 자료 폴더(state.json 의 memory)나 inventory.json 이 있을 때만 재고, 미달은 WARN(종료 코드 영향 없음).
|
|
5
5
|
// 항목마다 재는 밀도 미달 줄은 끝에 못 채운 대상 이름을 붙인다(`: a, b 외 n`).
|
|
6
|
-
//
|
|
6
|
+
// ADR(adr.json)의 스키마, 참조, docs/adr 갈라짐은 pm-adr.mjs 가 검사한다.
|
|
7
|
+
// 끝에 `density <탭> ok|low` 5줄(init 기록이 있을 때)과 늘 `adr 근거 없음 n칸, 미검증 n, 구현 못 함 n, 참조 없는 서술 n` 한 줄. ADR 밀도는 adr.json 이 있으면 늘 잰다.
|
|
8
|
+
// 형식 시험 자료와 옮긴 자료(둘 다 init 기록 없음)는 밀도를 재지 않는다.
|
|
7
9
|
import fs from 'node:fs';
|
|
8
10
|
import path from 'node:path';
|
|
9
11
|
import { loadConfig, noBom, readRegular, UsageError } from './pm-config.mjs';
|
|
@@ -12,6 +14,7 @@ import { ITEM_STATUS, STAGE_STATUS } from './pm-data.mjs';
|
|
|
12
14
|
import { archLayout, archFlaws, flowLayout, nameFits, FLOW } from './pm-render.mjs';
|
|
13
15
|
import { color, finite, textW, wrap, TABS } from './pm-html.mjs';
|
|
14
16
|
import { memoryWarnings } from './pm-memory.mjs';
|
|
17
|
+
import { checkAdr, adrRows, cleanAdr, adrTally } from './pm-adr.mjs';
|
|
15
18
|
|
|
16
19
|
const arr = (v) => (Array.isArray(v) ? v : []);
|
|
17
20
|
const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : null);
|
|
@@ -438,7 +441,8 @@ const lines23 = (n) => n >= 2 && n <= 3;
|
|
|
438
441
|
|
|
439
442
|
// 돌려줌: { rows: [{tab, item, value, min, miss}], low: Set(탭) }. value < min 이면 미달.
|
|
440
443
|
// miss 는 항목마다 재는 줄(상자, 단계, 흐름, 명세, 기타 항목)에서 기준을 못 채운 대상 이름. 수만 재는 줄은 빈 배열.
|
|
441
|
-
|
|
444
|
+
// adr: adr.json 값, adrBare: check 가 참조 확인으로 잰 참조 없는 서술 칸(없으면 적힌 참조로 잼).
|
|
445
|
+
export function density({ arch, flow, req, items, inv, storyCount, adr, adrBare }) {
|
|
442
446
|
const rows = [];
|
|
443
447
|
const add = (tab, item, value, min, miss = []) => rows.push({ tab, item, value, min, miss });
|
|
444
448
|
// 대상마다 ok 를 재어 값, 하한(대상 수), 못 채운 대상 이름을 한 줄로.
|
|
@@ -479,6 +483,13 @@ export function density({ arch, flow, req, items, inv, storyCount }) {
|
|
|
479
483
|
each('workflow', '실패분기', lanes, (l) => arr(l.branches).length, (l) => l.id);
|
|
480
484
|
each('workflow', '외부', lanes, (l) => arr(l.externals).length, (l) => l.id);
|
|
481
485
|
each('workflow', '흐름설명', lanes, (l) => sentences(l.note) >= 3, (l) => `${l.id}(${sentences(l.note)}문장)`);
|
|
486
|
+
reqRows({ add, each }, req, items, storyCount);
|
|
487
|
+
rows.push(...adrRows(adr, adrBare));
|
|
488
|
+
return { rows, low: new Set(rows.filter((x) => x.value < x.min).map((x) => x.tab)) };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// 요구, 명세, 기록 탭 줄. add, each: density 의 줄 추가 함수.
|
|
492
|
+
function reqRows({ add, each }, req, items, storyCount) {
|
|
482
493
|
const at = (x, alt) => (req.where && req.where.get(x)) || alt;
|
|
483
494
|
if (req.users.length) each('req', '요구', req.users, (u) => str(u.text) && str(u.source), (u, i) => at(u, `users[${i}]`));
|
|
484
495
|
else add('req', '요구', 0, 1);
|
|
@@ -488,7 +499,6 @@ export function density({ arch, flow, req, items, inv, storyCount }) {
|
|
|
488
499
|
const stated = req.specs.filter((x) => x.state !== undefined);
|
|
489
500
|
each('req', '비고', stated, (x) => str(x.note), (x) => at(x, `specs[${req.specs.indexOf(x)}]`));
|
|
490
501
|
each('history', '비고', items, (x) => str(x.note), (x, i) => (str(x.key) ? x.key : `items[${i}]`));
|
|
491
|
-
return { rows, low: new Set(rows.filter((x) => x.value < x.min).map((x) => x.tab)) };
|
|
492
502
|
}
|
|
493
503
|
|
|
494
504
|
// 밀도 미달 줄. 못 채운 대상이 있으면 `: ` 뒤에 앞 10개, 넘으면 `외 n`.
|
|
@@ -503,6 +513,24 @@ export function densityLine(x) {
|
|
|
503
513
|
|
|
504
514
|
// ---------- 실행 ----------
|
|
505
515
|
|
|
516
|
+
// state.json 의 memory(init 기록). 없거나 깨졌으면 null.
|
|
517
|
+
function memoryOf(ctx) {
|
|
518
|
+
try {
|
|
519
|
+
const state = obj(JSON.parse(readRegular(path.join(ctx.dataDir, 'state.json'))));
|
|
520
|
+
return (state && obj(state.memory)) || null;
|
|
521
|
+
} catch {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 메모리 경고를 WARN 줄로. 경고 줄에 뒤따르는 대안 줄은 그 WARN 한 줄에 붙인다.
|
|
527
|
+
function memoryInto(r, memory, project, env) {
|
|
528
|
+
for (const l of memoryWarnings(memory, project, env)) {
|
|
529
|
+
if (l.startsWith('경고: ')) r.warn('state.json', 'memory', l.slice(4));
|
|
530
|
+
else if (r.lines.length) r.lines[r.lines.length - 1].msg += `. ${l}`;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
506
534
|
// 검사 전체. 돌려줌: { lines: [출력 줄], fails }
|
|
507
535
|
export function runCheck(ctx, env = process.env) {
|
|
508
536
|
const r = sink();
|
|
@@ -514,35 +542,35 @@ export function runCheck(ctx, env = process.env) {
|
|
|
514
542
|
const req = checkReq(ctx.dataDir, r, ids);
|
|
515
543
|
const flow = checkFlow(ctx.dataDir, r);
|
|
516
544
|
const arch = checkArch(ctx.dataDir, r, config.text.plannedPrefix);
|
|
545
|
+
const got = checkAdr(ctx, config, r);
|
|
546
|
+
const adr = got && got.value;
|
|
547
|
+
const adrBare = got ? got.bare : [];
|
|
517
548
|
const inv = checkInventory(ctx, r);
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
state = obj(JSON.parse(readRegular(path.join(ctx.dataDir, 'state.json'))));
|
|
521
|
-
} catch {
|
|
522
|
-
state = null;
|
|
523
|
-
}
|
|
524
|
-
const memory = state && obj(state.memory);
|
|
525
|
-
// 경고 줄에 뒤따르는 대안 줄을 붙여 WARN 한 줄로.
|
|
526
|
-
for (const l of memoryWarnings(memory, ctx.project, env)) {
|
|
527
|
-
if (l.startsWith('경고: ')) r.warn('state.json', 'memory', l.slice(4));
|
|
528
|
-
else if (r.lines.length) r.lines[r.lines.length - 1].msg += `. ${l}`;
|
|
529
|
-
}
|
|
549
|
+
const memory = memoryOf(ctx);
|
|
550
|
+
memoryInto(r, memory, ctx.project, env);
|
|
530
551
|
const out = r.lines.map((x) => `${x.level} ${x.file} ${x.path} ${x.msg}`);
|
|
531
552
|
if (config.sources.workflowDirs === 'auto' && !workflowRoots(config, ctx, env).length) out.push('WARN workflowDirs auto: 찾은 폴더 0');
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
553
|
+
// 밀도는 init 기록이나 inventory.json 이 있을 때만. 다만 ADR 밀도는 adr.json 이 있으면 늘 낸다
|
|
554
|
+
const dens = inv || memory ? density({ arch, flow, req, items: items.items, inv, storyCount: ids.length, adr, adrBare }) : null;
|
|
555
|
+
const rows = dens ? dens.rows : adr ? adrRows(adr, adrBare) : [];
|
|
556
|
+
return finish(out, rows, dens, adrTally(adr, adrBare).text);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// 밀도 미달 줄, 합계 줄, 탭별 밀도 요약, ADR 집계(근거 없음 칸, 미검증, 구현 못 함, 참조 없는 서술. 늘 한 줄)를 붙인다. 돌려줌: { lines, fails }
|
|
560
|
+
function finish(out, rows, dens, tally) {
|
|
561
|
+
for (const x of rows) if (x.value < x.min) out.push(densityLine(x));
|
|
537
562
|
const fails = out.filter((l) => l.startsWith('FAIL ')).length;
|
|
538
563
|
out.push(`check: FAIL ${fails}, WARN ${out.length - fails}`);
|
|
539
564
|
if (dens) for (const [k] of TABS) out.push(`density ${k} ${dens.low.has(k) ? 'low' : 'ok'}`);
|
|
565
|
+
out.push(`adr ${tally}`);
|
|
540
566
|
return { lines: out, fails };
|
|
541
567
|
}
|
|
542
568
|
|
|
569
|
+
// --adr-cleanup: 검사 전에 init 정리 단계(pm-adr.mjs cleanAdr)를 적용한다. 그 밖에는 자료를 바꾸지 않는다.
|
|
543
570
|
export async function command(cmd, ctx, o, io) {
|
|
544
571
|
if (cmd !== 'check') throw new UsageError(`${cmd} 는 pm-check.mjs 가 맡지 않음`);
|
|
545
572
|
if (o._.length > 1) throw new UsageError(`check 는 위치 인자를 받지 않음: ${o._.slice(1).join(' ')}`);
|
|
573
|
+
if (o['adr-cleanup'] && ctx.exists) for (const l of await cleanAdr(ctx, loadConfig(ctx).config)) io.out(l);
|
|
546
574
|
const { lines, fails } = runCheck(ctx);
|
|
547
575
|
for (const l of lines) io.out(l);
|
|
548
576
|
return fails ? 1 : 0;
|
|
@@ -284,7 +284,7 @@ function storyChange(o, now, configIds) {
|
|
|
284
284
|
|
|
285
285
|
// ---------- init ----------
|
|
286
286
|
|
|
287
|
-
const NEXT = ['다음 할 일', ' 1. references/authoring.md 를 읽고 req-spec.json, reqs.json, flow.json, arch.json 작성', ' 2. check 로 검사. FAIL 0 까지 최대 3회', ' 3. start 로 서버 시작'];
|
|
287
|
+
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
288
|
|
|
289
289
|
const slash = (p) => p.split(path.sep).join('/');
|
|
290
290
|
const realOr = (p) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// pm-memory.mjs: 메모리 규칙 등록(계획 6.2절). 표식으로 중복을 막고 사용자 파일은 끝에 덧붙이기만 한다.
|
|
2
|
+
// 예외: 표식이 붙은 우리 줄(앞머리로 가린 규칙 줄과 색인 줄)이 지금 판과 다르면 그 줄만 제자리에서 바꾼다(첫 원본은 .progress-mon.bak).
|
|
2
3
|
// 대상 파일은 에이전트가 넘긴다(--memory, --memory-dir). 스크립트는 HOME 아래 쓰기 경로를 스스로 만들지 않는다.
|
|
3
4
|
// 덧붙인 뒤 규칙 로드 한도를 재고 경고 줄을 낸다(종료 코드는 그대로). status, check 도 같은 측정을 다시 한다.
|
|
4
5
|
import fs from 'node:fs';
|
|
@@ -14,22 +15,33 @@ const KINDS = ['claude-md', 'agents-md', 'auto-memory', 'none'];
|
|
|
14
15
|
// 규칙 줄. 경로는 자료 폴더 기준 상대 표기뿐(스킬 폴더 절대 경로는 state.json 의 skillDir).
|
|
15
16
|
export function ruleLine(runtime) {
|
|
16
17
|
if (runtime === 'codex') {
|
|
17
|
-
return `진척도 화면(progress-mon): 모든 명령에 --runtime codex 를 붙임. 단계는 stage, 스토리는 story, 그 밖 작업은 item 으로 갱신. .omx/progress-mon/state.json 의 skillDir 아래 scripts/progress-mon.mjs 를 node 로 직접 실행하고, 실패하면 banker-progress-mon 스킬(플러그인 설치는 banker:progress-mon)로 호출. 세션 시작 때 status, 꺼져 있으면 start. start, stop, status 는 sandbox_permissions "require_escalated" 로 승인 요청. 세션을 닫아도 화면을 두려면 사용자 터미널에서 serve 실행을 안내. ${MARKER}`;
|
|
18
|
+
return `진척도 화면(progress-mon): 모든 명령에 --runtime codex 를 붙임. 단계는 stage, 스토리는 story, 그 밖 작업은 item 으로 갱신. 결정이 생기거나 스토리가 끝나면 .omx/progress-mon/adr.json(ADR 탭)을 고치고 check. .omx/progress-mon/state.json 의 skillDir 아래 scripts/progress-mon.mjs 를 node 로 직접 실행하고, 실패하면 banker-progress-mon 스킬(플러그인 설치는 banker:progress-mon)로 호출. 세션 시작 때 status, 꺼져 있으면 start. start, stop, status 는 sandbox_permissions "require_escalated" 로 승인 요청. 세션을 닫아도 화면을 두려면 사용자 터미널에서 serve 실행을 안내. ${MARKER}`;
|
|
18
19
|
}
|
|
19
|
-
return `진척도 화면(progress-mon): 단계는 stage, 스토리는 story, 그 밖 작업은 item 으로 갱신. .omc/progress-mon/state.json 의 skillDir 아래 scripts/progress-mon.mjs 를 node 로 직접 실행하고, 실패하면 progress-mon 스킬로 호출. 세션 시작 때 status, 꺼져 있으면 start. ${MARKER}`;
|
|
20
|
+
return `진척도 화면(progress-mon): 단계는 stage, 스토리는 story, 그 밖 작업은 item 으로 갱신. 결정이 생기거나 스토리가 끝나면 .omc/progress-mon/adr.json(ADR 탭)을 고치고 check. .omc/progress-mon/state.json 의 skillDir 아래 scripts/progress-mon.mjs 를 node 로 직접 실행하고, 실패하면 progress-mon 스킬로 호출. 세션 시작 때 status, 꺼져 있으면 start. ${MARKER}`;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
// 자동 메모리 규칙 파일(새로 만들 때)과 MEMORY.md 색인 줄.
|
|
23
24
|
const ruleFile = (runtime) => `---
|
|
24
25
|
name: progress-mon
|
|
25
|
-
description: 진척도 화면(progress-mon) 갱신 규칙. 단계, 스토리, 그 밖 작업을 stage, story, item
|
|
26
|
+
description: 진척도 화면(progress-mon) 갱신 규칙. 단계, 스토리, 그 밖 작업을 stage, story, item 으로, 결정은 adr.json 에 기록
|
|
26
27
|
metadata:
|
|
27
28
|
type: feedback
|
|
28
29
|
---
|
|
29
30
|
|
|
30
31
|
${ruleLine(runtime)}
|
|
31
32
|
`;
|
|
32
|
-
const INDEX_LINE = `- [진척도 화면 갱신 규칙](progress-mon.md) - stage, story, item 으로 progress-mon 갱신 ${MARKER}`;
|
|
33
|
+
export const INDEX_LINE = `- [진척도 화면 갱신 규칙](progress-mon.md) - stage, story, item 과 adr.json 으로 progress-mon 갱신 ${MARKER}`;
|
|
34
|
+
|
|
35
|
+
// 표식 줄 가운데 우리가 쓴 줄을 가리는 앞머리. 규칙 줄의 런타임은 `--runtime codex` 가 있으면 codex.
|
|
36
|
+
const RULE_HEAD = '진척도 화면(progress-mon):';
|
|
37
|
+
const INDEX_HEAD = '- [진척도 화면 갱신 규칙](progress-mon.md)';
|
|
38
|
+
// 표식 줄이 지금 판에서 되어야 할 줄. 앞머리가 다른 표식 줄(사용자가 쓴 줄 등)은 null(손대지 않음).
|
|
39
|
+
export function currentLine(line) {
|
|
40
|
+
if (!line.includes(MARKER)) return null;
|
|
41
|
+
if (line.startsWith(INDEX_HEAD)) return INDEX_LINE;
|
|
42
|
+
if (line.startsWith(RULE_HEAD)) return ruleLine(line.includes('--runtime codex') ? 'codex' : 'claude');
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
33
45
|
|
|
34
46
|
// ---------- 한도 ----------
|
|
35
47
|
|
|
@@ -250,6 +262,8 @@ function inspect(file, name, base, alt = '') {
|
|
|
250
262
|
return { exists: true, text, eol: text.includes('\r\n') ? '\r\n' : '\n' };
|
|
251
263
|
}
|
|
252
264
|
|
|
265
|
+
const changedError = (file) => new UsageError(`${path.basename(file)} 이 검사 뒤 하드링크나 특수 파일로 바뀌어 쓰지 않음`);
|
|
266
|
+
|
|
253
267
|
// 원본 바이트를 그대로 두고 끝에 줄을 덧붙인다. 마지막 줄에 줄바꿈이 없으면 먼저 넣는다.
|
|
254
268
|
// 연 fd 를 fstat 으로 다시 본다: 검사(inspect)와 쓰기 사이에 하드링크나 특수 파일로 바뀌었으면 쓰지 않는다.
|
|
255
269
|
function append(file, info, lines) {
|
|
@@ -258,7 +272,7 @@ function append(file, info, lines) {
|
|
|
258
272
|
// O_NONBLOCK: 검사 뒤 FIFO 로 바뀐 자리에서 open 이 읽는 쪽을 기다리며 멈추지 않게 한다(Windows 에는 없어 0)
|
|
259
273
|
const { O_WRONLY, O_APPEND, O_CREAT, O_EXCL } = fs.constants;
|
|
260
274
|
const flags = O_WRONLY | (fs.constants.O_NONBLOCK || 0) | O_CREAT | (info.exists ? O_APPEND : O_EXCL);
|
|
261
|
-
const changed = () =>
|
|
275
|
+
const changed = () => changedError(file);
|
|
262
276
|
let fd;
|
|
263
277
|
try {
|
|
264
278
|
fd = fs.openSync(file, flags, 0o666);
|
|
@@ -312,32 +326,103 @@ export function planMemory({ kind, memoryDir, project, runtime }) {
|
|
|
312
326
|
return { kind, runtime, steps: [{ file: t.file, name, info: inspect(t.file, name, root, AGENTS_ALT), what: 'block' }], memory: { kind, path: t.file } };
|
|
313
327
|
}
|
|
314
328
|
|
|
329
|
+
// 표식 줄 가운데 우리 줄만 지금 판으로 바꾼 글. 줄 끝(CRLF, LF)과 나머지 줄은 그대로. 돌려줌: { text, changed }
|
|
330
|
+
function refreshText(text) {
|
|
331
|
+
let changed = 0;
|
|
332
|
+
const out = text.split('\n').map((seg) => {
|
|
333
|
+
const cr = seg.endsWith('\r') ? '\r' : '';
|
|
334
|
+
const line = cr ? seg.slice(0, -1) : seg;
|
|
335
|
+
const want = currentLine(line);
|
|
336
|
+
if (want === null || want === line) return seg;
|
|
337
|
+
changed++;
|
|
338
|
+
return want + cr;
|
|
339
|
+
});
|
|
340
|
+
return { text: out.join('\n'), changed };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const RENAME_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
344
|
+
const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
345
|
+
|
|
346
|
+
// 같은 파일을 원자적으로 다시 쓴다: 대상(링크면 실제 파일) 옆 임시 파일에 쓰고 권한을 맞춘 뒤 rename.
|
|
347
|
+
// 쓰기 전에 대상을 다시 보고 하드링크나 특수 파일로 바뀌었으면 쓰지 않는다. 실패하면 임시 파일을 지우고 원본 그대로.
|
|
348
|
+
function rewrite(file, body) {
|
|
349
|
+
const real = fs.realpathSync(file);
|
|
350
|
+
const st = fs.statSync(real);
|
|
351
|
+
if (!st.isFile() || st.nlink > 1) throw changedError(file);
|
|
352
|
+
const tmp = `${real}.${process.pid}.${Date.now().toString(36)}.tmp`;
|
|
353
|
+
try {
|
|
354
|
+
fs.writeFileSync(tmp, body, { flag: 'wx', mode: st.mode & 0o777 });
|
|
355
|
+
fs.chmodSync(tmp, st.mode & 0o7777);
|
|
356
|
+
for (let i = 0; ; i++) {
|
|
357
|
+
try {
|
|
358
|
+
fs.renameSync(tmp, real);
|
|
359
|
+
return;
|
|
360
|
+
} catch (e) {
|
|
361
|
+
if (i >= 4 || !RENAME_CODES.has(e.code)) throw e;
|
|
362
|
+
pause(50); // Windows 에서 다른 프로세스가 잠깐 쥔 파일
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
} catch (e) {
|
|
366
|
+
fs.rmSync(tmp, { force: true });
|
|
367
|
+
throw e instanceof UsageError ? e : new UsageError(`${path.basename(file)} 쓰기 실패(${e.code || e.message}). 원본 그대로`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 첫 원본 백업. 이미 있으면 덮지 않는다. 만들지 못하면 바꾸지 않고 거부.
|
|
372
|
+
function backup(file, buf) {
|
|
373
|
+
const bak = `${file}.progress-mon.bak`;
|
|
374
|
+
try {
|
|
375
|
+
fs.writeFileSync(bak, buf, { flag: 'wx' });
|
|
376
|
+
} catch (e) {
|
|
377
|
+
if (e.code !== 'EEXIST') throw new UsageError(`${path.basename(bak)} 백업을 만들지 못해 규칙 줄을 바꾸지 않음(${e.code || e.message})`);
|
|
378
|
+
}
|
|
379
|
+
return bak;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// 표식이 이미 있는 파일. 우리 줄이 지금 판과 다를 때만 그 줄을 바꾸고 백업을 남긴다. BOM 과 나머지 바이트는 그대로.
|
|
383
|
+
// 검사 뒤 UTF-8 이 아니게 바뀐 파일은 바꾸지 않고 경고 줄(U+FFFD 로 바뀐 바이트를 쓰지 않게).
|
|
384
|
+
function refreshStep(s) {
|
|
385
|
+
const buf = fs.readFileSync(s.file);
|
|
386
|
+
const bom = buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf;
|
|
387
|
+
let text;
|
|
388
|
+
try {
|
|
389
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bom ? buf.subarray(3) : buf);
|
|
390
|
+
} catch {
|
|
391
|
+
return `경고: ${s.name} 이 UTF-8 이 아니라 규칙 줄을 바꾸지 않음`;
|
|
392
|
+
}
|
|
393
|
+
const r = refreshText(text);
|
|
394
|
+
if (!r.changed) return `규칙 이미 있음: ${s.name}`;
|
|
395
|
+
const bak = backup(s.file, buf);
|
|
396
|
+
rewrite(s.file, Buffer.concat([bom ? buf.subarray(0, 3) : Buffer.alloc(0), Buffer.from(r.text, 'utf8')]));
|
|
397
|
+
return `규칙 줄 교체(백업 ${bak}): ${s.name}`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// 표식이 없는 파일에 규칙을 더한다. 돌려줌: 출력 줄.
|
|
401
|
+
function addStep(s, runtime) {
|
|
402
|
+
const { info } = s;
|
|
403
|
+
const fresh = !info.exists;
|
|
404
|
+
const made = fresh ? '새로 만듦, ' : '';
|
|
405
|
+
if (s.what === 'rule-file') {
|
|
406
|
+
if (fresh) fs.writeFileSync(s.file, ruleFile(runtime), { flag: 'wx' });
|
|
407
|
+
else append(s.file, info, [ruleLine(runtime)]);
|
|
408
|
+
return `규칙 추가: ${s.name} (${fresh ? '새로 만듦' : '1줄'})`;
|
|
409
|
+
}
|
|
410
|
+
if (s.what === 'index') {
|
|
411
|
+
append(s.file, info, [INDEX_LINE]);
|
|
412
|
+
return `규칙 추가: ${s.name} (${made}색인 1줄)`;
|
|
413
|
+
}
|
|
414
|
+
if (s.what === 'line') {
|
|
415
|
+
append(s.file, info, [ruleLine(runtime)]);
|
|
416
|
+
return `규칙 추가: ${s.name} (${made}1줄)`;
|
|
417
|
+
}
|
|
418
|
+
append(s.file, info, [OMX_OPEN, ruleLine(runtime), OMX_CLOSE]);
|
|
419
|
+
return `규칙 추가: ${s.name} (${made}3줄)`;
|
|
420
|
+
}
|
|
421
|
+
|
|
315
422
|
// 계획대로 쓴다. 돌려줌: 출력 줄 목록(규칙 줄, 경고 줄).
|
|
316
423
|
export function applyMemory(plan, project, env = process.env) {
|
|
317
424
|
if (plan.kind === 'none') return ['규칙 등록 안 함(--memory none)'];
|
|
318
|
-
const out = [];
|
|
319
425
|
if (plan.dir) fs.mkdirSync(plan.dir, { recursive: true });
|
|
320
|
-
|
|
321
|
-
const { info } = s;
|
|
322
|
-
if (info.text.includes(MARKER)) {
|
|
323
|
-
out.push(`규칙 이미 있음: ${s.name}`);
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
const fresh = !info.exists;
|
|
327
|
-
if (s.what === 'rule-file') {
|
|
328
|
-
if (fresh) fs.writeFileSync(s.file, ruleFile(plan.runtime), { flag: 'wx' });
|
|
329
|
-
else append(s.file, info, [ruleLine(plan.runtime)]);
|
|
330
|
-
out.push(`규칙 추가: ${s.name} (${fresh ? '새로 만듦' : '1줄'})`);
|
|
331
|
-
} else if (s.what === 'index') {
|
|
332
|
-
append(s.file, info, [INDEX_LINE]);
|
|
333
|
-
out.push(`규칙 추가: ${s.name} (${fresh ? '새로 만듦, ' : ''}색인 1줄)`);
|
|
334
|
-
} else if (s.what === 'line') {
|
|
335
|
-
append(s.file, info, [ruleLine(plan.runtime)]);
|
|
336
|
-
out.push(`규칙 추가: ${s.name} (${fresh ? '새로 만듦, ' : ''}1줄)`);
|
|
337
|
-
} else {
|
|
338
|
-
append(s.file, info, [OMX_OPEN, ruleLine(plan.runtime), OMX_CLOSE]);
|
|
339
|
-
out.push(`규칙 추가: ${s.name} (${fresh ? '새로 만듦, ' : ''}3줄)`);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
426
|
+
const out = plan.steps.map((s) => (s.info.text.includes(MARKER) ? refreshStep(s) : addStep(s, plan.runtime)));
|
|
342
427
|
return [...out, ...memoryWarnings(plan.memory, project, env)];
|
|
343
428
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// pm-render.mjs:
|
|
1
|
+
// pm-render.mjs: 5탭(진척도, 구현요건, workflow, 구성도, ADR)과 단일 HTML 스냅숏, workflow 배치와 구성도 자동 배치.
|
|
2
2
|
// 옛 진척도 서버의 클래스 이름과 DOM 구조를 그대로 둔다. 저장소 고유 값은 config.json 에서 읽는다.
|
|
3
3
|
// 시각은 now 인자로만 받는다. CLI 하위 명령 export 도 여기서 맡는다.
|
|
4
4
|
import fs from 'node:fs';
|
|
@@ -6,6 +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
10
|
|
|
10
11
|
const two = (n) => String(n).padStart(2, '0');
|
|
11
12
|
const hm = (ms) => (ms == null ? '-' : Math.floor(ms / 3600e3) + ':' + two(Math.floor((ms % 3600e3) / 60000)));
|
|
@@ -1540,13 +1541,14 @@ ${legend ? `<h2>읽는 법</h2><ul class="meta">${legend}</ul>` : ''}
|
|
|
1540
1541
|
export const tabKey = (tab) => (tab === 'flow' ? 'workflow' : TABS.some((t) => t[0] === tab) ? tab : 'history');
|
|
1541
1542
|
|
|
1542
1543
|
function tabBody(m, config, k, now) {
|
|
1544
|
+
if (k === 'adr') return adrBody(m, emptyNotice);
|
|
1543
1545
|
if (k === 'arch') return archBody(m, config);
|
|
1544
1546
|
if (k === 'workflow') return flowBody(m);
|
|
1545
1547
|
if (k === 'req') return reqBody(m, config);
|
|
1546
1548
|
return historyBody(m, config, now);
|
|
1547
1549
|
}
|
|
1548
1550
|
|
|
1549
|
-
// 모은 자료(m)로 탭 하나의 HTML. workflow
|
|
1551
|
+
// 모은 자료(m)로 탭 하나의 HTML. workflow, 구성도, ADR 은 자주 바뀌지 않아 자동 갱신하지 않는다.
|
|
1550
1552
|
export function pageFrom(m, config, tab, now) {
|
|
1551
1553
|
const k = tabKey(tab);
|
|
1552
1554
|
const refresh = k === 'history' || k === 'req' ? `<meta http-equiv="refresh" content="${config.refreshSec};url=/?tab=${k}">` : '';
|
|
@@ -1560,8 +1562,8 @@ ${tabsHtml(k)}
|
|
|
1560
1562
|
${tabBody(m, config, k, now)}</body></html>`;
|
|
1561
1563
|
}
|
|
1562
1564
|
|
|
1563
|
-
// 단일 HTML 스냅숏:
|
|
1564
|
-
// 탭 전환은 주소의 #history, #req, #workflow, #arch 와 짧은 인라인 스크립트. 스크립트가 꺼져 있으면
|
|
1565
|
+
// 단일 HTML 스냅숏: 다섯 탭을 한 파일에. 서버, 자동 갱신, 외부 자원 없이 file:// 로 열린다.
|
|
1566
|
+
// 탭 전환은 주소의 #history, #req, #workflow, #arch, #adr 와 짧은 인라인 스크립트. 스크립트가 꺼져 있으면 다섯 탭을 모두 보인다.
|
|
1565
1567
|
export function snapshotFrom(m, config, now) {
|
|
1566
1568
|
const tabs = TABS.map(([k, name]) => `<a href="#${k}" data-tab="${k}">${esc(name)}</a>`).join('');
|
|
1567
1569
|
const secs = TABS.map(([k, name]) => `<section id="tab-${k}" class="tab"><h2 class="tt">${esc(name)}</h2>\n\n${tabBody(m, config, k, now)}</section>`).join('\n');
|
|
@@ -780,7 +780,7 @@ function tabData(dataDir) {
|
|
|
780
780
|
return e.code === 'ENOENT' ? '없음' : '깨짐';
|
|
781
781
|
}
|
|
782
782
|
};
|
|
783
|
-
return `진척도 ${has('items.json')}, 구현요건 ${has('req-spec.json')}, workflow ${has('flow.json')}, 구성도 ${has('arch.json')}`;
|
|
783
|
+
return `진척도 ${has('items.json')}, 구현요건 ${has('req-spec.json')}, workflow ${has('flow.json')}, 구성도 ${has('arch.json')}, ADR ${has('adr.json')}`;
|
|
784
784
|
}
|
|
785
785
|
|
|
786
786
|
// 서버를 띄운 뒤 config.json 의 port, portStrict, hosts 가 바뀌었으면 경고 한 줄(그 값은 다시 띄워야 적용).
|
|
@@ -815,6 +815,7 @@ export async function status(ctx, io) {
|
|
|
815
815
|
const run = await runningServer(ctx.dataDir);
|
|
816
816
|
if (!run) {
|
|
817
817
|
io.out(`꺼짐(자료 폴더: ${label})`);
|
|
818
|
+
io.out(`탭 자료: ${tabData(ctx.dataDir)}`);
|
|
818
819
|
statusWarnings(ctx, io);
|
|
819
820
|
return 3;
|
|
820
821
|
}
|
|
@@ -27,7 +27,8 @@ export const HELP = `progress-mon: 프로젝트 진척도 웹 화면
|
|
|
27
27
|
note --key <k> [--note <t>] [--issue <t>]
|
|
28
28
|
stage --id <id> --status started|done|skipped|failed|stopped [--label <t>]
|
|
29
29
|
story --id <id> (--status todo|doing|done | --commit <hash>) [--name <t>]
|
|
30
|
-
check
|
|
30
|
+
check [--adr-cleanup]
|
|
31
|
+
자료 파일 검사. FAIL, WARN 한 줄씩. --adr-cleanup 은 먼저 adr.json 정리(참조 FAIL 은 gap, 스키마 FAIL 이 남으면 adr.json.rejected-<시각> 으로 옮김)
|
|
31
32
|
|
|
32
33
|
공통 인자
|
|
33
34
|
--runtime claude|codex 자료 폴더 기본값(.omc/progress-mon/, .omx/progress-mon/)
|
|
@@ -39,7 +40,7 @@ export const HELP = `progress-mon: 프로젝트 진척도 웹 화면
|
|
|
39
40
|
|
|
40
41
|
// 값을 받는 인자와 값 없는 인자.
|
|
41
42
|
const VALUE_FLAGS = new Set(['runtime', 'project', 'data', 'port', 'host', 'memory', 'memory-dir', 'title', 'now', 'key', 'status', 'text', 'note', 'issue', 'id', 'label', 'commit', 'name', 'at']);
|
|
42
|
-
const BOOL_FLAGS = new Set(['port-strict', 'help']);
|
|
43
|
+
const BOOL_FLAGS = new Set(['port-strict', 'help', 'adr-cleanup']);
|
|
43
44
|
|
|
44
45
|
// 서버 밖 명령을 맡는 모듈. 쓰는 명령에서만 적재한다.
|
|
45
46
|
const MODS = {
|