leerness 1.9.273 → 1.9.275
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 +36 -0
- package/README.md +27 -5
- package/bin/harness.js +50 -97
- package/lib/pure-utils.js +109 -0
- package/package.json +2 -1
- package/scripts/e2e.js +32 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.275 — 2026-06-03 — UR-0026: 릴리스 채널 (npm dist-tag 안정/실험)
|
|
4
|
+
|
|
5
|
+
**🚦 잦은 1.9.x 릴리스에 안정(latest)/실험(next) 채널 + 버전 고정 안내 — 사용자가 안정성을 제어 (GPT-5.5 리뷰).**
|
|
6
|
+
|
|
7
|
+
### 배경
|
|
8
|
+
GPT-5.5 리뷰: 릴리스가 매우 잦아 사용자가 안정/실험 버전을 구분하기 어려움(UR-0026). npm dist-tag 로 채널을 분리하고, 운영 코드는 버전 고정을 안내.
|
|
9
|
+
|
|
10
|
+
### 구현
|
|
11
|
+
1. **`_resolveNpmTag(explicit, env)` 순수 함수** — `--npm-tag` / `LEERNESS_NPM_TAG` / 기본 `latest`. 형식 검증(소문자/숫자/-, ≤39자), 잘못된 값은 latest 폴백.
|
|
12
|
+
2. **publish 경로 dist-tag 지원** — `_publishToNpm` + `release publish --npm-publish` 가 `--tag <tag>` 적용. `release sync-main --npm-tag next` 또는 `LEERNESS_NPM_TAG=next` 로 실험 채널 publish 가능 (기본은 latest 안정).
|
|
13
|
+
3. **`leerness release channel [--json]` 신규** — 현재 버전/기본 publish 태그/정책(latest=안정, next=실험) + 버전 고정 안내. 온라인 시 `npm view dist-tags` 회수(offline 시 생략).
|
|
14
|
+
4. **README 릴리스 채널 섹션(한/영)** — `npm i leerness` / `@next` / `@<버전>` 설치 가이드.
|
|
15
|
+
5. **selftest 25→26 + e2e 222→223** — `_resolveNpmTag` 검증 + `release channel` JSON/env 반영.
|
|
16
|
+
|
|
17
|
+
### 검증
|
|
18
|
+
- **selftest 26/26 PASS** · **E2E 223/223 PASS** (회귀 0). 기본 publish 채널은 latest 유지(기존 동작 불변).
|
|
19
|
+
|
|
20
|
+
## 1.9.274 — 2026-06-03 — UR-0025 1단계: bin/harness.js 모듈 분리 시작 (lib/ 순수 유틸 추출)
|
|
21
|
+
|
|
22
|
+
**🧩 단일 대형 파일(1.2MB) 모듈 분리의 비파괴 1단계 — `lib/` 모듈화 패턴 확립 + 순수 유틸 7종 추출 (GPT-5.5 리뷰).**
|
|
23
|
+
|
|
24
|
+
### 배경
|
|
25
|
+
GPT-5.5 리뷰: 거의 모든 기능이 `bin/harness.js` 한 파일(~20k줄)에 있어 유지보수성·리뷰 가능성이 낮음(UR-0025). 한 번에 전부 분리하면 고위험이므로, **점진적·비파괴**로 진행: 먼저 require 기반 `lib/` 모듈화 패턴을 확립하고, 부작용 0인 순수 함수부터 추출. selftest 가 추출 함수 전부를 검증하므로 회귀를 즉시 포착.
|
|
26
|
+
|
|
27
|
+
### 구현
|
|
28
|
+
1. **`lib/pure-utils.js` 신규** — 순수 함수 7종 추출: `_isSecretKey` / `compareVer` / `parseHarnessVersion` / `_classifyCJK` / `_riskLabel` / `_detectSystemLang` / `_parseSlashFromHelp`. harness 내부 상태·타 함수 의존 0.
|
|
29
|
+
2. **harness.js require 재연결** — 상단에서 `require('../lib/pure-utils')` 로 동일 이름 바인딩, 인라인 정의 제거. `module.exports` 재노출 유지(단위 테스트 호환).
|
|
30
|
+
3. **`package.json files` 에 `lib` 추가** — npm tarball 포함 확인(`npm pack --dry-run`: `lib/pure-utils.js` 4.6kB 포함). 미포함 시 require 실패 → 검증으로 차단.
|
|
31
|
+
4. **e2e +1 (221→222)** — lib 모듈 존재 + 7종 export + 동작 + files 포함 회귀 검증.
|
|
32
|
+
|
|
33
|
+
### 다음 단계 (UR-0025 계속)
|
|
34
|
+
- 2단계+: 모델 catalog/roles/슬래시 레지스트리 등 응집도 높은 클러스터를 순차 추출 (매 단계 selftest+e2e 녹색 유지).
|
|
35
|
+
|
|
36
|
+
### 검증
|
|
37
|
+
- **selftest 25/25 PASS** · **E2E 222/222 PASS** (회귀 0) · `npm pack` tarball 에 lib 포함 확인. 동작 동일(비파괴).
|
|
38
|
+
|
|
3
39
|
## 1.9.273 — 2026-06-03 — UR-0027: 빠른 테스트 서브셋 (test:fast smoke) — npm test 5분 초과 해소
|
|
4
40
|
|
|
5
41
|
**⚡ 전체 e2e(220+, 수 분) 외에 핵심-경로만 빠르게 확인하는 `npm run test:fast`(~10초) 추가 (GPT-5.5 리뷰 반영).**
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
|
|
4
4
|
> **A CLI harness that stops AI coding agents from faking completion, duplicating work, forgetting context, and colliding.**
|
|
5
5
|
|
|
6
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []() []() []() []()
|
|
7
7
|
|
|
8
8
|
```
|
|
9
9
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -188,6 +188,17 @@ leerness capabilities # 권한 표면 + opt-out + 주의 명령 (--json
|
|
|
188
188
|
- 회사/운영 코드에서는 먼저 한 프로젝트에서 `init` 후 `git diff` 로 검토하고 커밋하세요.
|
|
189
189
|
- 자세히: [SECURITY.md](SECURITY.md)
|
|
190
190
|
|
|
191
|
+
### 릴리스 채널 (안정 vs 실험)
|
|
192
|
+
|
|
193
|
+
leerness는 활발히 개발됩니다(잦은 1.9.x). 채널을 골라 안정성을 제어하세요:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
npm i leerness # latest (안정) — 일반 사용자 기본
|
|
197
|
+
npm i leerness@next # next (실험) — 조기 검증용
|
|
198
|
+
npm i leerness@1.9.275 # 버전 고정 — 재현성 (운영 코드 권장)
|
|
199
|
+
leerness release channel # 현재 채널/정책 확인 (--json)
|
|
200
|
+
```
|
|
201
|
+
|
|
191
202
|
---
|
|
192
203
|
|
|
193
204
|
<a id="english"></a>
|
|
@@ -356,6 +367,17 @@ leerness capabilities # capability surface + opt-out + cautious command
|
|
|
356
367
|
- For company/production code, trial `init` in one project, review with `git diff`, then commit.
|
|
357
368
|
- Details: [SECURITY.md](SECURITY.md)
|
|
358
369
|
|
|
370
|
+
### Release channels (stable vs experimental)
|
|
371
|
+
|
|
372
|
+
leerness is actively developed (frequent 1.9.x). Pick a channel to control stability:
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
npm i leerness # latest (stable) — default for most users
|
|
376
|
+
npm i leerness@next # next (experimental) — early validation
|
|
377
|
+
npm i leerness@1.9.275 # pin a version — reproducibility (recommended for prod)
|
|
378
|
+
leerness release channel # show current channel/policy (--json)
|
|
379
|
+
```
|
|
380
|
+
|
|
359
381
|
---
|
|
360
382
|
|
|
361
383
|
## 🔧 Environment variables / 환경변수
|
|
@@ -402,7 +424,7 @@ MIT — © leerness contributors
|
|
|
402
424
|
<!-- leerness:project-readme:start -->
|
|
403
425
|
## Leerness Project Harness
|
|
404
426
|
|
|
405
|
-
이 프로젝트는 Leerness v1.9.
|
|
427
|
+
이 프로젝트는 Leerness v1.9.275 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
406
428
|
|
|
407
429
|
### Core Commands
|
|
408
430
|
|
|
@@ -444,7 +466,7 @@ leerness memory restore decision <date|title>
|
|
|
444
466
|
|
|
445
467
|
### MCP server (외부 AI 통합)
|
|
446
468
|
|
|
447
|
-
Leerness v1.9.
|
|
469
|
+
Leerness v1.9.275는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **42개 도구**를 노출:
|
|
448
470
|
|
|
449
471
|
```jsonc
|
|
450
472
|
// 카테고리별
|
|
@@ -465,7 +487,7 @@ Leerness v1.9.273는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
465
487
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
466
488
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
|
|
467
489
|
|
|
468
|
-
현재 누적: **70 라운드 (1.9.40 → 1.9.
|
|
490
|
+
현재 누적: **70 라운드 (1.9.40 → 1.9.275)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
469
491
|
|
|
470
492
|
### 성능 가이드 (1.9.140 측정)
|
|
471
493
|
|
|
@@ -503,6 +525,6 @@ leerness release pack --close --auto-main-push
|
|
|
503
525
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
504
526
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
505
527
|
|
|
506
|
-
Last synced by Leerness v1.9.
|
|
528
|
+
Last synced by Leerness v1.9.275: 2026-06-03
|
|
507
529
|
<!-- leerness:project-readme:end -->
|
|
508
530
|
|
package/bin/harness.js
CHANGED
|
@@ -6,8 +6,10 @@ const path = require('path');
|
|
|
6
6
|
const cp = require('child_process');
|
|
7
7
|
const os = require('os'); // 1.9.178: _publishToNpm 에서 os.tmpdir() 사용 (전역 import)
|
|
8
8
|
const readline = require('readline');
|
|
9
|
+
// 1.9.274 (UR-0025 1단계): 순수 유틸 함수 모듈 분리 (require-based, 비파괴). selftest 7종이 동작 검증.
|
|
10
|
+
const { _isSecretKey, compareVer, parseHarnessVersion, _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp } = require('../lib/pure-utils');
|
|
9
11
|
|
|
10
|
-
const VERSION = '1.9.
|
|
12
|
+
const VERSION = '1.9.275';
|
|
11
13
|
|
|
12
14
|
// 1.9.184: DEP0190 (child_process shell: true) deprecation warning 억제 (사용자 명시).
|
|
13
15
|
// leerness 는 cross-platform PATH resolution 을 위해 shell: true 를 의도적으로 사용 (claude.cmd / ollama.cmd 등 Windows .cmd 처리).
|
|
@@ -228,22 +230,7 @@ function detectProjectName(root) { try { const pkg = JSON.parse(read(path.join(r
|
|
|
228
230
|
// 1.9.269 (UR-0022): OS 시스템 언어 감지 — 신규 init(빈 디렉토리, 프로젝트 콘텐츠 없음)에서 OS locale 로 설치 언어 판별.
|
|
229
231
|
// 우선순위: POSIX 환경변수(LC_ALL>LC_CTYPE>LANG>LANGUAGE) > Intl ICU resolvedOptions().locale(Windows 등 LANG 미설정 시) > null.
|
|
230
232
|
// 순수 함수(부작용 0) — env 주입 가능(selftest). 판별 불가 시 null 반환 → 호출부가 en 폴백.
|
|
231
|
-
|
|
232
|
-
env = env || process.env;
|
|
233
|
-
const raw = String(env.LC_ALL || env.LC_CTYPE || env.LANG || env.LANGUAGE || '').toLowerCase();
|
|
234
|
-
if (raw && raw !== 'c' && raw !== 'posix') {
|
|
235
|
-
if (/(^|[^a-z])ko([_\-.]|$)|korean|[_-]kr([_\-.]|$)/.test(raw)) return 'ko';
|
|
236
|
-
if (/(^|[^a-z])en([_\-.]|$)|english|[_-](us|gb)([_\-.]|$)/.test(raw)) return 'en';
|
|
237
|
-
}
|
|
238
|
-
// POSIX env 없거나 미매칭 → Node ICU locale (Windows 는 LANG 보통 미설정 → 여기서 OS 언어 회수)
|
|
239
|
-
try {
|
|
240
|
-
const loc = (Intl.DateTimeFormat().resolvedOptions().locale || '').toLowerCase();
|
|
241
|
-
const primary = loc.split('-')[0];
|
|
242
|
-
if (primary === 'ko') return 'ko';
|
|
243
|
-
if (primary === 'en') return 'en';
|
|
244
|
-
} catch {}
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
233
|
+
// _detectSystemLang → lib/pure-utils.js 로 이동 (1.9.274 UR-0025 1단계)
|
|
247
234
|
function detectLanguageValue(root, value = 'auto') {
|
|
248
235
|
const v = String(value || 'auto').toLowerCase();
|
|
249
236
|
if (v === 'ko' || v === 'en') return v;
|
|
@@ -2598,33 +2585,7 @@ function _terminalEncodingNotice(opts = {}) {
|
|
|
2598
2585
|
// 1.9.243: CJK 분류 — Korean/Japanese/Chinese 비-ASCII 패턴 통계 (UR-0014 3단계)
|
|
2599
2586
|
// 1.9.257: _scanShellScriptsEncoding 내부 중첩 → 모듈 스코프로 추출 (순수 함수 — 단위 테스트 가능).
|
|
2600
2587
|
// UTF-8 3-byte 시퀀스 시작 바이트로 CJK 언어 분류 (인코딩 오인식 위험 판단용).
|
|
2601
|
-
|
|
2602
|
-
let korean = 0, japanese = 0, chinese = 0, other = 0;
|
|
2603
|
-
for (let i = 0; i < Math.min(buf.length, len); i++) {
|
|
2604
|
-
const b = buf[i];
|
|
2605
|
-
if (b < 0x80) continue;
|
|
2606
|
-
// Korean Hangul Syllables U+AC00-D7AF → 3-byte: EA B0 80 ~ ED 9F BF
|
|
2607
|
-
if (b >= 0xEA && b <= 0xED) korean++;
|
|
2608
|
-
// Japanese Hiragana/Katakana U+3040-30FF → 3-byte: E3 81 80 ~ E3 83 BF
|
|
2609
|
-
else if (b === 0xE3) japanese++;
|
|
2610
|
-
// CJK Unified Ideographs U+4E00-9FFF → 3-byte: E4 B8 80 ~ E9 BF BF
|
|
2611
|
-
else if (b >= 0xE4 && b <= 0xE9) chinese++;
|
|
2612
|
-
else other++;
|
|
2613
|
-
}
|
|
2614
|
-
return { korean, japanese, chinese, other };
|
|
2615
|
-
}
|
|
2616
|
-
function _riskLabel(cjk) {
|
|
2617
|
-
if (cjk.korean >= cjk.japanese && cjk.korean >= cjk.chinese && cjk.korean > 0) {
|
|
2618
|
-
return { type: 'korean', risk: 'Windows 한국어 PowerShell 에서 CP949 로 오인식 가능 (BOM 추가 권장)' };
|
|
2619
|
-
}
|
|
2620
|
-
if (cjk.japanese > cjk.korean && cjk.japanese >= cjk.chinese) {
|
|
2621
|
-
return { type: 'japanese', risk: 'Windows 일본어 PowerShell 에서 CP932 (Shift-JIS) 로 오인식 가능 (BOM 추가 권장)' };
|
|
2622
|
-
}
|
|
2623
|
-
if (cjk.chinese > 0) {
|
|
2624
|
-
return { type: 'chinese', risk: 'Windows 중국어 PowerShell 에서 CP936 (GBK) 로 오인식 가능 (BOM 추가 권장)' };
|
|
2625
|
-
}
|
|
2626
|
-
return { type: 'non-ascii', risk: 'Windows 비-ASCII 셸 스크립트 — BOM 없는 UTF-8 인코딩 오인식 가능 (BOM 추가 권장)' };
|
|
2627
|
-
}
|
|
2588
|
+
// _classifyCJK, _riskLabel → lib/pure-utils.js 로 이동 (1.9.274 UR-0025 1단계)
|
|
2628
2589
|
|
|
2629
2590
|
// 셸 스크립트 (.ps1/.bat/.cmd/.sh) 인코딩 위험 감지
|
|
2630
2591
|
// 위험 시그널: 한국어 (또는 비-ASCII) 문자 + BOM 없음 + Windows 환경
|
|
@@ -2951,6 +2912,7 @@ function _selfTestCases() {
|
|
|
2951
2912
|
{ name: 'ROLE_CATALOG + _normalizeRole: 7종 + 한국어 별칭 (1.9.270)', run: () => { const keys = Object.keys(ROLE_CATALOG); return keys.length === 7 && keys.includes('coder') && keys.includes('reviewer') && _normalizeRole('코딩') === 'coder' && _normalizeRole('검수자') === 'reviewer' && _normalizeRole('지휘관') === 'commander'; } },
|
|
2952
2913
|
{ name: '_pickModel: top/code/fast 등급 선택 (1.9.270)', run: () => { return _pickModel('codex', 'code') === 'gpt-5-codex' && _pickModel('claude', 'top') === 'claude-opus-4-7' && /haiku/.test(_pickModel('claude', 'fast')); } },
|
|
2953
2914
|
{ name: 'CAPABILITY_SURFACE: 6 영역 + risk/optOut + 주의명령 (1.9.272)', run: () => { const ks = Object.keys(CAPABILITY_SURFACE); return ks.length === 6 && ks.includes('automationBridges') && Object.values(CAPABILITY_SURFACE).every(v => ['low','medium','high'].includes(v.risk) && v.optOut && v.desc) && POWERFUL_COMMANDS.length >= 5; } },
|
|
2915
|
+
{ name: '_resolveNpmTag: latest 기본 + next 허용 + 잘못된 형식 폴백 (1.9.275)', run: () => _resolveNpmTag(null, {}) === 'latest' && _resolveNpmTag('next', {}) === 'next' && _resolveNpmTag(null, { LEERNESS_NPM_TAG: 'next' }) === 'next' && _resolveNpmTag('bad tag!', {}) === 'latest' && _resolveNpmTag('Beta', {}) === 'beta' },
|
|
2954
2916
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
2955
2917
|
];
|
|
2956
2918
|
}
|
|
@@ -3608,6 +3570,7 @@ function commandsCmd(root) {
|
|
|
3608
3570
|
{ cmd: 'agents list|check|quota|dispatch|multi', desc: '외부 AI CLI 오케스트레이션 (dispatch --role 모델 라우팅 1.9.270)' },
|
|
3609
3571
|
{ cmd: 'roles list|set|unset|catalog|suggest|verify', desc: '모델별 역할 부여 (코딩/검수/지휘/디자인/디버그/설계/분배) — 1.9.270' },
|
|
3610
3572
|
{ cmd: 'capabilities [--json]', desc: '권한·보안 표면 공개 (무엇을 하는지 + opt-out + 주의 명령) — 1.9.272' },
|
|
3573
|
+
{ cmd: 'release channel [--json]', desc: '릴리스 채널 정책 (latest 안정 / next 실험 / 버전 고정) — 1.9.275' },
|
|
3611
3574
|
{ cmd: 'slash-commands [agent] [--json --record --detect --refresh]', desc: 'CLI 에이전트 슬래시 명령 레지스트리 + --help probe 자동 갱신 (1.9.265~267, UR-0021)' },
|
|
3612
3575
|
{ cmd: 'review-request "<request>"', desc: '사용자 요청 사전 검토 (1.9.176)' },
|
|
3613
3576
|
{ cmd: 'review <file> --persona <ids>', desc: '페르소나 리뷰 (1.9.29)' },
|
|
@@ -9470,9 +9433,7 @@ const _LEERNESS_NONSECRET_KEYS = new Set([
|
|
|
9470
9433
|
'LEERNESS_SKILL_AUTO_PRESETS'
|
|
9471
9434
|
]);
|
|
9472
9435
|
// 시크릿은 절대 .harness/leerness-config.json 으로 옮기지 않음 (TOKEN 패턴 자동 차단)
|
|
9473
|
-
|
|
9474
|
-
return /TOKEN|SECRET|PASSWORD|API_KEY|PRIVATE/i.test(k);
|
|
9475
|
-
}
|
|
9436
|
+
// _isSecretKey → lib/pure-utils.js 로 이동 (1.9.274 UR-0025 1단계)
|
|
9476
9437
|
function _leernessConfigPath(root) { return path.join(absRoot(root || process.cwd()), '.harness', 'leerness-config.json'); }
|
|
9477
9438
|
function _loadLeernessConfig(root) {
|
|
9478
9439
|
const f = _leernessConfigPath(root);
|
|
@@ -10273,30 +10234,7 @@ function _agentSlashHint(root, agentId) {
|
|
|
10273
10234
|
// 1.9.267 (UR-0021 3단계): CLI `--help` 출력에서 슬래시 명령/하위명령을 best-effort 파싱.
|
|
10274
10235
|
// 순수 함수 (부작용 0, selftest 가능). invoke='slash' → `/cmd desc` 패턴, invoke='subcommand' → 들여쓰기 `cmd desc` 패턴.
|
|
10275
10236
|
// CLI 플래그(--foo)·옵션은 의도적으로 제외. 검출 0건이면 큐레이션 빌트인을 그대로 유지(파괴적 덮어쓰기 방지).
|
|
10276
|
-
|
|
10277
|
-
const out = [];
|
|
10278
|
-
const seen = new Set();
|
|
10279
|
-
const lines = String(text || '').split(/\r?\n/);
|
|
10280
|
-
for (const raw of lines) {
|
|
10281
|
-
const ln = raw.replace(/\x1b\[[0-9;]*m/g, ''); // ANSI 색상 제거
|
|
10282
|
-
if (invoke === 'subcommand') {
|
|
10283
|
-
// 들여쓰기된 하위명령: " suggest 설명" — 슬래시/대시 시작 제외, 2칸+ 공백 구분
|
|
10284
|
-
const m = ln.match(/^\s{2,}([a-z][a-z0-9][\w-]*)\s{2,}(\S.*)$/);
|
|
10285
|
-
if (m && !/^--/.test(m[1])) {
|
|
10286
|
-
const cmd = m[1];
|
|
10287
|
-
if (!seen.has(cmd) && cmd.length <= 24) { seen.add(cmd); out.push({ cmd, desc: m[2].trim().slice(0, 80) }); }
|
|
10288
|
-
}
|
|
10289
|
-
continue;
|
|
10290
|
-
}
|
|
10291
|
-
// 슬래시 명령: "/help 설명" 또는 " /help - 설명"
|
|
10292
|
-
const m = ln.match(/^\s*(\/[a-zA-Z][\w-]*)(?:\s+[-–:]?\s*(.*))?$/);
|
|
10293
|
-
if (m) {
|
|
10294
|
-
const cmd = m[1];
|
|
10295
|
-
if (!seen.has(cmd) && cmd.length <= 24) { seen.add(cmd); out.push({ cmd, desc: (m[2] || '').trim().slice(0, 80) }); }
|
|
10296
|
-
}
|
|
10297
|
-
}
|
|
10298
|
-
return out;
|
|
10299
|
-
}
|
|
10237
|
+
// _parseSlashFromHelp → lib/pure-utils.js 로 이동 (1.9.274 UR-0025 1단계)
|
|
10300
10238
|
// 1.9.267 (UR-0021 3단계): 설치된 CLI 의 `--help` 를 spawn 해 슬래시 명령을 probe (best-effort, offline-first).
|
|
10301
10239
|
// extDef: EXTERNAL_AGENTS 항목 (bin/versionArgs 보유). copilot 처럼 subcommand 인 경우 versionArgs 의 base(예: ['copilot']) + '--help'.
|
|
10302
10240
|
// 실패/0건이면 ok:false + reason — 호출부가 큐레이션 빌트인 유지. 타임아웃 5s (행 방지).
|
|
@@ -14228,6 +14166,34 @@ function releaseCleanupCmd(root) {
|
|
|
14228
14166
|
// - NPM_TOKEN 또는 LEERNESS_NPM_TOKEN 환경변수에서만 읽음 (값 절대 로그 X)
|
|
14229
14167
|
// - 임시 .npmrc 파일 (mkdtempSync) 생성 → publish → 즉시 삭제 (finally)
|
|
14230
14168
|
// - 이미 publish된 버전 자동 detect → skip (중복 publish 차단)
|
|
14169
|
+
// 1.9.275 (UR-0026, GPT-5.5 리뷰): npm dist-tag 결정 — latest(안정, 기본) / next(실험). 잘못된 형식은 latest 폴백. 순수 함수.
|
|
14170
|
+
function _resolveNpmTag(explicit, env) {
|
|
14171
|
+
env = env || process.env;
|
|
14172
|
+
const raw = String(explicit || env.LEERNESS_NPM_TAG || 'latest').trim().toLowerCase();
|
|
14173
|
+
return /^[a-z][a-z0-9-]{0,38}$/.test(raw) ? raw : 'latest';
|
|
14174
|
+
}
|
|
14175
|
+
// 1.9.275 (UR-0026): leerness release channel — 안정/실험 채널 정책 + 버전 고정 안내 (npm dist-tag).
|
|
14176
|
+
function releaseChannelCmd(root) {
|
|
14177
|
+
root = absRoot(root || process.cwd());
|
|
14178
|
+
const json = has('--json');
|
|
14179
|
+
let v = null; try { v = JSON.parse(read(path.join(root, 'package.json'))).version; } catch {}
|
|
14180
|
+
const tag = _resolveNpmTag(arg('--npm-tag', null));
|
|
14181
|
+
let distTags = null;
|
|
14182
|
+
if (!has('--offline') && process.env.LEERNESS_OFFLINE !== '1') {
|
|
14183
|
+
try { const r = cp.spawnSync('npm', ['view', 'leerness', 'dist-tags', '--json'], { encoding: 'utf8', timeout: 10000, shell: true }); if (r.status === 0 && r.stdout) distTags = JSON.parse(r.stdout); } catch {}
|
|
14184
|
+
}
|
|
14185
|
+
if (json) { log(JSON.stringify({ version: v, defaultPublishTag: tag, distTags, policy: { stable: 'latest', experimental: 'next' } }, null, 2)); return; }
|
|
14186
|
+
log(`# leerness release channel (1.9.275, UR-0026)`);
|
|
14187
|
+
log(`현재 버전: ${v || '?'} · 기본 publish 태그: ${tag}`);
|
|
14188
|
+
if (distTags) { log('npm dist-tags:'); for (const [k, val] of Object.entries(distTags)) log(` ${k}: ${val}`); }
|
|
14189
|
+
log('');
|
|
14190
|
+
log(`## 채널 정책 (안정 vs 실험)`);
|
|
14191
|
+
log(` • latest (안정) — npm i leerness ← 일반 사용자 기본`);
|
|
14192
|
+
log(` • next (실험) — npm i leerness@next ← 조기 검증용`);
|
|
14193
|
+
log(` • 버전 고정 — npm i leerness@${v || '1.9.275'} ← 재현성 (권장: 운영 코드)`);
|
|
14194
|
+
log('');
|
|
14195
|
+
log(` 실험 채널 publish: leerness release sync-main --npm-tag next (또는 LEERNESS_NPM_TAG=next)`);
|
|
14196
|
+
}
|
|
14231
14197
|
function _publishToNpm(root, opts = {}) {
|
|
14232
14198
|
root = absRoot(root || process.cwd());
|
|
14233
14199
|
// .env 자동 로드 — root + 상위 3단계까지 탐색 (워크스페이스 root 의 .env 도 인식).
|
|
@@ -14285,10 +14251,12 @@ function _publishToNpm(root, opts = {}) {
|
|
|
14285
14251
|
// 3) npm publish (--userconfig 로 임시 .npmrc 사용, --access public)
|
|
14286
14252
|
// 1.9.178+: 2FA OTP 지원 — LEERNESS_NPM_OTP 환경변수 또는 --npm-otp 인자
|
|
14287
14253
|
const otp = process.env.LEERNESS_NPM_OTP || arg('--npm-otp', null) || opts.otp;
|
|
14288
|
-
|
|
14254
|
+
// 1.9.275 (UR-0026): dist-tag — latest(안정)/next(실험). 기본 latest.
|
|
14255
|
+
const npmTag = _resolveNpmTag(opts.tag || arg('--npm-tag', null));
|
|
14256
|
+
const baseArgs = ['publish', '--userconfig', tmpNpmrc, '--access', 'public', '--tag', npmTag];
|
|
14289
14257
|
if (otp) baseArgs.push(`--otp=${otp}`);
|
|
14290
14258
|
const args = opts.dryRun ? [...baseArgs, '--dry-run'] : baseArgs;
|
|
14291
|
-
log(` ${opts.dryRun ? '(dry-run) ' : ''}npm publish 시도
|
|
14259
|
+
log(` ${opts.dryRun ? '(dry-run) ' : ''}npm publish 시도 중... (dist-tag: ${npmTag})`);
|
|
14292
14260
|
const pubR = cp.spawnSync('npm', args, {
|
|
14293
14261
|
cwd: root, encoding: 'utf8', shell: true, timeout: 60000,
|
|
14294
14262
|
env: { ...process.env, npm_config_loglevel: 'warn' }
|
|
@@ -14478,7 +14446,9 @@ function releasePublish(root) {
|
|
|
14478
14446
|
|
|
14479
14447
|
// 6. npm publish (--npm-publish)
|
|
14480
14448
|
if (has('--npm-publish')) {
|
|
14481
|
-
|
|
14449
|
+
// 1.9.275 (UR-0026): dist-tag 지원 (latest/next)
|
|
14450
|
+
const npmTag = _resolveNpmTag(arg('--npm-tag', null));
|
|
14451
|
+
const args = dryRun ? ['publish', '--dry-run', '--tag', npmTag] : ['publish', '--access', 'public', '--tag', npmTag];
|
|
14482
14452
|
log('npm ' + args.join(' '));
|
|
14483
14453
|
const r = cp.spawnSync('npm', args, { cwd: root, encoding: 'utf8', shell: true });
|
|
14484
14454
|
log((r.stdout || '').split('\n').slice(-5).join('\n'));
|
|
@@ -15134,28 +15104,7 @@ function guideCmd(root, target) {
|
|
|
15134
15104
|
}
|
|
15135
15105
|
|
|
15136
15106
|
// ===== Auto update =====
|
|
15137
|
-
|
|
15138
|
-
const sa = String(a || '0').split('.').map(n => parseInt(n || '0', 10));
|
|
15139
|
-
const sb = String(b || '0').split('.').map(n => parseInt(n || '0', 10));
|
|
15140
|
-
for (let i = 0; i < 3; i++) {
|
|
15141
|
-
const x = sa[i] || 0, y = sb[i] || 0;
|
|
15142
|
-
if (x > y) return 1;
|
|
15143
|
-
if (x < y) return -1;
|
|
15144
|
-
}
|
|
15145
|
-
return 0;
|
|
15146
|
-
}
|
|
15147
|
-
function parseHarnessVersion(text) {
|
|
15148
|
-
const t = String(text || '').trim();
|
|
15149
|
-
// canonical: "1.9.0", legacy plus: "leerness@1.8.0+plus@1.0.1", legacy bare: "1.8.0", legacy "leerness@1.8.0"
|
|
15150
|
-
const plus = t.match(/plus@(\d+\.\d+\.\d+)/);
|
|
15151
|
-
const baseAt = t.match(/leerness@(\d+\.\d+\.\d+)/);
|
|
15152
|
-
const bare = t.match(/^(\d+\.\d+\.\d+)\s*$/);
|
|
15153
|
-
return {
|
|
15154
|
-
plus: plus ? plus[1] : null,
|
|
15155
|
-
base: baseAt ? baseAt[1] : (bare ? bare[1] : null),
|
|
15156
|
-
raw: t || '(not installed)'
|
|
15157
|
-
};
|
|
15158
|
-
}
|
|
15107
|
+
// compareVer, parseHarnessVersion → lib/pure-utils.js 로 이동 (1.9.274 UR-0025 1단계)
|
|
15159
15108
|
function updateCachePath(root) { return path.join(root, '.harness/cache/update-check.json'); }
|
|
15160
15109
|
function readUpdateCache(root) { try { const p = updateCachePath(root); if (!exists(p)) return null; return JSON.parse(read(p)); } catch { return null; } }
|
|
15161
15110
|
function writeUpdateCache(root, obj) { writeUtf8(updateCachePath(root), JSON.stringify({ at: Date.now(), ...obj }, null, 2) + '\n'); }
|
|
@@ -20904,6 +20853,8 @@ async function main() {
|
|
|
20904
20853
|
if (cmd === 'release' && args[1] === 'sync-main') return releaseSyncMainCmd(args[2] || arg('--path', process.cwd()));
|
|
20905
20854
|
// 1.9.235: leerness release cleanup — local release/* branches 정리 (merged 만, --apply 시 실 삭제)
|
|
20906
20855
|
if (cmd === 'release' && args[1] === 'cleanup') return releaseCleanupCmd((args[2] && !args[2].startsWith('-')) ? args[2] : arg('--path', process.cwd()));
|
|
20856
|
+
// 1.9.275 (UR-0026): leerness release channel — 안정/실험 채널 정책 (npm dist-tag)
|
|
20857
|
+
if (cmd === 'release' && args[1] === 'channel') return releaseChannelCmd((args[2] && !args[2].startsWith('-')) ? args[2] : arg('--path', process.cwd()));
|
|
20907
20858
|
// 1.9.141: feature causality graph
|
|
20908
20859
|
if (cmd === 'feature' && args[1] === 'add') return featureAddCmd(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('--')).join(' '));
|
|
20909
20860
|
if (cmd === 'feature' && args[1] === 'link') return featureLinkCmd(arg('--path', process.cwd()), args[2]);
|
|
@@ -21047,5 +20998,7 @@ module.exports = {
|
|
|
21047
20998
|
// 1.9.270: agent roles — 모델별 역할 부여 (사용자 명시) — 단위 테스트
|
|
21048
20999
|
ROLE_CATALOG, _normalizeRole, _pickModel, _rolesFile, _loadRoles, _saveRoles, _resolveRole, _suggestRoles, rolesCmd, _dispatchCommand,
|
|
21049
21000
|
// 1.9.272: 권한/보안 표면 공개 (GPT-5.5 리뷰) — 단위 테스트
|
|
21050
|
-
CAPABILITY_SURFACE, POWERFUL_COMMANDS, capabilitiesCmd
|
|
21001
|
+
CAPABILITY_SURFACE, POWERFUL_COMMANDS, capabilitiesCmd,
|
|
21002
|
+
// 1.9.275: 릴리스 채널 (npm dist-tag, GPT-5.5 리뷰) — 단위 테스트
|
|
21003
|
+
_resolveNpmTag, releaseChannelCmd
|
|
21051
21004
|
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// 1.9.274 (UR-0025 1단계, GPT-5.5 리뷰): bin/harness.js 단일 대형 파일 모듈 분리 — 점진적·비파괴 시작.
|
|
4
|
+
// 여기에는 harness 내부 상태/다른 함수에 의존하지 않는 "순수 함수"만 추출한다 (부작용 0, 단위 테스트 대상).
|
|
5
|
+
// harness.js 는 이 모듈을 require 해 동일 이름으로 사용한다. 동작 동일 — selftest 가 7종 모두 검증.
|
|
6
|
+
|
|
7
|
+
// 보안: 환경변수 키가 시크릿(TOKEN/SECRET/PASSWORD/API_KEY/PRIVATE)인지 판별.
|
|
8
|
+
function _isSecretKey(k) {
|
|
9
|
+
return /TOKEN|SECRET|PASSWORD|API_KEY|PRIVATE/i.test(k);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// semver 비교: a>b → 1, a<b → -1, 같음 → 0. (누락 파트/null 안전)
|
|
13
|
+
function compareVer(a, b) {
|
|
14
|
+
const sa = String(a || '0').split('.').map(n => parseInt(n || '0', 10));
|
|
15
|
+
const sb = String(b || '0').split('.').map(n => parseInt(n || '0', 10));
|
|
16
|
+
for (let i = 0; i < 3; i++) {
|
|
17
|
+
const x = sa[i] || 0, y = sb[i] || 0;
|
|
18
|
+
if (x > y) return 1;
|
|
19
|
+
if (x < y) return -1;
|
|
20
|
+
}
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// harness 버전 문자열 파싱: canonical "1.9.0" / legacy plus "leerness@1.8.0+plus@1.0.1" / "leerness@1.8.0".
|
|
25
|
+
function parseHarnessVersion(text) {
|
|
26
|
+
const t = String(text || '').trim();
|
|
27
|
+
const plus = t.match(/plus@(\d+\.\d+\.\d+)/);
|
|
28
|
+
const baseAt = t.match(/leerness@(\d+\.\d+\.\d+)/);
|
|
29
|
+
const bare = t.match(/^(\d+\.\d+\.\d+)\s*$/);
|
|
30
|
+
return {
|
|
31
|
+
plus: plus ? plus[1] : null,
|
|
32
|
+
base: baseAt ? baseAt[1] : (bare ? bare[1] : null),
|
|
33
|
+
raw: t || '(not installed)'
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// UTF-8 바이트열의 CJK 분류 (한국어/일본어/중국어/기타) — 인코딩 오인식 위험 감지용.
|
|
38
|
+
function _classifyCJK(buf, len) {
|
|
39
|
+
let korean = 0, japanese = 0, chinese = 0, other = 0;
|
|
40
|
+
for (let i = 0; i < Math.min(buf.length, len); i++) {
|
|
41
|
+
const b = buf[i];
|
|
42
|
+
if (b < 0x80) continue;
|
|
43
|
+
if (b >= 0xEA && b <= 0xED) korean++;
|
|
44
|
+
else if (b === 0xE3) japanese++;
|
|
45
|
+
else if (b >= 0xE4 && b <= 0xE9) chinese++;
|
|
46
|
+
else other++;
|
|
47
|
+
}
|
|
48
|
+
return { korean, japanese, chinese, other };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// CJK 분류 결과 → 위험 라벨 (Windows 코드페이지 오인식 안내).
|
|
52
|
+
function _riskLabel(cjk) {
|
|
53
|
+
if (cjk.korean >= cjk.japanese && cjk.korean >= cjk.chinese && cjk.korean > 0) {
|
|
54
|
+
return { type: 'korean', risk: 'Windows 한국어 PowerShell 에서 CP949 로 오인식 가능 (BOM 추가 권장)' };
|
|
55
|
+
}
|
|
56
|
+
if (cjk.japanese > cjk.korean && cjk.japanese >= cjk.chinese) {
|
|
57
|
+
return { type: 'japanese', risk: 'Windows 일본어 PowerShell 에서 CP932 (Shift-JIS) 로 오인식 가능 (BOM 추가 권장)' };
|
|
58
|
+
}
|
|
59
|
+
if (cjk.chinese > 0) {
|
|
60
|
+
return { type: 'chinese', risk: 'Windows 중국어 PowerShell 에서 CP936 (GBK) 로 오인식 가능 (BOM 추가 권장)' };
|
|
61
|
+
}
|
|
62
|
+
return { type: 'non-ascii', risk: 'Windows 비-ASCII 셸 스크립트 — BOM 없는 UTF-8 인코딩 오인식 가능 (BOM 추가 권장)' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// OS 시스템 언어 감지 (UR-0022): POSIX env > Intl ICU locale > null.
|
|
66
|
+
function _detectSystemLang(env) {
|
|
67
|
+
env = env || process.env;
|
|
68
|
+
const raw = String(env.LC_ALL || env.LC_CTYPE || env.LANG || env.LANGUAGE || '').toLowerCase();
|
|
69
|
+
if (raw && raw !== 'c' && raw !== 'posix') {
|
|
70
|
+
if (/(^|[^a-z])ko([_\-.]|$)|korean|[_-]kr([_\-.]|$)/.test(raw)) return 'ko';
|
|
71
|
+
if (/(^|[^a-z])en([_\-.]|$)|english|[_-](us|gb)([_\-.]|$)/.test(raw)) return 'en';
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const loc = (Intl.DateTimeFormat().resolvedOptions().locale || '').toLowerCase();
|
|
75
|
+
const primary = loc.split('-')[0];
|
|
76
|
+
if (primary === 'ko') return 'ko';
|
|
77
|
+
if (primary === 'en') return 'en';
|
|
78
|
+
} catch {}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// CLI `--help` 출력에서 슬래시 명령/하위명령 best-effort 파싱 (UR-0021 3단계). 순수 문자열 처리.
|
|
83
|
+
function _parseSlashFromHelp(text, invoke = 'slash') {
|
|
84
|
+
const out = [];
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
87
|
+
for (const raw of lines) {
|
|
88
|
+
const ln = raw.replace(/\x1b\[[0-9;]*m/g, ''); // ANSI 색상 제거
|
|
89
|
+
if (invoke === 'subcommand') {
|
|
90
|
+
const m = ln.match(/^\s{2,}([a-z][a-z0-9][\w-]*)\s{2,}(\S.*)$/);
|
|
91
|
+
if (m && !/^--/.test(m[1])) {
|
|
92
|
+
const cmd = m[1];
|
|
93
|
+
if (!seen.has(cmd) && cmd.length <= 24) { seen.add(cmd); out.push({ cmd, desc: m[2].trim().slice(0, 80) }); }
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const m = ln.match(/^\s*(\/[a-zA-Z][\w-]*)(?:\s+[-–:]?\s*(.*))?$/);
|
|
98
|
+
if (m) {
|
|
99
|
+
const cmd = m[1];
|
|
100
|
+
if (!seen.has(cmd) && cmd.length <= 24) { seen.add(cmd); out.push({ cmd, desc: (m[2] || '').trim().slice(0, 80) }); }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
_isSecretKey, compareVer, parseHarnessVersion,
|
|
108
|
+
_classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp
|
|
109
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.275",
|
|
4
4
|
"description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"files": [
|
|
40
40
|
"bin",
|
|
41
|
+
"lib",
|
|
41
42
|
"scripts",
|
|
42
43
|
"docs",
|
|
43
44
|
"README.md",
|
package/scripts/e2e.js
CHANGED
|
@@ -2783,5 +2783,37 @@ total++;
|
|
|
2783
2783
|
if (!ok) failed++;
|
|
2784
2784
|
}
|
|
2785
2785
|
|
|
2786
|
+
// 1.9.274 회귀 (UR-0025 1단계): lib/ 모듈 분리 — pure-utils.js 존재 + require + 7종 export + 패키지 files 포함.
|
|
2787
|
+
total++;
|
|
2788
|
+
{
|
|
2789
|
+
const libPath = path.resolve(__dirname, '..', 'lib', 'pure-utils.js');
|
|
2790
|
+
const libExists = fs.existsSync(libPath);
|
|
2791
|
+
let exportsOk = false;
|
|
2792
|
+
try {
|
|
2793
|
+
const m = require(libPath);
|
|
2794
|
+
exportsOk = ['_isSecretKey','compareVer','parseHarnessVersion','_classifyCJK','_riskLabel','_detectSystemLang','_parseSlashFromHelp']
|
|
2795
|
+
.every(n => typeof m[n] === 'function') && m.compareVer('1.9.1','1.9.0') === 1 && m._detectSystemLang({ LANG: 'ko_KR.UTF-8' }) === 'ko';
|
|
2796
|
+
} catch {}
|
|
2797
|
+
let filesOk = false;
|
|
2798
|
+
try { const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8')); filesOk = Array.isArray(pkg.files) && pkg.files.includes('lib'); } catch {}
|
|
2799
|
+
const ok = libExists && exportsOk && filesOk;
|
|
2800
|
+
console.log(ok ? '✓ B(1.9.274) lib 모듈 분리: pure-utils 7종 export + 동작 + files 포함' : `✗ lib 분리 실패 (exists=${libExists} exports=${exportsOk} files=${filesOk})`);
|
|
2801
|
+
if (!ok) failed++;
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
// 1.9.275 회귀 (UR-0026): release channel — 안정/실험 채널 정책 (npm dist-tag)
|
|
2805
|
+
total++;
|
|
2806
|
+
{
|
|
2807
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'release', 'channel', tmp, '--json', '--offline'], { encoding: 'utf8', timeout: 15000 });
|
|
2808
|
+
let jsonOk = false;
|
|
2809
|
+
try { const j = JSON.parse(r.stdout); jsonOk = j.defaultPublishTag === 'latest' && j.policy && j.policy.stable === 'latest' && j.policy.experimental === 'next'; } catch {}
|
|
2810
|
+
const r2 = cp.spawnSync(process.execPath, [CLI, 'release', 'channel', tmp, '--json', '--offline'], { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_NPM_TAG: 'next' } });
|
|
2811
|
+
let nextOk = false;
|
|
2812
|
+
try { nextOk = JSON.parse(r2.stdout).defaultPublishTag === 'next'; } catch {}
|
|
2813
|
+
const ok = jsonOk && nextOk;
|
|
2814
|
+
console.log(ok ? '✓ B(1.9.275) release channel: latest 기본 + LEERNESS_NPM_TAG=next 반영 + 정책' : `✗ release channel 실패 (json=${jsonOk} next=${nextOk})`);
|
|
2815
|
+
if (!ok) failed++;
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2786
2818
|
console.log(`\nE2E result: ${total - failed}/${total} passed`);
|
|
2787
2819
|
if (failed > 0) process.exit(1);
|