leerness 1.9.70 → 1.9.72
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 +45 -0
- package/README.md +4 -2
- package/bin/harness.js +173 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.72 — 2026-05-20
|
|
4
|
+
|
|
5
|
+
**`leerness brainstorm`에 skill-suggestions.md history + task-log 실패 라인 통합**.
|
|
6
|
+
|
|
7
|
+
### Improved — brainstorm 자원 회수 확장
|
|
8
|
+
- 기존: decisions / skills / tasks / rules / evidence / lessons.
|
|
9
|
+
- **신규**: `skillHistory` (1.9.68 rolling history) + `taskLogFails` (1.9.67 task-log 실패 라인).
|
|
10
|
+
- 출력 추가 섹션:
|
|
11
|
+
- `📒 같은 주제 이전 skill match 이력` — `[timestamp] "query"` 형식
|
|
12
|
+
- `📜 task-log 실패 라인` — 실패/롤백/incomplete/버그 라인 회수
|
|
13
|
+
- total 카운트에 신규 필드 합산.
|
|
14
|
+
- 매칭 알고리즘: 기존 unicode word boundary regex 그대로 사용.
|
|
15
|
+
|
|
16
|
+
### Verified
|
|
17
|
+
- stress-v18 — brainstorm 신규 hits + 누적 회귀 + 성능.
|
|
18
|
+
- e2e 회귀: 219/219 PASS 유지.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1.9.71 — 2026-05-20
|
|
23
|
+
|
|
24
|
+
**`.env` / `.env.example` 자동 동기화 (보안 정책: 키만, 실제 값 절대 노출 안 함)**.
|
|
25
|
+
|
|
26
|
+
### Added — `leerness env check` / `env sync` 명령
|
|
27
|
+
- `leerness env check [<path>]` — `.env`에 있는데 `.env.example`에 없는 키 / 반대도 자동 감지.
|
|
28
|
+
- `--json`: 구조화된 JSON 출력 (CI 친화).
|
|
29
|
+
- exit code: `.env.example` 누락 키 ≥1 시 1 (보안 가시화).
|
|
30
|
+
- `leerness env sync [<path>]` — 누락 키만 `.env.example` 끝에 append (값은 빈 문자열).
|
|
31
|
+
|
|
32
|
+
### Improved — `leerness audit` 통합
|
|
33
|
+
- 매 audit 시 `.env` ↔ `.env.example` 자동 비교, 누락 시 warning 추가.
|
|
34
|
+
- `audit --fix` 시 누락 키 자동 추가 (보안 정책: 실제 값 미노출).
|
|
35
|
+
- `--no-env-check`로 비활성화 가능.
|
|
36
|
+
|
|
37
|
+
### 보안 정책 (검증됨)
|
|
38
|
+
- `.env`의 실제 값은 **절대** `.env.example`로 옮기지 않음.
|
|
39
|
+
- 추가되는 줄: `KEY=` (빈 문자열).
|
|
40
|
+
- 사용자 글로벌 룰 (.env 보안) 준수.
|
|
41
|
+
|
|
42
|
+
### Verified
|
|
43
|
+
- stress-v17 — env check / sync / audit 통합 + 보안 정책 + 누적 회귀.
|
|
44
|
+
- e2e 회귀: 219/219 PASS 유지.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
3
48
|
## 1.9.70 — 2026-05-19
|
|
4
49
|
|
|
5
50
|
**MCP server `tools/call` 자동 사용 통계** (1.9.65 usage-stats 확장).
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []()
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
|
|
13
13
|
║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
|
|
14
14
|
║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
|
|
15
|
-
║ v1.9.
|
|
15
|
+
║ v1.9.72 AI Agent Reliability Harness ║
|
|
16
16
|
║ verify · remember · orchestrate · audit · prevent drift ║
|
|
17
17
|
╚══════════════════════════════════════════════════════════════╝
|
|
18
18
|
```
|
|
@@ -433,6 +433,8 @@ npm test # = node ./scripts/e2e.js
|
|
|
433
433
|
|
|
434
434
|
## 변경 이력 (최근)
|
|
435
435
|
|
|
436
|
+
- **1.9.72** — **`leerness brainstorm`에 skill-suggestions.md history + task-log 실패 라인 통합** — 누적 컨텍스트 기반 brainstorm 강화 (이전 매칭 이력 + task-log 실패 회수).
|
|
437
|
+
- **1.9.71** — **`.env` / `.env.example` 자동 동기화** — `leerness env check` / `env sync` 명령 + `audit` 통합 (`--fix`로 누락 키 자동 추가). 보안 정책: 실제 값 절대 노출 안 함 (키만 추가, 값은 빈 문자열).
|
|
436
438
|
- **1.9.70** — **MCP server `tools/call` 자동 사용 통계** — 도구별 호출 카운트 (`.harness/cache/usage-stats.json#mcp.tools`) + `leerness usage stats` 출력에 MCP 섹션 + 드물게 호출되는 도구 식별.
|
|
437
439
|
- **1.9.69** — **handoff에 skill-suggestions.md history hit 노출** (fuzzy 매칭, 최근 2건 + top 2 매치). AI가 이전 세션 결정을 일관 유지. mtime 기반 캐시 (1.9.65/66/67 캐시 패밀리 연속).
|
|
438
440
|
- **1.9.68** — **`skill match` 결과 → `.harness/skill-suggestions.md` rolling history 자동 누적** (AI가 다음 세션에 이전 추천 참조 가능). `--no-save` / `LEERNESS_NO_SKILL_HISTORY=1`로 끄기.
|
package/bin/harness.js
CHANGED
|
@@ -6,7 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const cp = require('child_process');
|
|
7
7
|
const readline = require('readline');
|
|
8
8
|
|
|
9
|
-
const VERSION = '1.9.
|
|
9
|
+
const VERSION = '1.9.72';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -1480,6 +1480,32 @@ function audit(root) {
|
|
|
1480
1480
|
}
|
|
1481
1481
|
} catch {}
|
|
1482
1482
|
}
|
|
1483
|
+
// 1.9.71: .env / .env.example 동기화 감사 (--no-env-check로 끄기)
|
|
1484
|
+
if (!has('--no-env-check')) {
|
|
1485
|
+
try {
|
|
1486
|
+
const d = envDiff(root);
|
|
1487
|
+
if (exists(d.envPath) && exists(d.examplePath)) {
|
|
1488
|
+
if (d.inEnvOnly.length) {
|
|
1489
|
+
warnings++;
|
|
1490
|
+
warn(`.env에 있는 키 ${d.inEnvOnly.length}건이 .env.example에 누락: ${d.inEnvOnly.slice(0, 4).join(', ')}${d.inEnvOnly.length > 4 ? ' …' : ''}`);
|
|
1491
|
+
if (fix) {
|
|
1492
|
+
// 자동 동기화: 누락 키만 .env.example 끝에 append (값 비움)
|
|
1493
|
+
let example = read(d.examplePath);
|
|
1494
|
+
if (!example.endsWith('\n')) example += '\n';
|
|
1495
|
+
example += `\n# 1.9.71 audit --fix: 누락 키 자동 추가 (값은 빈 문자열, 보안 정책)\n`;
|
|
1496
|
+
for (const k of d.inEnvOnly) example += `${k}=\n`;
|
|
1497
|
+
writeUtf8(d.examplePath, example);
|
|
1498
|
+
ok(` ↳ fixed: .env.example에 ${d.inEnvOnly.length}건 자동 추가 (값은 빈 문자열, 1.9.71)`);
|
|
1499
|
+
fixed++;
|
|
1500
|
+
} else {
|
|
1501
|
+
log(` → 자동 동기화: leerness env sync 또는 leerness audit --fix`);
|
|
1502
|
+
}
|
|
1503
|
+
} else {
|
|
1504
|
+
ok('.env ↔ .env.example 동기화됨 (1.9.71)');
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
} catch {}
|
|
1508
|
+
}
|
|
1483
1509
|
// 1.9.63: --strict — warnings ≥ threshold 시 failures로 승격 (CI 친화)
|
|
1484
1510
|
if (has('--strict')) {
|
|
1485
1511
|
const threshold = parseInt(arg('--threshold', '1'), 10);
|
|
@@ -3216,11 +3242,12 @@ function _banner(opts = {}) {
|
|
|
3216
3242
|
lines.push('');
|
|
3217
3243
|
for (const ln of lines) log(ln);
|
|
3218
3244
|
if (opts.quickStart) {
|
|
3219
|
-
log(C.bold(C.cyan(' ✨ 빠른 시작 (1.9.
|
|
3245
|
+
log(C.bold(C.cyan(' ✨ 빠른 시작 (1.9.72+ 워크플로)')));
|
|
3220
3246
|
log(' ' + C.green('npx leerness@latest init .') + C.dim(' # 신규 프로젝트 + 외부 AI CLI 설정'));
|
|
3221
3247
|
log(' ' + C.green('npx leerness handoff .') + C.dim(' # 컨텍스트 + lessons + 매칭 skill + 이전 history hit (1.9.69)'));
|
|
3222
3248
|
log(' ' + C.green('npx leerness skill match "<query>"') + C.dim(' # 매칭 skill + rolling history 자동 누적 (1.9.68)'));
|
|
3223
3249
|
log(' ' + C.green('npx leerness verify-claim T-0001 --run-tests') + C.dim(' # AI 거짓 완료 자동 검증'));
|
|
3250
|
+
log(' ' + C.green('npx leerness env check .') + C.dim(' # .env ↔ .env.example 동기화 검사 (1.9.71)'));
|
|
3224
3251
|
log(' ' + C.green('npx leerness session close .') + C.dim(' # 마감 + 다음 라운드 추천 (default)'));
|
|
3225
3252
|
log('');
|
|
3226
3253
|
log(C.bold(C.cyan(' 🤖 메인 에이전트 (Claude/Cursor/Copilot)용')));
|
|
@@ -4435,7 +4462,8 @@ function _brainstormFor(root, topic) {
|
|
|
4435
4462
|
const tokens = String(topic).split(/\s+/).filter(t => t.length >= 2);
|
|
4436
4463
|
const wordRes = tokens.map(t => new RegExp(`(?<![\\p{L}\\p{N}_])${_escUnicode(t)}(?![\\p{L}\\p{N}_])`, 'iu'));
|
|
4437
4464
|
function matches(text) { return wordRes.every(re => re.test(text)); }
|
|
4438
|
-
|
|
4465
|
+
// 1.9.72: skillHistory + taskLogFails 필드 추가
|
|
4466
|
+
const hits = { decisions: [], skills: [], tasks: [], rules: [], evidence: [], lessons: [], code: [], skillHistory: [], taskLogFails: [] };
|
|
4439
4467
|
const dec = exists(decisionsPath(root)) ? read(decisionsPath(root)) : '';
|
|
4440
4468
|
const decLines = dec.split('\n');
|
|
4441
4469
|
for (const b of _extractDecisionBlocks(dec)) {
|
|
@@ -4491,6 +4519,32 @@ function _brainstormFor(root, topic) {
|
|
|
4491
4519
|
if (/✗|fail|롤백|incomplete|버그/i.test(block)) hits.lessons.push({ title: t.trim(), line: lineNo });
|
|
4492
4520
|
}
|
|
4493
4521
|
}
|
|
4522
|
+
// 1.9.72: skill-suggestions.md rolling history hits
|
|
4523
|
+
const histPath = path.join(root, '.harness', 'skill-suggestions.md');
|
|
4524
|
+
if (exists(histPath)) {
|
|
4525
|
+
const histTxt = read(histPath);
|
|
4526
|
+
for (const block of histTxt.split(/\n(?=## )/)) {
|
|
4527
|
+
if (!block.startsWith('## ')) continue;
|
|
4528
|
+
const h = block.match(/^## ([\d-]+ [\d:]+) — query "([^"]+)"/);
|
|
4529
|
+
if (h && matches(block)) {
|
|
4530
|
+
const idx = histTxt.indexOf(block);
|
|
4531
|
+
const lineNo = idx >= 0 ? histTxt.slice(0, idx).split('\n').length : 0;
|
|
4532
|
+
hits.skillHistory.push({ at: h[1], query: h[2], preview: block.slice(0, 220).replace(/\n+/g, ' '), line: lineNo });
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
// 1.9.72: task-log.md 실패 라인 hits
|
|
4537
|
+
const tlogPath = path.join(root, '.harness', 'task-log.md');
|
|
4538
|
+
if (exists(tlogPath)) {
|
|
4539
|
+
const tlog = read(tlogPath);
|
|
4540
|
+
const lines = tlog.split('\n');
|
|
4541
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4542
|
+
const line = lines[i];
|
|
4543
|
+
if (line.length > 4 && /✗|\bfail|롤백|재발|incomplete|버그/i.test(line) && matches(line)) {
|
|
4544
|
+
hits.taskLogFails.push({ title: line.replace(/^[-*]\s*/, '').slice(0, 100), line: i + 1 });
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4494
4548
|
// 1.9.25: --include-code 옵션 — 소스 본문 검색 추가 (모순 감지 핵심)
|
|
4495
4549
|
if (has('--include-code')) {
|
|
4496
4550
|
const codeDirs = ['src', 'tests', 'bin', 'lib'];
|
|
@@ -4522,7 +4576,7 @@ function _brainstormFor(root, topic) {
|
|
|
4522
4576
|
return hits;
|
|
4523
4577
|
}
|
|
4524
4578
|
|
|
4525
|
-
function _brainstormTotal(h) { return h.decisions.length + h.skills.length + h.tasks.length + h.rules.length + h.evidence.length + (h.code?.length || 0); }
|
|
4579
|
+
function _brainstormTotal(h) { return h.decisions.length + h.skills.length + h.tasks.length + h.rules.length + h.evidence.length + (h.code?.length || 0) + (h.skillHistory?.length || 0) + (h.taskLogFails?.length || 0); }
|
|
4526
4580
|
|
|
4527
4581
|
// 1.9.16: 워크스페이스 통합 brainstorm
|
|
4528
4582
|
function _brainstormWorkspace(rootBase, topic) {
|
|
@@ -4595,7 +4649,8 @@ function brainstormCmd(root, topic) {
|
|
|
4595
4649
|
const tokens = String(topic).split(/\s+/).filter(t => t.length >= 2);
|
|
4596
4650
|
const wordRes = tokens.map(t => new RegExp(`(?<![\\p{L}\\p{N}_])${_escUnicode(t)}(?![\\p{L}\\p{N}_])`, 'iu'));
|
|
4597
4651
|
function matches(text) { return wordRes.every(re => re.test(text)); }
|
|
4598
|
-
|
|
4652
|
+
// 1.9.72: skillHistory + taskLogFails 필드 추가 (brainstorm에 누적 컨텍스트 추가 회수)
|
|
4653
|
+
const hits = { decisions: [], skills: [], tasks: [], rules: [], evidence: [], lessons: [], code: [], skillHistory: [], taskLogFails: [] };
|
|
4599
4654
|
|
|
4600
4655
|
// decisions (1.9.14: 코드블록/Template 제외, 1.9.15: 라인 번호)
|
|
4601
4656
|
const dec = exists(decisionsPath(root)) ? read(decisionsPath(root)) : '';
|
|
@@ -4658,9 +4713,36 @@ function brainstormCmd(root, topic) {
|
|
|
4658
4713
|
if (/✗|fail|롤백|incomplete|버그/i.test(block)) hits.lessons.push({ title: t.trim(), line: lineNo });
|
|
4659
4714
|
}
|
|
4660
4715
|
}
|
|
4716
|
+
// 1.9.72: skill-suggestions.md rolling history hits (이전 매칭 결과 회수)
|
|
4717
|
+
const histPath = path.join(root, '.harness', 'skill-suggestions.md');
|
|
4718
|
+
if (exists(histPath)) {
|
|
4719
|
+
const histTxt = read(histPath);
|
|
4720
|
+
let pos = 0;
|
|
4721
|
+
for (const block of histTxt.split(/\n(?=## )/)) {
|
|
4722
|
+
if (!block.startsWith('## ')) { pos += block.length + 1; continue; }
|
|
4723
|
+
const h = block.match(/^## ([\d-]+ [\d:]+) — query "([^"]+)"/);
|
|
4724
|
+
if (h && matches(block)) {
|
|
4725
|
+
const idx = histTxt.indexOf(block);
|
|
4726
|
+
const lineNo = idx >= 0 ? histTxt.slice(0, idx).split('\n').length : 0;
|
|
4727
|
+
hits.skillHistory.push({ at: h[1], query: h[2], preview: block.slice(0, 220).replace(/\n+/g, ' '), line: lineNo });
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4731
|
+
// 1.9.72: task-log.md 실패 라인 hits
|
|
4732
|
+
const tlogPath = path.join(root, '.harness', 'task-log.md');
|
|
4733
|
+
if (exists(tlogPath)) {
|
|
4734
|
+
const tlog = read(tlogPath);
|
|
4735
|
+
const lines = tlog.split('\n');
|
|
4736
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4737
|
+
const line = lines[i];
|
|
4738
|
+
if (line.length > 4 && /✗|\bfail|롤백|재발|incomplete|버그/i.test(line) && matches(line)) {
|
|
4739
|
+
hits.taskLogFails.push({ title: line.replace(/^[-*]\s*/, '').slice(0, 100), line: i + 1 });
|
|
4740
|
+
}
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4661
4743
|
|
|
4662
|
-
const total = hits.decisions.length + hits.skills.length + hits.tasks.length + hits.rules.length + hits.evidence.length;
|
|
4663
|
-
log(`\n📦 총 ${total}건 발견 (decisions ${hits.decisions.length} · skills ${hits.skills.length} · tasks ${hits.tasks.length} · rules ${hits.rules.length} · evidence ${hits.evidence.length})`);
|
|
4744
|
+
const total = hits.decisions.length + hits.skills.length + hits.tasks.length + hits.rules.length + hits.evidence.length + (hits.skillHistory ? hits.skillHistory.length : 0) + (hits.taskLogFails ? hits.taskLogFails.length : 0);
|
|
4745
|
+
log(`\n📦 총 ${total}건 발견 (decisions ${hits.decisions.length} · skills ${hits.skills.length} · tasks ${hits.tasks.length} · rules ${hits.rules.length} · evidence ${hits.evidence.length}${hits.skillHistory && hits.skillHistory.length ? ` · skill-history ${hits.skillHistory.length}` : ''}${hits.taskLogFails && hits.taskLogFails.length ? ` · task-log-fails ${hits.taskLogFails.length}` : ''})`);
|
|
4664
4746
|
|
|
4665
4747
|
// 1.9.15: 모든 출력에 출처 파일:라인 표시
|
|
4666
4748
|
if (hits.decisions.length) {
|
|
@@ -4687,6 +4769,16 @@ function brainstormCmd(root, topic) {
|
|
|
4687
4769
|
log(`\n## ⚠ 같은 주제 과거 실패/롤백 (${hits.lessons.length}) — 같은 실수 방지`);
|
|
4688
4770
|
hits.lessons.slice(0, 5).forEach(l => log(` - .harness/review-evidence.md:${l.line || '?'} — ${l.title}`));
|
|
4689
4771
|
}
|
|
4772
|
+
// 1.9.72: skill-suggestions.md rolling history hits
|
|
4773
|
+
if (hits.skillHistory.length) {
|
|
4774
|
+
log(`\n## 📒 같은 주제 이전 skill match 이력 (${hits.skillHistory.length}) — 1.9.68 누적`);
|
|
4775
|
+
hits.skillHistory.slice(0, 5).forEach(h => log(` - .harness/skill-suggestions.md:${h.line || '?'} — [${h.at}] "${h.query}"`));
|
|
4776
|
+
}
|
|
4777
|
+
// 1.9.72: task-log.md 실패 라인 hits
|
|
4778
|
+
if (hits.taskLogFails.length) {
|
|
4779
|
+
log(`\n## 📜 task-log 실패 라인 (${hits.taskLogFails.length}) — 1.9.67 인덱스 + brainstorm`);
|
|
4780
|
+
hits.taskLogFails.slice(0, 5).forEach(t => log(` - .harness/task-log.md:${t.line || '?'} — ${t.title}`));
|
|
4781
|
+
}
|
|
4690
4782
|
|
|
4691
4783
|
log(`\n## 💡 시작 전 권장 액션`);
|
|
4692
4784
|
log(` 1. 위 자원을 모두 검토 후 plan add 또는 task add로 새 작업 등록`);
|
|
@@ -7138,6 +7230,77 @@ function whatsNewCmd(root) {
|
|
|
7138
7230
|
log(` 4. 상세: \`cat CHANGELOG.md\` 또는 \`leerness whats-new --json\``);
|
|
7139
7231
|
}
|
|
7140
7232
|
|
|
7233
|
+
// 1.9.71: .env / .env.example 자동 동기화 — 누락 키 감지 + (옵션) 자동 추가
|
|
7234
|
+
// 보안 정책: .env의 실제 값은 절대 옮기지 않음. .env.example엔 키만 (빈 값).
|
|
7235
|
+
function _parseEnvKeys(text) {
|
|
7236
|
+
// KEY=value 형식, comment(#) 무시, 빈 줄 무시
|
|
7237
|
+
const out = new Set();
|
|
7238
|
+
for (const raw of String(text || '').split('\n')) {
|
|
7239
|
+
const line = raw.trim();
|
|
7240
|
+
if (!line || line.startsWith('#')) continue;
|
|
7241
|
+
const m = line.match(/^([A-Z_][A-Z0-9_]*)\s*=/i);
|
|
7242
|
+
if (m) out.add(m[1]);
|
|
7243
|
+
}
|
|
7244
|
+
return out;
|
|
7245
|
+
}
|
|
7246
|
+
function envDiff(root) {
|
|
7247
|
+
root = absRoot(root || process.cwd());
|
|
7248
|
+
const envPath = path.join(root, '.env');
|
|
7249
|
+
const examplePath = path.join(root, '.env.example');
|
|
7250
|
+
const envKeys = exists(envPath) ? _parseEnvKeys(read(envPath)) : new Set();
|
|
7251
|
+
const exKeys = exists(examplePath) ? _parseEnvKeys(read(examplePath)) : new Set();
|
|
7252
|
+
const inEnvOnly = [...envKeys].filter(k => !exKeys.has(k));
|
|
7253
|
+
const inExampleOnly = [...exKeys].filter(k => !envKeys.has(k));
|
|
7254
|
+
return { envPath, examplePath, envKeys: [...envKeys], exKeys: [...exKeys], inEnvOnly, inExampleOnly };
|
|
7255
|
+
}
|
|
7256
|
+
function envCheckCmd(root) {
|
|
7257
|
+
const d = envDiff(root);
|
|
7258
|
+
const isJson = has('--json');
|
|
7259
|
+
if (isJson) { log(JSON.stringify(d, null, 2)); return; }
|
|
7260
|
+
log(`# leerness env check (1.9.71)`);
|
|
7261
|
+
log(`.env 존재: ${exists(d.envPath)} · .env.example 존재: ${exists(d.examplePath)}`);
|
|
7262
|
+
log(`총 .env 키 ${d.envKeys.length} · .env.example 키 ${d.exKeys.length}`);
|
|
7263
|
+
if (d.inEnvOnly.length) {
|
|
7264
|
+
log('');
|
|
7265
|
+
log(`⚠ .env에 있는데 .env.example에 없는 키 ${d.inEnvOnly.length}건 (보안 정책: 값 없이 키만 추가):`);
|
|
7266
|
+
for (const k of d.inEnvOnly) log(` - ${k}`);
|
|
7267
|
+
}
|
|
7268
|
+
if (d.inExampleOnly.length) {
|
|
7269
|
+
log('');
|
|
7270
|
+
log(`ℹ .env.example에 있는데 .env에 없는 키 ${d.inExampleOnly.length}건 (런타임 누락 가능):`);
|
|
7271
|
+
for (const k of d.inExampleOnly) log(` - ${k}`);
|
|
7272
|
+
}
|
|
7273
|
+
if (!d.inEnvOnly.length && !d.inExampleOnly.length) {
|
|
7274
|
+
log('');
|
|
7275
|
+
ok('.env ↔ .env.example 동기화됨');
|
|
7276
|
+
} else {
|
|
7277
|
+
log('');
|
|
7278
|
+
log(`💡 자동 동기화: leerness env sync${d.inEnvOnly.length ? ' (.env.example에 누락 키 추가 — 값은 빈 문자열)' : ''}`);
|
|
7279
|
+
}
|
|
7280
|
+
// 1.9.71: exit code = .env.example 누락 키 있으면 1 (보안 가시화)
|
|
7281
|
+
if (d.inEnvOnly.length) process.exitCode = 1;
|
|
7282
|
+
}
|
|
7283
|
+
function envSyncCmd(root) {
|
|
7284
|
+
const d = envDiff(root);
|
|
7285
|
+
log(`# leerness env sync (1.9.71)`);
|
|
7286
|
+
if (!exists(d.examplePath)) {
|
|
7287
|
+
fail(`.env.example 없음 — leerness init . 먼저 실행`);
|
|
7288
|
+
return;
|
|
7289
|
+
}
|
|
7290
|
+
if (!d.inEnvOnly.length) {
|
|
7291
|
+
ok('동기화 불필요 — .env.example에 누락 키 없음');
|
|
7292
|
+
return;
|
|
7293
|
+
}
|
|
7294
|
+
// 누락 키를 .env.example 끝에 append (값 비움, 보안 정책 코멘트 동반)
|
|
7295
|
+
let example = read(d.examplePath);
|
|
7296
|
+
if (!example.endsWith('\n')) example += '\n';
|
|
7297
|
+
example += `\n# 1.9.71 sync: .env에서 발견된 누락 키 (값은 빈 문자열 — 보안 정책)\n`;
|
|
7298
|
+
for (const k of d.inEnvOnly) example += `${k}=\n`;
|
|
7299
|
+
writeUtf8(d.examplePath, example);
|
|
7300
|
+
ok(`${d.inEnvOnly.length}건 추가됨 → ${rel(root, d.examplePath)}`);
|
|
7301
|
+
for (const k of d.inEnvOnly) log(` + ${k}=`);
|
|
7302
|
+
}
|
|
7303
|
+
|
|
7141
7304
|
function usageStatsCmd(root) {
|
|
7142
7305
|
root = absRoot(root || process.cwd());
|
|
7143
7306
|
const stats = _readUsageStats(root);
|
|
@@ -7466,6 +7629,9 @@ async function main() {
|
|
|
7466
7629
|
if (cmd === 'contract' && args[1] === 'verify') return contractVerifyCmd(args[2], args[3]);
|
|
7467
7630
|
if (cmd === 'drift' && (args[1] === 'check' || !args[1])) return driftCheckCmd(args[2] || arg('--path', process.cwd()));
|
|
7468
7631
|
if (cmd === 'usage' && (args[1] === 'stats' || !args[1])) return usageStatsCmd(args[2] || arg('--path', process.cwd()));
|
|
7632
|
+
// 1.9.71: leerness env check / sync — .env vs .env.example 자동 동기화
|
|
7633
|
+
if (cmd === 'env' && args[1] === 'check') return envCheckCmd(args[2] || arg('--path', process.cwd()));
|
|
7634
|
+
if (cmd === 'env' && args[1] === 'sync') return envSyncCmd(args[2] || arg('--path', process.cwd()));
|
|
7469
7635
|
if (cmd === 'whats-new') return whatsNewCmd(args[1] || arg('--path', process.cwd()));
|
|
7470
7636
|
if (cmd === 'reuse' && args[1] === 'autodetect') return reuseAutodetectCmd(args[2] || arg('--path', process.cwd()));
|
|
7471
7637
|
if (cmd === 'setup-agents' || cmd === 'setup' && args[1] === 'agents') return await setupAgentsCmd(args[1] && args[1] !== 'agents' ? args[1] : (args[2] || process.cwd()));
|