@kaydash9999/banker-plugins 0.23.2 → 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 +56 -0
- package/README.md +14 -6
- package/codex/manifest.json +6 -2
- package/codex/skills/teamwork/SKILL.md +19 -6
- package/codex/skills/teamwork/scripts/teamwork.mjs +87 -22
- 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 +11 -9
- 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 +124 -29
- package/skills/progress-mon/scripts/pm-render.mjs +6 -4
- package/skills/progress-mon/scripts/pm-server.mjs +82 -30
- 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,10 +272,20 @@ 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
|
|
275
|
+
const changed = () => changedError(file);
|
|
276
|
+
let fd;
|
|
277
|
+
try {
|
|
278
|
+
fd = fs.openSync(file, flags, 0o666);
|
|
279
|
+
} catch (e) {
|
|
280
|
+
// 읽는 쪽이 없는 FIFO(ENXIO)는 검사 뒤 바뀐 자리다. EAGAIN 은 막힌 특수 파일이나 다른 프로세스의 잠금(lease)일 수 있다.
|
|
281
|
+
// 둘 다 날 오류 대신 거부 문구로 끝낸다
|
|
282
|
+
if (e.code === 'ENXIO') throw changed();
|
|
283
|
+
if (e.code === 'EAGAIN') throw new UsageError(`${path.basename(file)} 이 검사 뒤 바뀌었거나 다른 프로세스가 잡고 있어 쓰지 않음`);
|
|
284
|
+
throw e;
|
|
285
|
+
}
|
|
262
286
|
try {
|
|
263
287
|
const st = fs.fstatSync(fd);
|
|
264
|
-
if (!st.isFile() || st.nlink > 1) throw
|
|
288
|
+
if (!st.isFile() || st.nlink > 1) throw changed();
|
|
265
289
|
fs.writeFileSync(fd, body); // 다 쓸 때까지 반복
|
|
266
290
|
} finally {
|
|
267
291
|
fs.closeSync(fd);
|
|
@@ -302,32 +326,103 @@ export function planMemory({ kind, memoryDir, project, runtime }) {
|
|
|
302
326
|
return { kind, runtime, steps: [{ file: t.file, name, info: inspect(t.file, name, root, AGENTS_ALT), what: 'block' }], memory: { kind, path: t.file } };
|
|
303
327
|
}
|
|
304
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
|
+
|
|
305
422
|
// 계획대로 쓴다. 돌려줌: 출력 줄 목록(규칙 줄, 경고 줄).
|
|
306
423
|
export function applyMemory(plan, project, env = process.env) {
|
|
307
424
|
if (plan.kind === 'none') return ['규칙 등록 안 함(--memory none)'];
|
|
308
|
-
const out = [];
|
|
309
425
|
if (plan.dir) fs.mkdirSync(plan.dir, { recursive: true });
|
|
310
|
-
|
|
311
|
-
const { info } = s;
|
|
312
|
-
if (info.text.includes(MARKER)) {
|
|
313
|
-
out.push(`규칙 이미 있음: ${s.name}`);
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
const fresh = !info.exists;
|
|
317
|
-
if (s.what === 'rule-file') {
|
|
318
|
-
if (fresh) fs.writeFileSync(s.file, ruleFile(plan.runtime), { flag: 'wx' });
|
|
319
|
-
else append(s.file, info, [ruleLine(plan.runtime)]);
|
|
320
|
-
out.push(`규칙 추가: ${s.name} (${fresh ? '새로 만듦' : '1줄'})`);
|
|
321
|
-
} else if (s.what === 'index') {
|
|
322
|
-
append(s.file, info, [INDEX_LINE]);
|
|
323
|
-
out.push(`규칙 추가: ${s.name} (${fresh ? '새로 만듦, ' : ''}색인 1줄)`);
|
|
324
|
-
} else if (s.what === 'line') {
|
|
325
|
-
append(s.file, info, [ruleLine(plan.runtime)]);
|
|
326
|
-
out.push(`규칙 추가: ${s.name} (${fresh ? '새로 만듦, ' : ''}1줄)`);
|
|
327
|
-
} else {
|
|
328
|
-
append(s.file, info, [OMX_OPEN, ruleLine(plan.runtime), OMX_CLOSE]);
|
|
329
|
-
out.push(`규칙 추가: ${s.name} (${fresh ? '새로 만듦, ' : ''}3줄)`);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
426
|
+
const out = plan.steps.map((s) => (s.info.text.includes(MARKER) ? refreshStep(s) : addStep(s, plan.runtime)));
|
|
332
427
|
return [...out, ...memoryWarnings(plan.memory, project, env)];
|
|
333
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');
|
|
@@ -102,24 +102,30 @@ function openLog(file, append = true) {
|
|
|
102
102
|
return fd;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
// 한도를 넘은 server.log 를 server.log.1 로 옮기고 새 fd 를 돌려준다(rename 은 링크를 따라가지 않음).
|
|
106
|
-
//
|
|
107
|
-
function rotateLog(file, fd, warn) {
|
|
105
|
+
// 한도를 넘은 server.log 를 server.log.1 로 옮기고 새 fd 를 돌려준다(rename 은 링크를 따라가지 않음). 옮기면 ok() 를 부른다.
|
|
106
|
+
// 옮긴 뒤 새 파일을 못 열면 그 오류를 던진다(옮기기는 된 것이라 쓰기 실패로 알림).
|
|
107
|
+
function rotateLog(file, fd, warn, ok) {
|
|
108
108
|
fs.closeSync(fd);
|
|
109
109
|
try {
|
|
110
110
|
fs.renameSync(file, `${file}.1`);
|
|
111
|
-
return openLog(file);
|
|
112
111
|
} catch (e) {
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
fs.ftruncateSync(again, 0);
|
|
116
|
-
} catch (t) {
|
|
117
|
-
fs.closeSync(again);
|
|
118
|
-
throw t;
|
|
119
|
-
}
|
|
120
|
-
warn(`server.log 를 server.log.1 로 옮기지 못함(${e.code || e.message}). 대신 server.log 를 비우고 계속 기록. server.log.1 자리를 치우면 다시 옮김`);
|
|
121
|
-
return again;
|
|
112
|
+
return keepLog(file, e, warn);
|
|
122
113
|
}
|
|
114
|
+
ok();
|
|
115
|
+
return openLog(file);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 옮기지 못하면(그 자리에 폴더 등) 로그를 버리지 않도록 server.log(일반 파일로 확인함)를 비우고 계속 쓰며, 그 사실을 warn 으로 알린다.
|
|
119
|
+
function keepLog(file, e, warn) {
|
|
120
|
+
const again = openLog(file, false);
|
|
121
|
+
try {
|
|
122
|
+
fs.ftruncateSync(again, 0);
|
|
123
|
+
} catch (t) {
|
|
124
|
+
fs.closeSync(again);
|
|
125
|
+
throw t;
|
|
126
|
+
}
|
|
127
|
+
warn(`server.log 를 server.log.1 로 옮기지 못함(${e.code || e.message}). 대신 server.log 를 비우고 계속 기록. server.log.1 자리를 치우면 다시 옮김`);
|
|
128
|
+
return again;
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
// server.log 를 못 쓴 까닭의 경고 문구. 링크, 특수 파일, 하드링크 거부와 fs 오류를 나눈다.
|
|
@@ -128,34 +134,79 @@ function logFailure(e) {
|
|
|
128
134
|
return `server.log 에 기록하지 못함(${(e && (e.code || e.message)) || e}). 서버는 계속`;
|
|
129
135
|
}
|
|
130
136
|
|
|
137
|
+
const PENDING_KEEP = 10; // 쓰지 못하고 쌓아 두는 경고와 회복 줄은 앞 10줄과 뒤 10줄만 남기고 가운데는 개수만 남긴다
|
|
138
|
+
|
|
139
|
+
// server.log 경고 상태. 종류별 칸이 따로 있다: 'fail' 은 기록이 멈춘 경고라 쓰기가 다시 되면 지우고, 'rotate' 는 다음 옮기기가 되면 지운다.
|
|
140
|
+
// 같은 까닭은 한 번만 알리고, 까닭이 바뀌면 다시 알린다. status 에는 fail 이 있으면 fail, 없으면 rotate 를 보인다(바뀔 때만 onWarn).
|
|
141
|
+
// 알림과 회복 줄은 쌓아 두었다가 다음에 쓰는 줄과 함께 쓴다. 쓰기에 성공한 뒤에만 비운다
|
|
142
|
+
function logWarnings(onWarn) {
|
|
143
|
+
const shown = { fail: null, rotate: null };
|
|
144
|
+
let lines = [];
|
|
145
|
+
let dropped = 0;
|
|
146
|
+
let droppedAt = ''; // 처음 빠진 줄의 시각. 생략 줄에 써서 로그 시각이 거꾸로 가지 않게 한다
|
|
147
|
+
const current = () => shown.fail || shown.rotate;
|
|
148
|
+
const set = (kind, text) => {
|
|
149
|
+
const before = current();
|
|
150
|
+
shown[kind] = text;
|
|
151
|
+
if (current() !== before) onWarn(current());
|
|
152
|
+
};
|
|
153
|
+
// 처음 까닭과 최신 까닭(회복 줄 포함)이 함께 남도록 넘치면 가운데 줄을 뺀다
|
|
154
|
+
const note = (text) => {
|
|
155
|
+
lines.push(`${new Date().toISOString()} ${text}\n`);
|
|
156
|
+
if (lines.length > PENDING_KEEP * 2) {
|
|
157
|
+
const [gone] = lines.splice(PENDING_KEEP, 1);
|
|
158
|
+
if (!dropped) droppedAt = gone.slice(0, gone.indexOf(' '));
|
|
159
|
+
dropped++;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const omitted = () => (dropped ? [`${droppedAt} 경고: 쓰지 못한 경고와 회복 줄 ${dropped}개 생략\n`] : []);
|
|
163
|
+
return {
|
|
164
|
+
warn(kind, text) {
|
|
165
|
+
if (shown[kind] === text) return;
|
|
166
|
+
set(kind, text);
|
|
167
|
+
note(`경고: ${text}`);
|
|
168
|
+
},
|
|
169
|
+
clear(kind) {
|
|
170
|
+
if (!shown[kind]) return false;
|
|
171
|
+
note(`회복: 앞의 경고가 풀림(${shown[kind]})`);
|
|
172
|
+
set(kind, null);
|
|
173
|
+
return true;
|
|
174
|
+
},
|
|
175
|
+
head: () => [...lines.slice(0, PENDING_KEEP), ...omitted(), ...lines.slice(PENDING_KEEP)].join(''),
|
|
176
|
+
sent() {
|
|
177
|
+
lines = [];
|
|
178
|
+
dropped = 0;
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
131
183
|
// server.log 한 줄 덧붙이기. 덧붙이면 max(1 MB)를 넘을 때 먼저 server.log.1 로 옮긴다(대상을 자르지 않음).
|
|
132
|
-
// 링크이거나 일반 파일이 아니면 쓰지 않는다. 옮기지 못한 일은 onWarn(서버의 status 경고)과 로그의 경고 줄로
|
|
184
|
+
// 링크이거나 일반 파일이 아니면 쓰지 않는다. 못 쓰거나 옮기지 못한 일은 onWarn(서버의 status 경고)과 로그의 경고 줄로 알리고,
|
|
185
|
+
// 회복하면 onWarn(null) 로 status 경고를 지운다. 실패해도 서버는 계속.
|
|
133
186
|
export function makeLogger(dataDir, max = LOG_MAX, onWarn = () => {}) {
|
|
134
187
|
const file = logPath(dataDir);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
if (warned) return;
|
|
139
|
-
warned = true;
|
|
140
|
-
onWarn(w);
|
|
141
|
-
pending = `${new Date().toISOString()} 경고: ${w}\n`;
|
|
142
|
-
};
|
|
188
|
+
const w = logWarnings(onWarn);
|
|
189
|
+
const rotateWarn = (text) => w.warn('rotate', text);
|
|
190
|
+
const rotateOk = () => w.clear('rotate');
|
|
143
191
|
return (msg) => {
|
|
144
192
|
const line = `${new Date().toISOString()} ${msg}\n`;
|
|
145
193
|
let fd = null;
|
|
146
194
|
try {
|
|
147
195
|
fd = openLog(file);
|
|
148
|
-
if (fs.fstatSync(fd).size + Buffer.byteLength(line) > max) {
|
|
196
|
+
if (fs.fstatSync(fd).size + Buffer.byteLength(w.head() + line) > max) {
|
|
149
197
|
const old = fd;
|
|
150
198
|
fd = null;
|
|
151
|
-
fd = rotateLog(file, old,
|
|
199
|
+
fd = rotateLog(file, old, rotateWarn, rotateOk);
|
|
200
|
+
}
|
|
201
|
+
fs.writeSync(fd, w.head() + line);
|
|
202
|
+
w.sent();
|
|
203
|
+
if (w.clear('fail')) {
|
|
204
|
+
fs.writeSync(fd, w.head());
|
|
205
|
+
w.sent();
|
|
152
206
|
}
|
|
153
|
-
if (pending) fs.writeSync(fd, pending);
|
|
154
|
-
pending = '';
|
|
155
|
-
fs.writeSync(fd, line);
|
|
156
207
|
} catch (e) {
|
|
157
|
-
// 로그를 못 써도 응답은 계속. 기록이 멈춘 것을 까닭과 함께
|
|
158
|
-
warn(logFailure(e));
|
|
208
|
+
// 로그를 못 써도 응답은 계속. 기록이 멈춘 것을 까닭과 함께 알린다(status 경고)
|
|
209
|
+
w.warn('fail', logFailure(e));
|
|
159
210
|
} finally {
|
|
160
211
|
if (fd !== null) fs.closeSync(fd);
|
|
161
212
|
}
|
|
@@ -729,7 +780,7 @@ function tabData(dataDir) {
|
|
|
729
780
|
return e.code === 'ENOENT' ? '없음' : '깨짐';
|
|
730
781
|
}
|
|
731
782
|
};
|
|
732
|
-
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')}`;
|
|
733
784
|
}
|
|
734
785
|
|
|
735
786
|
// 서버를 띄운 뒤 config.json 의 port, portStrict, hosts 가 바뀌었으면 경고 한 줄(그 값은 다시 띄워야 적용).
|
|
@@ -764,6 +815,7 @@ export async function status(ctx, io) {
|
|
|
764
815
|
const run = await runningServer(ctx.dataDir);
|
|
765
816
|
if (!run) {
|
|
766
817
|
io.out(`꺼짐(자료 폴더: ${label})`);
|
|
818
|
+
io.out(`탭 자료: ${tabData(ctx.dataDir)}`);
|
|
767
819
|
statusWarnings(ctx, io);
|
|
768
820
|
return 3;
|
|
769
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 = {
|