leerness 1.13.0 → 1.15.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/CHANGELOG.md +98 -0
- package/README.md +23 -5
- package/bin/leerness.js +41 -55
- package/lib/diagnostics.js +194 -193
- package/lib/review-request.js +297 -289
- package/package.json +1 -1
package/lib/diagnostics.js
CHANGED
|
@@ -1,193 +1,194 @@
|
|
|
1
|
-
// lib/diagnostics.js — 설치/환경 진단 핸들러 (doctor / which).
|
|
2
|
-
// 1.9.392 (UR-0025 큰 핸들러 모듈화 4번째): bin/harness.js 에서 doctor/which 분리.
|
|
3
|
-
// - I/O: ./io(log). node: child_process.
|
|
4
|
-
// - harness 고유 의존(VERSION · _selfTestCases · _detectShellCtx · _mcpToolCount · argv 파서 has · harnessPath)은 deps 주입(DI).
|
|
5
|
-
// _selfTestCases 의 각 case.run 클로저는 harness 스코프를 유지하므로 함수 참조 주입만으로 정상 동작.
|
|
6
|
-
'use strict';
|
|
7
|
-
const cp = require('child_process');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
const { log, fail, absRoot, exists, read } = require('./io');
|
|
10
|
-
const { parseHarnessVersion, _parseChangelogBetween } = require('./pure-utils');
|
|
11
|
-
|
|
12
|
-
function doctorCmd(opts = {}, deps = {}) {
|
|
13
|
-
const { VERSION, _selfTestCases, _detectShellCtx, _mcpToolCount, has, harnessPath } = deps;
|
|
14
|
-
const json = opts.json || has('--json');
|
|
15
|
-
// 1) 코어 무결성: selftest 케이스 인라인 실행
|
|
16
|
-
let pass = 0; const failNames = [];
|
|
17
|
-
try {
|
|
18
|
-
for (const c of _selfTestCases()) {
|
|
19
|
-
let okc = false;
|
|
20
|
-
try { okc = !!c.run(); } catch { okc = false; }
|
|
21
|
-
if (okc) pass++; else failNames.push(c.name);
|
|
22
|
-
}
|
|
23
|
-
} catch {}
|
|
24
|
-
const total = pass + failNames.length;
|
|
25
|
-
// 2) 셸/PowerShell 컨텍스트 (UR-0052)
|
|
26
|
-
let shell = null, psVersion = null;
|
|
27
|
-
try { const ctx = _detectShellCtx(); shell = ctx.shell; psVersion = ctx.psVersion; } catch {}
|
|
28
|
-
const mcpCount = (() => { try { return _mcpToolCount(); } catch { return null; } })();
|
|
29
|
-
const report = {
|
|
30
|
-
version: VERSION, node: process.version, platform: process.platform + '/' + process.arch,
|
|
31
|
-
runningFrom: harnessPath, mcpTools: mcpCount,
|
|
32
|
-
selftest: { pass, total, ok: failNames.length === 0, failed: failNames },
|
|
33
|
-
shell, psVersion, healthy: failNames.length === 0
|
|
34
|
-
};
|
|
35
|
-
if (json) { process.stdout.write(JSON.stringify(report, null, 2) + '\n'); if (!report.healthy) process.exitCode = 1; return report; }
|
|
36
|
-
const isTty = process.stdout && process.stdout.isTTY;
|
|
37
|
-
const gr = s => isTty ? `\x1b[32m${s}\x1b[0m` : s, rd = s => isTty ? `\x1b[31m${s}\x1b[0m` : s, cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s, dm = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
38
|
-
log(cy(`# leerness doctor — 설치/환경 진단`));
|
|
39
|
-
log('');
|
|
40
|
-
log(` ${gr('✓')} version ${VERSION} · node ${process.version} · ${process.platform}/${process.arch}`);
|
|
41
|
-
log(` ${gr('✓')} 설치 경로: ${dm(harnessPath)}`);
|
|
42
|
-
log(` ${gr('✓')} MCP 도구: ${mcpCount}`);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
log('');
|
|
47
|
-
log(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
out.diagnostics.push(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
out.diagnostics.push(
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
log(
|
|
103
|
-
log(
|
|
104
|
-
log(
|
|
105
|
-
log(
|
|
106
|
-
log(
|
|
107
|
-
|
|
108
|
-
if (out.npm.
|
|
109
|
-
if (out.npm.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
log(
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
log(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
log(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
log(
|
|
126
|
-
log(
|
|
127
|
-
log(`
|
|
128
|
-
log(`
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return null;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (!exists(changelogPath))
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
log(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
log(
|
|
162
|
-
log(
|
|
163
|
-
log(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
v.
|
|
172
|
-
v.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
if (
|
|
177
|
-
log('');
|
|
178
|
-
log(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
log(
|
|
187
|
-
log(
|
|
188
|
-
log(`
|
|
189
|
-
log(`
|
|
190
|
-
log(`
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
1
|
+
// lib/diagnostics.js — 설치/환경 진단 핸들러 (doctor / which).
|
|
2
|
+
// 1.9.392 (UR-0025 큰 핸들러 모듈화 4번째): bin/harness.js 에서 doctor/which 분리.
|
|
3
|
+
// - I/O: ./io(log). node: child_process.
|
|
4
|
+
// - harness 고유 의존(VERSION · _selfTestCases · _detectShellCtx · _mcpToolCount · argv 파서 has · harnessPath)은 deps 주입(DI).
|
|
5
|
+
// _selfTestCases 의 각 case.run 클로저는 harness 스코프를 유지하므로 함수 참조 주입만으로 정상 동작.
|
|
6
|
+
'use strict';
|
|
7
|
+
const cp = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { log, fail, absRoot, exists, read } = require('./io');
|
|
10
|
+
const { parseHarnessVersion, _parseChangelogBetween } = require('./pure-utils');
|
|
11
|
+
|
|
12
|
+
function doctorCmd(opts = {}, deps = {}) {
|
|
13
|
+
const { VERSION, _selfTestCases, _detectShellCtx, _mcpToolCount, has, harnessPath } = deps;
|
|
14
|
+
const json = opts.json || has('--json');
|
|
15
|
+
// 1) 코어 무결성: selftest 케이스 인라인 실행
|
|
16
|
+
let pass = 0; const failNames = [];
|
|
17
|
+
try {
|
|
18
|
+
for (const c of _selfTestCases()) {
|
|
19
|
+
let okc = false;
|
|
20
|
+
try { okc = !!c.run(); } catch { okc = false; }
|
|
21
|
+
if (okc) pass++; else failNames.push(c.name);
|
|
22
|
+
}
|
|
23
|
+
} catch {}
|
|
24
|
+
const total = pass + failNames.length;
|
|
25
|
+
// 2) 셸/PowerShell 컨텍스트 (UR-0052)
|
|
26
|
+
let shell = null, psVersion = null;
|
|
27
|
+
try { const ctx = _detectShellCtx(); shell = ctx.shell; psVersion = ctx.psVersion; } catch {}
|
|
28
|
+
const mcpCount = (() => { try { return _mcpToolCount(); } catch { return null; } })();
|
|
29
|
+
const report = {
|
|
30
|
+
version: VERSION, node: process.version, platform: process.platform + '/' + process.arch,
|
|
31
|
+
runningFrom: harnessPath, mcpTools: mcpCount,
|
|
32
|
+
selftest: { pass, total, ok: failNames.length === 0, failed: failNames },
|
|
33
|
+
shell, psVersion, healthy: failNames.length === 0
|
|
34
|
+
};
|
|
35
|
+
if (json) { process.stdout.write(JSON.stringify(report, null, 2) + '\n'); if (!report.healthy) process.exitCode = 1; return report; }
|
|
36
|
+
const isTty = process.stdout && process.stdout.isTTY;
|
|
37
|
+
const gr = s => isTty ? `\x1b[32m${s}\x1b[0m` : s, rd = s => isTty ? `\x1b[31m${s}\x1b[0m` : s, cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s, dm = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
38
|
+
log(cy(`# leerness doctor — 설치/환경 진단`));
|
|
39
|
+
log('');
|
|
40
|
+
log(` ${gr('✓')} version ${VERSION} · node ${process.version} · ${process.platform}/${process.arch}`);
|
|
41
|
+
log(` ${gr('✓')} 설치 경로: ${dm(harnessPath)}`);
|
|
42
|
+
log(` ${gr('✓')} MCP 도구: ${mcpCount}`);
|
|
43
|
+
// 1.13.1 (15th 블라인드 리뷰 P3, Sonnet): pass 수에 '실패' 가 붙어 "209/210 실패"(=209건 실패로 오독)되던 문구 → "통과 N/M (K건 실패)" 로 명확화.
|
|
44
|
+
log(` ${report.selftest.ok ? gr('✓') : rd('✗')} selftest: ${pass}/${total} 통과${report.selftest.ok ? '' : ` (${total - pass}건 실패)`}`);
|
|
45
|
+
if (!report.selftest.ok) report.selftest.failed.slice(0, 5).forEach(n => log(rd(` ✗ ${n}`)));
|
|
46
|
+
log(` ${gr('✓')} 셸: ${shell || 'unknown'}${psVersion && shell === 'powershell' ? ` (PowerShell ${psVersion})` : ''}`);
|
|
47
|
+
log('');
|
|
48
|
+
log(report.healthy ? gr(' ✓ leerness 설치 정상') : rd(' ✗ 문제 감지 — 재설치: npm i -g leerness@latest · 진단: leerness which'));
|
|
49
|
+
if (!report.healthy) process.exitCode = 1;
|
|
50
|
+
return report;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function whichCmd(deps = {}) {
|
|
54
|
+
const { VERSION, has, harnessPath } = deps;
|
|
55
|
+
const out = {
|
|
56
|
+
version: VERSION,
|
|
57
|
+
runningFrom: harnessPath,
|
|
58
|
+
nodeVersion: process.version,
|
|
59
|
+
platform: process.platform,
|
|
60
|
+
arch: process.arch,
|
|
61
|
+
npm: {},
|
|
62
|
+
pathCandidates: []
|
|
63
|
+
};
|
|
64
|
+
// npm root -g (글로벌 설치 경로)
|
|
65
|
+
try {
|
|
66
|
+
const r = cp.spawnSync('npm', ['root', '-g'], { encoding: 'utf8', timeout: 5000, shell: true });
|
|
67
|
+
if (r.status === 0) out.npm.globalRoot = (r.stdout || '').trim();
|
|
68
|
+
} catch {}
|
|
69
|
+
// npm cache (npx 캐시 경로)
|
|
70
|
+
try {
|
|
71
|
+
const r = cp.spawnSync('npm', ['config', 'get', 'cache'], { encoding: 'utf8', timeout: 5000, shell: true });
|
|
72
|
+
if (r.status === 0) out.npm.cacheDir = (r.stdout || '').trim();
|
|
73
|
+
} catch {}
|
|
74
|
+
// npm 글로벌 leerness 설치 정보
|
|
75
|
+
try {
|
|
76
|
+
const r = cp.spawnSync('npm', ['ls', '-g', 'leerness', '--depth=0', '--json'], { encoding: 'utf8', timeout: 8000, shell: true });
|
|
77
|
+
if (r.stdout) {
|
|
78
|
+
try {
|
|
79
|
+
const j = JSON.parse(r.stdout);
|
|
80
|
+
if (j.dependencies?.leerness) out.npm.globalInstalled = j.dependencies.leerness.version;
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
83
|
+
} catch {}
|
|
84
|
+
// PATH 후보 (Windows: where / Unix: which)
|
|
85
|
+
try {
|
|
86
|
+
const isWin = process.platform === 'win32';
|
|
87
|
+
const tool = isWin ? 'where' : 'which';
|
|
88
|
+
const r = cp.spawnSync(tool, ['-a', 'leerness'], { encoding: 'utf8', timeout: 5000, shell: true });
|
|
89
|
+
if (r.stdout) out.pathCandidates = (r.stdout || '').trim().split(/\r?\n/).filter(Boolean);
|
|
90
|
+
} catch {}
|
|
91
|
+
// 진단: 글로벌 설치된 leerness 와 현재 실행 버전이 다르면 경고
|
|
92
|
+
out.diagnostics = [];
|
|
93
|
+
if (out.npm.globalInstalled && out.npm.globalInstalled !== VERSION) {
|
|
94
|
+
out.diagnostics.push(`⚠ 글로벌 설치 ${out.npm.globalInstalled} ≠ 현재 실행 ${VERSION} — npx 캐시 또는 PATH 충돌 의심`);
|
|
95
|
+
out.diagnostics.push(` → 강제 최신: npm i -g leerness@latest / 또는 npx --yes leerness@latest <command>`);
|
|
96
|
+
}
|
|
97
|
+
if (out.pathCandidates.length > 1) {
|
|
98
|
+
out.diagnostics.push(`⚠ PATH 에 leerness 가 ${out.pathCandidates.length}개 — 우선순위 충돌 가능`);
|
|
99
|
+
out.diagnostics.push(` → 명시적 경로 사용: ${out.runningFrom}`);
|
|
100
|
+
}
|
|
101
|
+
if (has('--json')) { log(JSON.stringify(out, null, 2)); return; }
|
|
102
|
+
log(`# leerness which (1.9.164)`);
|
|
103
|
+
log(`현재 실행: ${out.runningFrom}`);
|
|
104
|
+
log(`버전: v${out.version}`);
|
|
105
|
+
log(`Node: ${out.nodeVersion} (${out.platform}/${out.arch})`);
|
|
106
|
+
log('');
|
|
107
|
+
log(`## npm 환경`);
|
|
108
|
+
if (out.npm.globalRoot) log(` npm root -g: ${out.npm.globalRoot}`);
|
|
109
|
+
if (out.npm.cacheDir) log(` npm cache: ${out.npm.cacheDir} (npx 옛 버전이 여기 캐싱 — 의심 시 \`npm cache clean --force\`)`);
|
|
110
|
+
if (out.npm.globalInstalled) log(` 글로벌 설치: leerness@${out.npm.globalInstalled}`);
|
|
111
|
+
else log(` 글로벌 설치: (없음 — npx 또는 로컬 경로만 사용 중)`);
|
|
112
|
+
if (out.pathCandidates.length) {
|
|
113
|
+
log('');
|
|
114
|
+
log(`## PATH 후보 (${out.pathCandidates.length}개)`);
|
|
115
|
+
for (const p of out.pathCandidates) log(` - ${p}`);
|
|
116
|
+
}
|
|
117
|
+
if (out.diagnostics.length) {
|
|
118
|
+
log('');
|
|
119
|
+
log(`## ⚠ 진단`);
|
|
120
|
+
for (const d of out.diagnostics) log(` ${d}`);
|
|
121
|
+
} else {
|
|
122
|
+
log('');
|
|
123
|
+
log(`✓ 충돌 없음 (현재 실행 버전 = 글로벌 설치 버전)`);
|
|
124
|
+
}
|
|
125
|
+
log('');
|
|
126
|
+
log(`💡 강제 최신 실행 방법:`);
|
|
127
|
+
log(` 1) npx --yes leerness@latest <command> # npx 캐시 무시하고 최신 다운로드`);
|
|
128
|
+
log(` 2) npm i -g leerness@latest # 글로벌 설치 갱신`);
|
|
129
|
+
log(` 3) npm cache clean --force # npx 캐시 강제 비우기 (의심 시)`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 1.9.394 (UR-0025): whats-new — 현재 워크스페이스 버전 → 도구 버전 CHANGELOG 차분(신규 명령/플래그/파일 요약). introspection 핸들러.
|
|
133
|
+
// 순수 파서 _parseChangelogBetween(pure-utils) 사용. deps: VERSION/arg/has. CHANGELOG 경로는 root 우선, 없으면 pkg 자체(lib/../).
|
|
134
|
+
function whatsNewCmd(root, deps = {}) {
|
|
135
|
+
const { VERSION, arg, has } = deps;
|
|
136
|
+
root = absRoot(root || process.cwd());
|
|
137
|
+
const fromV = arg('--from', null) || (function () {
|
|
138
|
+
const hv = path.join(root, '.harness', 'HARNESS_VERSION');
|
|
139
|
+
if (exists(hv)) { try { return parseHarnessVersion(read(hv)).base || parseHarnessVersion(read(hv)).plus; } catch { return null; } }
|
|
140
|
+
return null;
|
|
141
|
+
})();
|
|
142
|
+
const toV = arg('--to', null) || VERSION;
|
|
143
|
+
if (!fromV) {
|
|
144
|
+
fail('현재 버전을 파악할 수 없습니다. --from <version> 명시');
|
|
145
|
+
return process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
// CHANGELOG.md — 우선 root, 없으면 leerness-pkg 자체 (lib/../CHANGELOG.md = pkg 루트)
|
|
148
|
+
let changelogPath = path.join(root, 'CHANGELOG.md');
|
|
149
|
+
if (!exists(changelogPath)) changelogPath = path.join(__dirname, '..', 'CHANGELOG.md');
|
|
150
|
+
if (!exists(changelogPath)) {
|
|
151
|
+
fail('CHANGELOG.md 없음');
|
|
152
|
+
return process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
const diff = _parseChangelogBetween(read(changelogPath), fromV, toV);
|
|
155
|
+
if (has('--json')) { log(JSON.stringify({ from: fromV, to: toV, versions: diff }, null, 2)); return; }
|
|
156
|
+
if (!diff.length) {
|
|
157
|
+
log(`# leerness whats-new (1.9.41)`);
|
|
158
|
+
log(`현재 ${fromV} ↔ 대상 ${toV}: 새 항목 없음 (또는 CHANGELOG에 기록 안 됨)`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
log(`# leerness whats-new (1.9.41)`);
|
|
162
|
+
log(`현재 워크스페이스 버전: ${fromV} → 대상: ${toV}`);
|
|
163
|
+
log(`범위: ${diff.length}개 버전 (${diff[0].version} → ${diff[diff.length - 1].version})`);
|
|
164
|
+
log('');
|
|
165
|
+
// AI 가독 요약 — 각 버전당 한 줄 + 신규 명령/플래그/파일
|
|
166
|
+
log(`## 🆕 신규 명령·플래그·파일 (AI 에이전트는 다음 명령을 우선 시도)`);
|
|
167
|
+
const allCommands = new Set();
|
|
168
|
+
const allFlags = new Set();
|
|
169
|
+
const allFiles = new Set();
|
|
170
|
+
for (const v of diff) {
|
|
171
|
+
v.newCommands.forEach(c => allCommands.add(c));
|
|
172
|
+
v.newFlags.forEach(f => allFlags.add(f));
|
|
173
|
+
v.newFiles.forEach(f => allFiles.add(f));
|
|
174
|
+
}
|
|
175
|
+
if (allCommands.size) log(` 📌 신규 명령: ${[...allCommands].join(', ')}`);
|
|
176
|
+
if (allFlags.size) log(` 🚩 신규 플래그: ${[...allFlags].join(', ')}`);
|
|
177
|
+
if (allFiles.size) log(` 📄 신규 파일: ${[...allFiles].join(', ')}`);
|
|
178
|
+
log('');
|
|
179
|
+
log(`## 📜 버전별 헤드라인`);
|
|
180
|
+
for (const v of diff) {
|
|
181
|
+
// body 첫 줄(또는 strong header) 추출
|
|
182
|
+
const firstLine = (v.body.match(/^\*\*([^*]+)\*\*/) || [])[1]
|
|
183
|
+
|| (v.body.split('\n').find(l => l.trim() && !l.startsWith('##')) || '').trim().slice(0, 120);
|
|
184
|
+
log(` • ${v.version}${v.date ? ` (${v.date})` : ''} — ${firstLine || '(no headline)'}`);
|
|
185
|
+
}
|
|
186
|
+
log('');
|
|
187
|
+
log(`## 💡 권장 행동`);
|
|
188
|
+
log(` 1. 위 신규 명령들을 시도해 보세요 (예: \`leerness <명령> --help\`)`);
|
|
189
|
+
log(` 2. 신규 파일들을 읽어 보세요 (예: \`cat .harness/session-workflow.md\`)`);
|
|
190
|
+
log(` 3. AGENTS.md/CLAUDE.md 재독 — migrate가 인스트럭션을 업데이트했을 수 있음`);
|
|
191
|
+
log(` 4. 상세: \`cat CHANGELOG.md\` 또는 \`leerness whats-new --json\``);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = { doctorCmd, whichCmd, whatsNewCmd };
|