leerness 1.36.183 → 1.36.185

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.
@@ -1,13 +1,13 @@
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 스코프를 유지하므로 함수 참조 주입만으로 정상 동작.
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
6
  'use strict';
7
7
  const cp = require('child_process');
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
- const { log, fail, absRoot, exists, read } = require('./io');
10
+ const { log, fail, absRoot, exists, read } = require('./io');
11
11
  const { parseHarnessVersion, _parseChangelogBetween, compareVer } = require('./pure-utils');
12
12
  const { spawnNpmSync } = require('./npm-process');
13
13
  const { windowsPathCandidates } = require('./portable-process');
@@ -148,7 +148,7 @@ function _groupPathInstallations(candidates, options = {}) {
148
148
  }
149
149
  return groups;
150
150
  }
151
-
151
+
152
152
  function doctorCmd(opts = {}, deps = {}) {
153
153
  const { VERSION, uiLang, _selfTestCases, _selfTestReport, _controlSelftestEnv,
154
154
  _detectShellCtx, _mcpToolCount, has, harnessPath } = deps;
@@ -177,67 +177,67 @@ function doctorCmd(opts = {}, deps = {}) {
177
177
  finally { try { if (controlledEnv && typeof controlledEnv.restore === 'function') controlledEnv.restore(); } catch {} }
178
178
  total = pass + failNames.length;
179
179
  }
180
- // 2) 셸/PowerShell 컨텍스트 (UR-0052)
181
- let shell = null, psVersion = null;
182
- try { const ctx = _detectShellCtx(); shell = ctx.shell; psVersion = ctx.psVersion; } catch {}
183
- const mcpCount = (() => { try { return _mcpToolCount(); } catch { return null; } })();
184
- // 1.36.106 (T-0090): selftest/doctor 가 임시 디렉토리를 흘려 왔다(실행당 8/3개). 새는 자리는 고쳤지만
185
- // **이미 쌓인 것**은 사용자 디스크다 — 조용히 지우지 않고 사실만 알린다(삭제는 사용자 판단).
186
- // 비용은 readdir 1회이고 stat 은 하지 않는다. 실패는 진단 실패가 아니므로 통째로 삼킨다.
187
- // codex 검수 LOW#4/#5: 처음엔 readdir 이름만 봤다 — 그래서 (a) `leerness-note.txt` 같은 **파일**을
188
- // "임시 디렉토리" 로 세고, (b) Windows(대소문자 무시 파일시스템)에서 `__LEERNESS_*` 를 놓쳤다.
189
- // withFileTypes 로 디렉토리만 세고, 접두 비교는 대소문자를 무시한다.
190
- let tmpLeftover = null;
191
- try {
192
- const tdir = require('os').tmpdir();
193
- const ents = require('fs').readdirSync(tdir, { withFileTypes: true });
194
- const n = ents.filter(e => e.isDirectory() && /^(__leerness_|leerness-)/i.test(e.name)).length;
195
- if (n > 200) tmpLeftover = { dir: tdir, count: n };
196
- } catch { tmpLeftover = null; }
197
- const report = {
198
- version: VERSION, node: process.version, platform: process.platform + '/' + process.arch,
199
- runningFrom: harnessPath, mcpTools: mcpCount,
200
- selftest: { pass, total, ok: failNames.length === 0, failed: failNames },
201
- shell, psVersion, tmpLeftover, healthy: failNames.length === 0
202
- };
203
- if (json) { process.stdout.write(JSON.stringify(report, null, 2) + '\n'); if (!report.healthy) process.exitCode = 1; return report; }
204
- const isTty = process.stdout && process.stdout.isTTY;
205
- 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;
206
- log(cy(t(`# leerness doctor — 설치/환경 진단`, `# leerness doctor — install/environment diagnosis`)));
207
- log('');
208
- log(` ${gr('✓')} version ${VERSION} · node ${process.version} · ${process.platform}/${process.arch}`);
209
- log(` ${gr('✓')} ${t('설치 경로', 'install path')}: ${dm(harnessPath)}`);
210
- log(` ${gr('✓')} ${t('MCP 도구', 'MCP tools')}: ${mcpCount}`);
211
- // 1.13.1 (15th 블라인드 리뷰 P3, Sonnet): pass 수에 '실패' 가 붙어 "209/210 실패"(=209건 실패로 오독)되던 문구 → "통과 N/M (K건 실패)" 로 명확화.
212
- log(` ${report.selftest.ok ? gr('✓') : rd('✗')} selftest: ${pass}/${total} ${t('통과', 'passed')}${report.selftest.ok ? '' : t(` (${total - pass}건 실패)`, ` (${total - pass} failed)`)}`);
213
- if (!report.selftest.ok) report.selftest.failed.slice(0, 5).forEach(n => log(rd(` ✗ ${n}`)));
214
- log(` ${gr('✓')} ${t('셸', 'shell')}: ${shell || 'unknown'}${psVersion && shell === 'powershell' ? ` (PowerShell ${psVersion})` : ''}`);
215
- // 남은 임시 디렉토리는 '문제 감지'(healthy)로 올리지 않는다 — 동작에 지장이 없고, 과거 버전이 남긴 흔적이다.
216
- // 지우는 명령을 대신 실행하지도 않는다: 경로를 알려주고 사용자가 판단한다.
217
- if (tmpLeftover) {
218
- log(` ${dm('ⓘ')} ${t(`임시 디렉토리 잔존 ${tmpLeftover.count}개 (1.36.106 이전 버전이 남긴 것 — 새 실행은 흘리지 않습니다)`, `${tmpLeftover.count} leftover temp dirs (created by versions before 1.36.106 — current runs leak none)`)}`);
219
- log(` ${dm(tmpLeftover.dir)} ${dm(t('— 필요하면 직접 정리하세요(leerness 는 지우지 않습니다)', '— remove them yourself if you want (leerness will not)'))}`);
220
- }
221
- log('');
222
- log(report.healthy ? gr(t(' ✓ leerness 설치 정상', ' ✓ leerness install OK')) : rd(t(' ✗ 문제 감지 — 재설치: npm i -g leerness@latest · 진단: leerness which', ' ✗ problem detected — reinstall: npm i -g leerness@latest · diagnose: leerness which')));
223
- if (!report.healthy) process.exitCode = 1;
224
- return report;
225
- }
226
-
180
+ // 2) 셸/PowerShell 컨텍스트 (UR-0052)
181
+ let shell = null, psVersion = null;
182
+ try { const ctx = _detectShellCtx(); shell = ctx.shell; psVersion = ctx.psVersion; } catch {}
183
+ const mcpCount = (() => { try { return _mcpToolCount(); } catch { return null; } })();
184
+ // 1.36.106 (T-0090): selftest/doctor 가 임시 디렉토리를 흘려 왔다(실행당 8/3개). 새는 자리는 고쳤지만
185
+ // **이미 쌓인 것**은 사용자 디스크다 — 조용히 지우지 않고 사실만 알린다(삭제는 사용자 판단).
186
+ // 비용은 readdir 1회이고 stat 은 하지 않는다. 실패는 진단 실패가 아니므로 통째로 삼킨다.
187
+ // codex 검수 LOW#4/#5: 처음엔 readdir 이름만 봤다 — 그래서 (a) `leerness-note.txt` 같은 **파일**을
188
+ // "임시 디렉토리" 로 세고, (b) Windows(대소문자 무시 파일시스템)에서 `__LEERNESS_*` 를 놓쳤다.
189
+ // withFileTypes 로 디렉토리만 세고, 접두 비교는 대소문자를 무시한다.
190
+ let tmpLeftover = null;
191
+ try {
192
+ const tdir = require('os').tmpdir();
193
+ const ents = require('fs').readdirSync(tdir, { withFileTypes: true });
194
+ const n = ents.filter(e => e.isDirectory() && /^(__leerness_|leerness-)/i.test(e.name)).length;
195
+ if (n > 200) tmpLeftover = { dir: tdir, count: n };
196
+ } catch { tmpLeftover = null; }
197
+ const report = {
198
+ version: VERSION, node: process.version, platform: process.platform + '/' + process.arch,
199
+ runningFrom: harnessPath, mcpTools: mcpCount,
200
+ selftest: { pass, total, ok: failNames.length === 0, failed: failNames },
201
+ shell, psVersion, tmpLeftover, healthy: failNames.length === 0
202
+ };
203
+ if (json) { process.stdout.write(JSON.stringify(report, null, 2) + '\n'); if (!report.healthy) process.exitCode = 1; return report; }
204
+ const isTty = process.stdout && process.stdout.isTTY;
205
+ 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;
206
+ log(cy(t(`# leerness doctor — 설치/환경 진단`, `# leerness doctor — install/environment diagnosis`)));
207
+ log('');
208
+ log(` ${gr('✓')} version ${VERSION} · node ${process.version} · ${process.platform}/${process.arch}`);
209
+ log(` ${gr('✓')} ${t('설치 경로', 'install path')}: ${dm(harnessPath)}`);
210
+ log(` ${gr('✓')} ${t('MCP 도구', 'MCP tools')}: ${mcpCount}`);
211
+ // 1.13.1 (15th 블라인드 리뷰 P3, Sonnet): pass 수에 '실패' 가 붙어 "209/210 실패"(=209건 실패로 오독)되던 문구 → "통과 N/M (K건 실패)" 로 명확화.
212
+ log(` ${report.selftest.ok ? gr('✓') : rd('✗')} selftest: ${pass}/${total} ${t('통과', 'passed')}${report.selftest.ok ? '' : t(` (${total - pass}건 실패)`, ` (${total - pass} failed)`)}`);
213
+ if (!report.selftest.ok) report.selftest.failed.slice(0, 5).forEach(n => log(rd(` ✗ ${n}`)));
214
+ log(` ${gr('✓')} ${t('셸', 'shell')}: ${shell || 'unknown'}${psVersion && shell === 'powershell' ? ` (PowerShell ${psVersion})` : ''}`);
215
+ // 남은 임시 디렉토리는 '문제 감지'(healthy)로 올리지 않는다 — 동작에 지장이 없고, 과거 버전이 남긴 흔적이다.
216
+ // 지우는 명령을 대신 실행하지도 않는다: 경로를 알려주고 사용자가 판단한다.
217
+ if (tmpLeftover) {
218
+ log(` ${dm('ⓘ')} ${t(`임시 디렉토리 잔존 ${tmpLeftover.count}개 (1.36.106 이전 버전이 남긴 것 — 새 실행은 흘리지 않습니다)`, `${tmpLeftover.count} leftover temp dirs (created by versions before 1.36.106 — current runs leak none)`)}`);
219
+ log(` ${dm(tmpLeftover.dir)} ${dm(t('— 필요하면 직접 정리하세요(leerness 는 지우지 않습니다)', '— remove them yourself if you want (leerness will not)'))}`);
220
+ }
221
+ log('');
222
+ log(report.healthy ? gr(t(' ✓ leerness 설치 정상', ' ✓ leerness install OK')) : rd(t(' ✗ 문제 감지 — 재설치: npm i -g leerness@latest · 진단: leerness which', ' ✗ problem detected — reinstall: npm i -g leerness@latest · diagnose: leerness which')));
223
+ if (!report.healthy) process.exitCode = 1;
224
+ return report;
225
+ }
226
+
227
227
  function whichCmd(deps = {}) {
228
228
  const { VERSION, uiLang, has, harnessPath } = deps;
229
229
  const t = (ko, en) => (uiLang === 'en' ? en : ko); // 1.28.2 (UR-0010 Phase 10c)
230
230
  const platform = deps.platform || process.platform;
231
231
  const out = {
232
- version: VERSION,
233
- runningFrom: harnessPath,
234
- nodeVersion: process.version,
232
+ version: VERSION,
233
+ runningFrom: harnessPath,
234
+ nodeVersion: process.version,
235
235
  platform,
236
- arch: process.arch,
237
- npm: {},
236
+ arch: process.arch,
237
+ npm: {},
238
238
  pathCandidates: [],
239
239
  pathInstallations: []
240
- };
240
+ };
241
241
  if (!deps.skipNpm) {
242
242
  // npm root -g (글로벌 설치 경로)
243
243
  try {
@@ -276,28 +276,28 @@ function whichCmd(deps = {}) {
276
276
  platform, command: 'leerness', packageName: 'leerness',
277
277
  ...(typeof deps.packageIdentity === 'function' ? { packageIdentity: deps.packageIdentity } : {}),
278
278
  });
279
- // 진단: 글로벌 설치된 leerness 와 현재 실행 버전이 다르면 경고
280
- out.diagnostics = [];
281
- if (out.npm.globalInstalled && out.npm.globalInstalled !== VERSION) {
282
- out.diagnostics.push(t(`⚠ 글로벌 설치 ${out.npm.globalInstalled} ≠ 현재 실행 ${VERSION} — npx 캐시 또는 PATH 충돌 의심`, `⚠ global install ${out.npm.globalInstalled} ≠ running ${VERSION} — suspect npx cache or PATH conflict`));
283
- out.diagnostics.push(t(` → 강제 최신: npm i -g leerness@latest / 또는 npx --yes leerness@latest <command>`, ` → force latest: npm i -g leerness@latest / or npx --yes leerness@latest <command>`));
284
- }
279
+ // 진단: 글로벌 설치된 leerness 와 현재 실행 버전이 다르면 경고
280
+ out.diagnostics = [];
281
+ if (out.npm.globalInstalled && out.npm.globalInstalled !== VERSION) {
282
+ out.diagnostics.push(t(`⚠ 글로벌 설치 ${out.npm.globalInstalled} ≠ 현재 실행 ${VERSION} — npx 캐시 또는 PATH 충돌 의심`, `⚠ global install ${out.npm.globalInstalled} ≠ running ${VERSION} — suspect npx cache or PATH conflict`));
283
+ out.diagnostics.push(t(` → 강제 최신: npm i -g leerness@latest / 또는 npx --yes leerness@latest <command>`, ` → force latest: npm i -g leerness@latest / or npx --yes leerness@latest <command>`));
284
+ }
285
285
  if (out.pathInstallations.length > 1) {
286
286
  out.diagnostics.push(t(`⚠ PATH 에 leerness 설치가 ${out.pathInstallations.length}개 — 우선순위 충돌 가능`, `⚠ ${out.pathInstallations.length} leerness installations appear on PATH — possible precedence conflict`));
287
- out.diagnostics.push(t(` → 명시적 경로 사용: ${out.runningFrom}`, ` → use the explicit path: ${out.runningFrom}`));
288
- }
287
+ out.diagnostics.push(t(` → 명시적 경로 사용: ${out.runningFrom}`, ` → use the explicit path: ${out.runningFrom}`));
288
+ }
289
289
  if (deps.quiet) return out;
290
290
  if (has('--json')) { log(JSON.stringify(out, null, 2)); return out; }
291
- log(`# leerness which (1.9.164)`);
292
- log(`${t('현재 실행', 'running from')}: ${out.runningFrom}`);
293
- log(`${t('버전', 'version')}: v${out.version}`);
294
- log(`Node: ${out.nodeVersion} (${out.platform}/${out.arch})`);
295
- log('');
296
- log(t(`## npm 환경`, `## npm environment`));
297
- if (out.npm.globalRoot) log(` npm root -g: ${out.npm.globalRoot}`);
298
- if (out.npm.cacheDir) log(t(` npm cache: ${out.npm.cacheDir} (npx 옛 버전이 여기 캐싱 — 의심 시 \`npm cache clean --force\`)`, ` npm cache: ${out.npm.cacheDir} (npx caches old versions here — if suspect: \`npm cache clean --force\`)`));
299
- if (out.npm.globalInstalled) log(t(` 글로벌 설치: leerness@${out.npm.globalInstalled}`, ` global install: leerness@${out.npm.globalInstalled}`));
300
- else log(t(` 글로벌 설치: (없음 — npx 또는 로컬 경로만 사용 중)`, ` global install: (none — using npx or a local path only)`));
291
+ log(`# leerness which (1.9.164)`);
292
+ log(`${t('현재 실행', 'running from')}: ${out.runningFrom}`);
293
+ log(`${t('버전', 'version')}: v${out.version}`);
294
+ log(`Node: ${out.nodeVersion} (${out.platform}/${out.arch})`);
295
+ log('');
296
+ log(t(`## npm 환경`, `## npm environment`));
297
+ if (out.npm.globalRoot) log(` npm root -g: ${out.npm.globalRoot}`);
298
+ if (out.npm.cacheDir) log(t(` npm cache: ${out.npm.cacheDir} (npx 옛 버전이 여기 캐싱 — 의심 시 \`npm cache clean --force\`)`, ` npm cache: ${out.npm.cacheDir} (npx caches old versions here — if suspect: \`npm cache clean --force\`)`));
299
+ if (out.npm.globalInstalled) log(t(` 글로벌 설치: leerness@${out.npm.globalInstalled}`, ` global install: leerness@${out.npm.globalInstalled}`));
300
+ else log(t(` 글로벌 설치: (없음 — npx 또는 로컬 경로만 사용 중)`, ` global install: (none — using npx or a local path only)`));
301
301
  if (out.pathCandidates.length) {
302
302
  log('');
303
303
  log(t(`## PATH 후보 (${out.pathCandidates.length}개 · 설치 ${out.pathInstallations.length}개)`, `## PATH candidates (${out.pathCandidates.length} · ${out.pathInstallations.length} installation(s))`));
@@ -307,113 +307,113 @@ function whichCmd(deps = {}) {
307
307
  const variants = groupedShims.reduce((sum, group) => sum + group.candidates.length, 0);
308
308
  log(t(` ⓘ 동일 npm 설치의 shim 변형 ${variants}개를 설치 ${groupedShims.length}개로 그룹화`, ` ⓘ grouped ${variants} shim variants into ${groupedShims.length} npm installation(s)`));
309
309
  }
310
- }
311
- if (out.diagnostics.length) {
312
- log('');
313
- log(t(`## ⚠ 진단`, `## ⚠ diagnostics`));
314
- for (const d of out.diagnostics) log(` ${d}`);
315
- } else {
316
- log('');
317
- log(t(`✓ 충돌 없음 (현재 실행 버전 = 글로벌 설치 버전)`, `✓ no conflict (running version = global install version)`));
318
- }
319
- log('');
320
- log(t(`💡 강제 최신 실행 방법:`, `💡 how to force the latest:`));
321
- log(t(` 1) npx --yes leerness@latest <command> # npx 캐시 무시하고 최신 다운로드`, ` 1) npx --yes leerness@latest <command> # bypass npx cache, download latest`));
322
- log(t(` 2) npm i -g leerness@latest # 글로벌 설치 갱신`, ` 2) npm i -g leerness@latest # update the global install`));
310
+ }
311
+ if (out.diagnostics.length) {
312
+ log('');
313
+ log(t(`## ⚠ 진단`, `## ⚠ diagnostics`));
314
+ for (const d of out.diagnostics) log(` ${d}`);
315
+ } else {
316
+ log('');
317
+ log(t(`✓ 충돌 없음 (현재 실행 버전 = 글로벌 설치 버전)`, `✓ no conflict (running version = global install version)`));
318
+ }
319
+ log('');
320
+ log(t(`💡 강제 최신 실행 방법:`, `💡 how to force the latest:`));
321
+ log(t(` 1) npx --yes leerness@latest <command> # npx 캐시 무시하고 최신 다운로드`, ` 1) npx --yes leerness@latest <command> # bypass npx cache, download latest`));
322
+ log(t(` 2) npm i -g leerness@latest # 글로벌 설치 갱신`, ` 2) npm i -g leerness@latest # update the global install`));
323
323
  log(t(` 3) npm cache clean --force # npx 캐시 강제 비우기 (의심 시)`, ` 3) npm cache clean --force # force-clear the npx cache (if suspect)`));
324
324
  return out;
325
325
  }
326
-
327
- // 1.9.394 (UR-0025): whats-new — 현재 워크스페이스 버전 → 도구 버전 CHANGELOG 차분(신규 명령/플래그/파일 요약). introspection 핸들러.
328
- // 순수 파서 _parseChangelogBetween(pure-utils) 사용. deps: VERSION/arg/has. CHANGELOG 경로는 root 우선, 없으면 pkg 자체(lib/../).
329
- function whatsNewCmd(root, deps = {}) {
330
- const { VERSION, uiLang, arg, has } = deps;
331
- const t = (ko, en) => (uiLang === 'en' ? en : ko); // 1.28.2 (UR-0010 Phase 10c)
332
- root = absRoot(root || process.cwd());
333
- const fromV = arg('--from', null) || (function () {
334
- const hv = path.join(root, '.leerness', 'HARNESS_VERSION');
335
- if (exists(hv)) { try { return parseHarnessVersion(read(hv)).base || parseHarnessVersion(read(hv)).plus; } catch { return null; } }
336
- return null;
337
- })();
338
- const toV = arg('--to', null) || VERSION;
339
- if (!fromV) {
340
- fail(t('현재 버전을 파악할 수 없습니다. --from <version> 명시', 'cannot determine current version. specify --from <version>'));
341
- return process.exit(1);
342
- }
343
- // 1.36.49 (codex 6차 #10): 역순 범위(from > to)는 빈 성공이 아니라 명시 오류 — 인자 순서 착오를 조용히 삼키지 않는다.
344
- if (compareVer(fromV, toV) > 0) {
345
- if (has('--json')) { log(JSON.stringify({ ok: false, code: 'invalid_range', error: `--from(${fromV}) 이 --to(${toV}) 보다 큼 — 인자 순서 확인` }, null, 2)); return process.exit(1); }
346
- fail(t(`--from(${fromV}) 이 --to(${toV}) 보다 큼 — 인자 순서 확인`, `--from (${fromV}) is newer than --to (${toV}) — check argument order`));
347
- return process.exit(1);
348
- }
349
- // CHANGELOG.md — 우선 root, 없으면 leerness-pkg 자체 (lib/../CHANGELOG.md = pkg 루트)
350
- let changelogPath = path.join(root, 'CHANGELOG.md');
351
- if (!exists(changelogPath)) changelogPath = path.join(__dirname, '..', 'CHANGELOG.md');
352
- if (!exists(changelogPath)) {
353
- fail(t('CHANGELOG.md 없음', 'CHANGELOG.md not found'));
354
- return process.exit(1);
355
- }
356
- const diff = _parseChangelogBetween(read(changelogPath), fromV, toV);
357
- if (has('--json')) {
358
- // 1.36.69 (성장 한계 클래스): CHANGELOG 누적(1MB+)으로 --json 이 800KB 를 넘겨 AI 소비자의 컨텍스트를 폭파시켰다.
359
- // 기본은 최신 30개 버전으로 제한(에이전트가 실제로 읽는 범위) + 절단 사실을 정직하게 표기. 전체는 --all/--limit 로 옵트인.
360
- // (검수 #2) 무효/비양수 limit 은 기본값으로 폴백 — `--limit -5` 가 1 이 되던 비일관 제거
361
- const _rawLim = parseInt(arg('--limit', '30'), 10);
362
- const _lim = has('--all') ? diff.length : (Number.isFinite(_rawLim) && _rawLim > 0 ? _rawLim : 30);
363
- const _shown = diff.slice(0, _lim);
364
- const _truncated = _shown.length < diff.length;
365
- log(JSON.stringify({
366
- from: fromV, to: toV,
367
- versions: _shown,
368
- totalVersions: diff.length,
369
- shown: _shown.length,
370
- truncated: _truncated,
371
- // (검수 #3) 힌트도 UI 언어 준수
372
- ...(_truncated ? { hint: t(`최신 ${_shown.length}개만 표시 — 전체는 --all, 개수 조절은 --limit N`, `showing newest ${_shown.length} only — use --all for everything, --limit N to adjust`) } : {})
373
- }, null, 2));
374
- return;
375
- }
376
- if (!diff.length) {
377
- log(`# leerness whats-new (1.9.41)`);
378
- log(t(`현재 ${fromV} ↔ 대상 ${toV}: 새 항목 없음 (또는 CHANGELOG에 기록 안 됨)`, `current ${fromV} ↔ target ${toV}: no new entries (or not recorded in CHANGELOG)`));
379
- return;
380
- }
381
- log(`# leerness whats-new (1.9.41)`);
382
- log(t(`현재 워크스페이스 버전: ${fromV} → 대상: ${toV}`, `current workspace version: ${fromV} → target: ${toV}`));
383
- log(t(`범위: ${diff.length}개 버전 (${diff[0].version} → ${diff[diff.length - 1].version})`, `range: ${diff.length} version(s) (${diff[0].version} → ${diff[diff.length - 1].version})`));
384
- log('');
385
- // AI 가독 요약 — 각 버전당 한 줄 + 신규 명령/플래그/파일
386
- log(t(`## 🆕 신규 명령·플래그·파일 (AI 에이전트는 다음 명령을 우선 시도)`, `## 🆕 new commands·flags·files (AI agents: try these first)`));
387
- const allCommands = new Set();
388
- const allFlags = new Set();
389
- const allFiles = new Set();
390
- for (const v of diff) {
391
- v.newCommands.forEach(c => allCommands.add(c));
392
- v.newFlags.forEach(f => allFlags.add(f));
393
- v.newFiles.forEach(f => allFiles.add(f));
394
- }
395
- if (allCommands.size) log(t(` 📌 신규 명령: ${[...allCommands].join(', ')}`, ` 📌 new commands: ${[...allCommands].join(', ')}`));
396
- if (allFlags.size) log(t(` 🚩 신규 플래그: ${[...allFlags].join(', ')}`, ` 🚩 new flags: ${[...allFlags].join(', ')}`));
397
- if (allFiles.size) log(t(` 📄 신규 파일: ${[...allFiles].join(', ')}`, ` 📄 new files: ${[...allFiles].join(', ')}`));
398
- log('');
399
- log(t(`## 📜 버전별 헤드라인`, `## 📜 per-version headlines`));
400
- // 1.36.70 (검수 #1): migrate 보고서가 광고하는 `--limit N` 이 텍스트 모드에서 조용히 무시되던 것 —
401
- // 명시적 --limit 만 존중(기본 텍스트 출력은 전체 유지, 계약 무변경).
402
- const _txtRawLim = parseInt(arg('--limit', ''), 10);
403
- const _txtLim = (has('--limit') && Number.isFinite(_txtRawLim) && _txtRawLim > 0) ? _txtRawLim : diff.length;
404
- for (const v of diff.slice(0, _txtLim)) {
405
- // body 첫 줄(또는 strong header) 추출
406
- const firstLine = (v.body.match(/^\*\*([^*]+)\*\*/) || [])[1]
407
- || (v.body.split('\n').find(l => l.trim() && !l.startsWith('##')) || '').trim().slice(0, 120);
408
- log(` • ${v.version}${v.date ? ` (${v.date})` : ''} — ${firstLine || '(no headline)'}`);
409
- }
410
- if (_txtLim < diff.length) log(t(` … 외 ${diff.length - _txtLim}개 버전 (전체: --all 또는 --limit 생략)`, ` … ${diff.length - _txtLim} more version(s) (drop --limit or use --all for everything)`));
411
- log('');
412
- log(t(`## 💡 권장 행동`, `## 💡 recommended actions`));
413
- log(t(` 1. 위 신규 명령들을 시도해 보세요 (예: \`leerness <명령> --help\`)`, ` 1. Try the new commands above (e.g. \`leerness <command> --help\`)`));
414
- log(t(` 2. 신규 파일들을 읽어 보세요 (예: \`cat .leerness/session-workflow.md\`)`, ` 2. Read the new files (e.g. \`cat .leerness/session-workflow.md\`)`));
415
- log(t(` 3. AGENTS.md/CLAUDE.md 재독 — migrate가 인스트럭션을 업데이트했을 수 있음`, ` 3. Re-read AGENTS.md/CLAUDE.md — migrate may have updated the instructions`));
416
- log(t(` 4. 상세: \`cat CHANGELOG.md\` 또는 \`leerness whats-new --json\``, ` 4. Details: \`cat CHANGELOG.md\` or \`leerness whats-new --json\``));
417
- }
418
-
326
+
327
+ // 1.9.394 (UR-0025): whats-new — 현재 워크스페이스 버전 → 도구 버전 CHANGELOG 차분(신규 명령/플래그/파일 요약). introspection 핸들러.
328
+ // 순수 파서 _parseChangelogBetween(pure-utils) 사용. deps: VERSION/arg/has. CHANGELOG 경로는 root 우선, 없으면 pkg 자체(lib/../).
329
+ function whatsNewCmd(root, deps = {}) {
330
+ const { VERSION, uiLang, arg, has } = deps;
331
+ const t = (ko, en) => (uiLang === 'en' ? en : ko); // 1.28.2 (UR-0010 Phase 10c)
332
+ root = absRoot(root || process.cwd());
333
+ const fromV = arg('--from', null) || (function () {
334
+ const hv = path.join(root, '.leerness', 'HARNESS_VERSION');
335
+ if (exists(hv)) { try { return parseHarnessVersion(read(hv)).base || parseHarnessVersion(read(hv)).plus; } catch { return null; } }
336
+ return null;
337
+ })();
338
+ const toV = arg('--to', null) || VERSION;
339
+ if (!fromV) {
340
+ fail(t('현재 버전을 파악할 수 없습니다. --from <version> 명시', 'cannot determine current version. specify --from <version>'));
341
+ return process.exit(1);
342
+ }
343
+ // 1.36.49 (codex 6차 #10): 역순 범위(from > to)는 빈 성공이 아니라 명시 오류 — 인자 순서 착오를 조용히 삼키지 않는다.
344
+ if (compareVer(fromV, toV) > 0) {
345
+ if (has('--json')) { log(JSON.stringify({ ok: false, code: 'invalid_range', error: `--from(${fromV}) 이 --to(${toV}) 보다 큼 — 인자 순서 확인` }, null, 2)); return process.exit(1); }
346
+ fail(t(`--from(${fromV}) 이 --to(${toV}) 보다 큼 — 인자 순서 확인`, `--from (${fromV}) is newer than --to (${toV}) — check argument order`));
347
+ return process.exit(1);
348
+ }
349
+ // CHANGELOG.md — 우선 root, 없으면 leerness-pkg 자체 (lib/../CHANGELOG.md = pkg 루트)
350
+ let changelogPath = path.join(root, 'CHANGELOG.md');
351
+ if (!exists(changelogPath)) changelogPath = path.join(__dirname, '..', 'CHANGELOG.md');
352
+ if (!exists(changelogPath)) {
353
+ fail(t('CHANGELOG.md 없음', 'CHANGELOG.md not found'));
354
+ return process.exit(1);
355
+ }
356
+ const diff = _parseChangelogBetween(read(changelogPath), fromV, toV);
357
+ if (has('--json')) {
358
+ // 1.36.69 (성장 한계 클래스): CHANGELOG 누적(1MB+)으로 --json 이 800KB 를 넘겨 AI 소비자의 컨텍스트를 폭파시켰다.
359
+ // 기본은 최신 30개 버전으로 제한(에이전트가 실제로 읽는 범위) + 절단 사실을 정직하게 표기. 전체는 --all/--limit 로 옵트인.
360
+ // (검수 #2) 무효/비양수 limit 은 기본값으로 폴백 — `--limit -5` 가 1 이 되던 비일관 제거
361
+ const _rawLim = parseInt(arg('--limit', '30'), 10);
362
+ const _lim = has('--all') ? diff.length : (Number.isFinite(_rawLim) && _rawLim > 0 ? _rawLim : 30);
363
+ const _shown = diff.slice(0, _lim);
364
+ const _truncated = _shown.length < diff.length;
365
+ log(JSON.stringify({
366
+ from: fromV, to: toV,
367
+ versions: _shown,
368
+ totalVersions: diff.length,
369
+ shown: _shown.length,
370
+ truncated: _truncated,
371
+ // (검수 #3) 힌트도 UI 언어 준수
372
+ ...(_truncated ? { hint: t(`최신 ${_shown.length}개만 표시 — 전체는 --all, 개수 조절은 --limit N`, `showing newest ${_shown.length} only — use --all for everything, --limit N to adjust`) } : {})
373
+ }, null, 2));
374
+ return;
375
+ }
376
+ if (!diff.length) {
377
+ log(`# leerness whats-new (1.9.41)`);
378
+ log(t(`현재 ${fromV} ↔ 대상 ${toV}: 새 항목 없음 (또는 CHANGELOG에 기록 안 됨)`, `current ${fromV} ↔ target ${toV}: no new entries (or not recorded in CHANGELOG)`));
379
+ return;
380
+ }
381
+ log(`# leerness whats-new (1.9.41)`);
382
+ log(t(`현재 워크스페이스 버전: ${fromV} → 대상: ${toV}`, `current workspace version: ${fromV} → target: ${toV}`));
383
+ log(t(`범위: ${diff.length}개 버전 (${diff[0].version} → ${diff[diff.length - 1].version})`, `range: ${diff.length} version(s) (${diff[0].version} → ${diff[diff.length - 1].version})`));
384
+ log('');
385
+ // AI 가독 요약 — 각 버전당 한 줄 + 신규 명령/플래그/파일
386
+ log(t(`## 🆕 신규 명령·플래그·파일 (AI 에이전트는 다음 명령을 우선 시도)`, `## 🆕 new commands·flags·files (AI agents: try these first)`));
387
+ const allCommands = new Set();
388
+ const allFlags = new Set();
389
+ const allFiles = new Set();
390
+ for (const v of diff) {
391
+ v.newCommands.forEach(c => allCommands.add(c));
392
+ v.newFlags.forEach(f => allFlags.add(f));
393
+ v.newFiles.forEach(f => allFiles.add(f));
394
+ }
395
+ if (allCommands.size) log(t(` 📌 신규 명령: ${[...allCommands].join(', ')}`, ` 📌 new commands: ${[...allCommands].join(', ')}`));
396
+ if (allFlags.size) log(t(` 🚩 신규 플래그: ${[...allFlags].join(', ')}`, ` 🚩 new flags: ${[...allFlags].join(', ')}`));
397
+ if (allFiles.size) log(t(` 📄 신규 파일: ${[...allFiles].join(', ')}`, ` 📄 new files: ${[...allFiles].join(', ')}`));
398
+ log('');
399
+ log(t(`## 📜 버전별 헤드라인`, `## 📜 per-version headlines`));
400
+ // 1.36.70 (검수 #1): migrate 보고서가 광고하는 `--limit N` 이 텍스트 모드에서 조용히 무시되던 것 —
401
+ // 명시적 --limit 만 존중(기본 텍스트 출력은 전체 유지, 계약 무변경).
402
+ const _txtRawLim = parseInt(arg('--limit', ''), 10);
403
+ const _txtLim = (has('--limit') && Number.isFinite(_txtRawLim) && _txtRawLim > 0) ? _txtRawLim : diff.length;
404
+ for (const v of diff.slice(0, _txtLim)) {
405
+ // body 첫 줄(또는 strong header) 추출
406
+ const firstLine = (v.body.match(/^\*\*([^*]+)\*\*/) || [])[1]
407
+ || (v.body.split('\n').find(l => l.trim() && !l.startsWith('##')) || '').trim().slice(0, 120);
408
+ log(` • ${v.version}${v.date ? ` (${v.date})` : ''} — ${firstLine || '(no headline)'}`);
409
+ }
410
+ if (_txtLim < diff.length) log(t(` … 외 ${diff.length - _txtLim}개 버전 (전체: --all 또는 --limit 생략)`, ` … ${diff.length - _txtLim} more version(s) (drop --limit or use --all for everything)`));
411
+ log('');
412
+ log(t(`## 💡 권장 행동`, `## 💡 recommended actions`));
413
+ log(t(` 1. 위 신규 명령들을 시도해 보세요 (예: \`leerness <명령> --help\`)`, ` 1. Try the new commands above (e.g. \`leerness <command> --help\`)`));
414
+ log(t(` 2. 신규 파일들을 읽어 보세요 (예: \`cat .leerness/session-workflow.md\`)`, ` 2. Read the new files (e.g. \`cat .leerness/session-workflow.md\`)`));
415
+ log(t(` 3. AGENTS.md/CLAUDE.md 재독 — migrate가 인스트럭션을 업데이트했을 수 있음`, ` 3. Re-read AGENTS.md/CLAUDE.md — migrate may have updated the instructions`));
416
+ log(t(` 4. 상세: \`cat CHANGELOG.md\` 또는 \`leerness whats-new --json\``, ` 4. Details: \`cat CHANGELOG.md\` or \`leerness whats-new --json\``));
417
+ }
418
+
419
419
  module.exports = { doctorCmd, whichCmd, whatsNewCmd, _groupPathInstallations, _npmPackageIdentity };