leerness 1.9.32 → 1.9.34
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 +73 -0
- package/README.md +288 -342
- package/bin/harness.js +306 -66
- package/package.json +1 -1
- package/scripts/e2e.js +79 -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.34';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -363,23 +363,51 @@ async function resolveInstallOptions(root, opts = {}) {
|
|
|
363
363
|
let lang = explicitLang ? detectLanguageValue(root, explicitLang) : detectLanguageValue(root, 'auto');
|
|
364
364
|
let skills = explicitSkills ? parseSkillsValue(explicitSkills) : [];
|
|
365
365
|
const shouldAsk = !has('--yes') && !opts.nonInteractive && process.stdin.isTTY && process.stdout.isTTY && !opts.migration;
|
|
366
|
+
// 1.9.34: 인터랙티브 multi-select (방향키 + Space + Enter) — 기존 숫자 선택 폴백 유지
|
|
367
|
+
// --no-interactive-select 또는 LEERNESS_NO_INTERACTIVE=1 → 구식 숫자 선택
|
|
368
|
+
const useInteractive = shouldAsk && !has('--no-interactive-select') && process.env.LEERNESS_NO_INTERACTIVE !== '1';
|
|
366
369
|
if (shouldAsk && !explicitLang) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
370
|
+
if (useInteractive) {
|
|
371
|
+
const langOpt = await _selectOne('설치 언어를 선택하세요', [
|
|
372
|
+
{ label: '자동 감지', description: '디렉토리/파일 분석 (한국어/영어 자동 판별)', id: 'auto' },
|
|
373
|
+
{ label: '한국어', description: '모든 인스트럭션을 한국어로 생성', id: 'ko' },
|
|
374
|
+
{ label: 'English', description: '모든 인스트럭션을 영어로 생성', id: 'en' }
|
|
375
|
+
], { defaultIndex: 0 });
|
|
376
|
+
lang = langOpt && langOpt.id ? detectLanguageValue(root, langOpt.id) : detectLanguageValue(root, 'auto');
|
|
377
|
+
} else {
|
|
378
|
+
log('\n설치 언어를 선택하세요.');
|
|
379
|
+
log('1) 자동 감지'); log('2) 한국어'); log('3) English');
|
|
380
|
+
const a = await ask('선택 [1]: ');
|
|
381
|
+
lang = a === '2' ? 'ko' : a === '3' ? 'en' : detectLanguageValue(root, 'auto');
|
|
382
|
+
}
|
|
371
383
|
}
|
|
372
384
|
if (shouldAsk && !explicitSkills) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
385
|
+
if (useInteractive) {
|
|
386
|
+
// 카탈로그에서 옵션 생성
|
|
387
|
+
const cat = Object.entries(skillCatalog).map(([id, meta]) => ({
|
|
388
|
+
id, label: id, description: (meta.displayNameKo || id).slice(0, 50)
|
|
389
|
+
}));
|
|
390
|
+
// 추천 4개의 인덱스 계산
|
|
391
|
+
const recommended = ['office', 'commerce-api', 'ai-verified-skill-publisher', 'feature-implementation'];
|
|
392
|
+
const defaults = recommended.map(id => cat.findIndex(c => c.id === id)).filter(i => i >= 0);
|
|
393
|
+
const picked = await _selectMany(
|
|
394
|
+
'설치할 스킬 라이브러리 (Space=토글, a=전체, n=해제, Enter=확정)',
|
|
395
|
+
cat,
|
|
396
|
+
{ defaults }
|
|
397
|
+
);
|
|
398
|
+
skills = picked.map(p => p.id);
|
|
399
|
+
} else {
|
|
400
|
+
log('\n설치할 스킬 라이브러리를 선택하세요.');
|
|
401
|
+
log('0) 기본 하네스만 설치');
|
|
402
|
+
log('1) 추천: office, commerce-api, ai-verified-skill-publisher, feature-implementation');
|
|
403
|
+
log('2) 전체 스킬 설치'); log('3) 직접 입력');
|
|
404
|
+
skillList();
|
|
405
|
+
const a = await ask('선택 [1]: ');
|
|
406
|
+
if (!a || a === '1') skills = parseSkillsValue('recommended');
|
|
407
|
+
else if (a === '2') skills = parseSkillsValue('all');
|
|
408
|
+
else if (a === '3') skills = parseSkillsValue(await ask('스킬 ID를 쉼표로 입력: '));
|
|
409
|
+
else if (a === '0') skills = [];
|
|
410
|
+
}
|
|
383
411
|
}
|
|
384
412
|
return { lang, skills };
|
|
385
413
|
}
|
|
@@ -388,6 +416,10 @@ async function install(root, opts = {}) {
|
|
|
388
416
|
root = absRoot(root); mkdirp(root);
|
|
389
417
|
// 1.9.32: init 시 ASCII 배너 + 빠른 시작 가이드 (migrate는 quiet)
|
|
390
418
|
if (!opts.migration && !has('--no-banner')) _banner({ quickStart: !opts.dry });
|
|
419
|
+
// 1.9.33: npx 캐시로 옛 버전이 실행될 때 경고 (migrate/--no-stale-check 시 스킵)
|
|
420
|
+
if (!opts.migration && !has('--no-stale-check') && !opts.nonInteractive) {
|
|
421
|
+
try { await _warnIfStale(root); } catch {}
|
|
422
|
+
}
|
|
391
423
|
const resolved = await resolveInstallOptions(root, opts);
|
|
392
424
|
const lang = resolved.lang;
|
|
393
425
|
const skills = resolved.skills;
|
|
@@ -2320,49 +2352,112 @@ function _checkAgent(agent, opts = {}) {
|
|
|
2320
2352
|
};
|
|
2321
2353
|
}
|
|
2322
2354
|
|
|
2323
|
-
// 1.9.
|
|
2355
|
+
// 1.9.33: npx 캐시 함정 방지 — install 진입 시 npm latest와 비교, stale이면 경고
|
|
2356
|
+
async function _warnIfStale(root, opts = {}) {
|
|
2357
|
+
if (process.env.LEERNESS_NO_STALE_CHECK === '1') return null;
|
|
2358
|
+
const offline = process.env.LEERNESS_OFFLINE === '1';
|
|
2359
|
+
// 24h 캐시: .harness/cache/update-check.json 재사용 — 캐시 fresh면 OFFLINE이어도 비교는 수행
|
|
2360
|
+
try {
|
|
2361
|
+
let latest = null;
|
|
2362
|
+
const cached = readUpdateCache(root);
|
|
2363
|
+
if (cacheFresh(cached, 24) && cached.nextLeerness) {
|
|
2364
|
+
latest = cached.nextLeerness;
|
|
2365
|
+
} else if (!offline) {
|
|
2366
|
+
// 캐시 없음 + 온라인 → npm view 호출 (timeout 8초 — 네트워크 끊겼어도 init 진행 차단 X)
|
|
2367
|
+
latest = await Promise.race([
|
|
2368
|
+
fetchNpmLatest('leerness'),
|
|
2369
|
+
new Promise(resolve => setTimeout(() => resolve(null), 8000))
|
|
2370
|
+
]);
|
|
2371
|
+
if (latest) {
|
|
2372
|
+
try { writeUpdateCache(root, { nextLeerness: latest, runningCli: VERSION }); } catch {}
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
// offline + 캐시 없으면 비교 스킵 (네트워크 차단 환경)
|
|
2376
|
+
if (!latest) return null;
|
|
2377
|
+
if (compareVer(latest, VERSION) > 0) {
|
|
2378
|
+
// 옛 버전이 실행 중. ANSI 노란/빨강.
|
|
2379
|
+
const isTty = process.stdout && process.stdout.isTTY;
|
|
2380
|
+
const C = isTty ? { y: s => `\x1b[33m${s}\x1b[0m`, r: s => `\x1b[31m${s}\x1b[0m`, b: s => `\x1b[1m${s}\x1b[0m`, d: s => `\x1b[2m${s}\x1b[0m` }
|
|
2381
|
+
: { y: s => s, r: s => s, b: s => s, d: s => s };
|
|
2382
|
+
log('');
|
|
2383
|
+
log(C.y(' ⚠ ') + C.b(C.r(`옛 버전이 실행 중입니다 — v${VERSION} → v${latest} (npm 최신)`)));
|
|
2384
|
+
log('');
|
|
2385
|
+
log(C.d(' npm registry latest: ') + C.b(`v${latest}`));
|
|
2386
|
+
log(C.d(' 이 CLI가 실행한 버전: ') + C.b(`v${VERSION}`) + C.d(' (npx 캐시 또는 글로벌 설치 stale)'));
|
|
2387
|
+
log('');
|
|
2388
|
+
log(C.d(' 해결 — 둘 중 하나 실행 후 다시 시도:'));
|
|
2389
|
+
log(' ' + C.b('npx --yes clear-npx-cache && npx leerness@latest init .'));
|
|
2390
|
+
log(' ' + C.b('npm i -g leerness@latest → leerness init .'));
|
|
2391
|
+
log('');
|
|
2392
|
+
log(C.d(' (이 경고는 LEERNESS_NO_STALE_CHECK=1 또는 --no-stale-check로 끌 수 있습니다)'));
|
|
2393
|
+
log('');
|
|
2394
|
+
return { stale: true, current: VERSION, latest };
|
|
2395
|
+
}
|
|
2396
|
+
return { stale: false, current: VERSION, latest };
|
|
2397
|
+
} catch (e) {
|
|
2398
|
+
// 어떤 이유로든 실패해도 init 진행 차단 X
|
|
2399
|
+
return null;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
// 1.9.32/1.9.34: ASCII 배너 — init/version 시 출력 (그라데이션 다중 색상 강화)
|
|
2324
2404
|
function _banner(opts = {}) {
|
|
2325
2405
|
const v = `v${VERSION}`;
|
|
2326
|
-
// 사용자 콘솔이 너무 좁을 때(<70) 또는 LEERNESS_NO_BANNER=1이면 스킵
|
|
2327
2406
|
const cols = process.stdout && process.stdout.columns ? process.stdout.columns : 80;
|
|
2328
2407
|
if (process.env.LEERNESS_NO_BANNER === '1') return;
|
|
2329
2408
|
if (cols < 70) {
|
|
2330
2409
|
log(`Leerness ${v} — 한국어 우선 AI 개발 하네스`);
|
|
2331
2410
|
return;
|
|
2332
2411
|
}
|
|
2333
|
-
// ANSI 색상 (TTY일 때만)
|
|
2334
2412
|
const isTty = process.stdout && process.stdout.isTTY;
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2413
|
+
// 1.9.34: ANSI 256색 그라데이션 (cyan → magenta) + 굵게
|
|
2414
|
+
// 색상 안전 fallback (Windows 구버전 cmd는 256색 불가 시 그냥 기본색)
|
|
2415
|
+
const mk = (code) => isTty ? `\x1b[38;5;${code}m` : '';
|
|
2416
|
+
const reset = isTty ? '\x1b[0m' : '';
|
|
2417
|
+
const bold = isTty ? '\x1b[1m' : '';
|
|
2418
|
+
// 그라데이션 색상 (cyan/teal/blue/purple/magenta): 6 LEERNESS 라인 × 단색씩
|
|
2419
|
+
const grad = [51, 45, 39, 33, 99, 165]; // cyan → magenta
|
|
2420
|
+
const C = {
|
|
2421
|
+
cyan: s => isTty ? `\x1b[36m${s}\x1b[0m` : s,
|
|
2422
|
+
dim: s => isTty ? `\x1b[2m${s}\x1b[0m` : s,
|
|
2423
|
+
bold: s => isTty ? `\x1b[1m${s}\x1b[0m` : s,
|
|
2424
|
+
green: s => isTty ? `\x1b[32m${s}\x1b[0m` : s,
|
|
2425
|
+
yel: s => isTty ? `\x1b[33m${s}\x1b[0m` : s,
|
|
2426
|
+
mag: s => isTty ? `\x1b[35m${s}\x1b[0m` : s,
|
|
2427
|
+
g: (s, code) => isTty ? `${mk(code)}${bold}${s}${reset}` : s
|
|
2428
|
+
};
|
|
2429
|
+
// 박스 외곽선 + ASCII 본문 그라데이션
|
|
2430
|
+
const asciiLines = [
|
|
2431
|
+
'██╗ ███████╗███████╗██████╗ ███╗ ██╗███████╗███████╗',
|
|
2432
|
+
'██║ ██╔════╝██╔════╝██╔══██╗████╗ ██║██╔════╝██╔════╝',
|
|
2433
|
+
'██║ █████╗ █████╗ ██████╔╝██╔██╗ ██║█████╗ ███████╗',
|
|
2434
|
+
'██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║',
|
|
2435
|
+
'███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║',
|
|
2436
|
+
'╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝'
|
|
2437
|
+
];
|
|
2438
|
+
const border = C.cyan;
|
|
2340
2439
|
const lines = [
|
|
2341
2440
|
'',
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
C.cyan(' ║ ') + C.bold('██╗ ███████╗███████╗██████╗ ███╗ ██╗███████╗███████╗') + C.cyan(' ║'),
|
|
2345
|
-
C.cyan(' ║ ') + C.bold('██║ ██╔════╝██╔════╝██╔══██╗████╗ ██║██╔════╝██╔════╝') + C.cyan(' ║'),
|
|
2346
|
-
C.cyan(' ║ ') + C.bold('██║ █████╗ █████╗ ██████╔╝██╔██╗ ██║█████╗ ███████╗') + C.cyan(' ║'),
|
|
2347
|
-
C.cyan(' ║ ') + C.bold('██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║') + C.cyan(' ║'),
|
|
2348
|
-
C.cyan(' ║ ') + C.bold('███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║') + C.cyan(' ║'),
|
|
2349
|
-
C.cyan(' ║ ') + C.bold('╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝') + C.cyan(' ║'),
|
|
2350
|
-
C.cyan(' ║ ║'),
|
|
2351
|
-
// ASCII-only 라인은 정확히 60칸 (좌2 + 본문56 + 우2)
|
|
2352
|
-
C.cyan(' ║ ') + C.green(`${v.padEnd(10)}`) + C.dim('Korean-first AI Development Harness') + C.cyan(' ║'),
|
|
2353
|
-
C.cyan(' ║ ') + C.dim('verify · reuse-map · handoff · agents · orchestrate') + C.cyan(' ║'),
|
|
2354
|
-
C.cyan(' ║ ║'),
|
|
2355
|
-
C.cyan(' ╚══════════════════════════════════════════════════════════════╝'),
|
|
2356
|
-
' ' + C.dim('한국어 우선 AI 개발 하네스 — verify · reuse-map · handoff · agents'),
|
|
2357
|
-
''
|
|
2441
|
+
border(' ╔══════════════════════════════════════════════════════════════╗'),
|
|
2442
|
+
border(' ║ ║'),
|
|
2358
2443
|
];
|
|
2444
|
+
for (let i = 0; i < asciiLines.length; i++) {
|
|
2445
|
+
lines.push(border(' ║ ') + C.g(asciiLines[i], grad[i]) + border(' ║'));
|
|
2446
|
+
}
|
|
2447
|
+
lines.push(border(' ║ ║'));
|
|
2448
|
+
lines.push(border(' ║ ') + C.green(`${v.padEnd(10)}`) + C.dim('Korean-first AI Development Harness') + border(' ║'));
|
|
2449
|
+
lines.push(border(' ║ ') + C.yel('★ ') + C.dim('verify · reuse-map · handoff · agents · orchestrate') + border(' ║'));
|
|
2450
|
+
lines.push(border(' ║ ║'));
|
|
2451
|
+
lines.push(border(' ╚══════════════════════════════════════════════════════════════╝'));
|
|
2452
|
+
lines.push(' ' + C.dim('한국어 우선 AI 개발 하네스 — ') + C.mag('verify') + C.dim(' · ') + C.mag('reuse-map') + C.dim(' · ') + C.mag('handoff') + C.dim(' · ') + C.mag('agents'));
|
|
2453
|
+
lines.push('');
|
|
2359
2454
|
for (const ln of lines) log(ln);
|
|
2360
2455
|
if (opts.quickStart) {
|
|
2361
|
-
log(C.
|
|
2362
|
-
log(C.
|
|
2363
|
-
log(C.
|
|
2364
|
-
log(C.
|
|
2365
|
-
log(C.
|
|
2456
|
+
log(C.bold(C.cyan(' ✨ 빠른 시작')));
|
|
2457
|
+
log(' ' + C.green('npx leerness@latest init .') + C.dim(' # 신규 프로젝트'));
|
|
2458
|
+
log(' ' + C.green('npx leerness@latest setup-agents .') + C.dim(' # 외부 AI CLI 설정'));
|
|
2459
|
+
log(' ' + C.green('npx leerness handoff .') + C.dim(' # 컨텍스트 적재'));
|
|
2460
|
+
log(' ' + C.green('npx leerness verify-claim T-0001 --run-tests') + C.dim(' # 자동 검증'));
|
|
2366
2461
|
log('');
|
|
2367
2462
|
}
|
|
2368
2463
|
}
|
|
@@ -2390,6 +2485,132 @@ async function _confirm(question, defaultYes = false) {
|
|
|
2390
2485
|
return /^(y|yes|예|네|ㅇ|1|true)$/i.test(ans.trim());
|
|
2391
2486
|
}
|
|
2392
2487
|
|
|
2488
|
+
// 1.9.34: 방향키 + 스페이스 + Enter 인터랙티브 single-select prompt (raw mode)
|
|
2489
|
+
// 비-TTY 또는 LEERNESS_NO_PROMPT=1 → 첫 옵션 반환
|
|
2490
|
+
async function _selectOne(question, options, opts = {}) {
|
|
2491
|
+
if (!process.stdin.isTTY || process.env.LEERNESS_NO_PROMPT === '1' || has('--yes') || has('-y')) {
|
|
2492
|
+
return opts.defaultIndex != null ? options[opts.defaultIndex] : options[0];
|
|
2493
|
+
}
|
|
2494
|
+
const stdin = process.stdin;
|
|
2495
|
+
const stdout = process.stdout;
|
|
2496
|
+
const isTty = stdout.isTTY;
|
|
2497
|
+
const C = isTty ? {
|
|
2498
|
+
cyan: s => `\x1b[36m${s}\x1b[0m`, dim: s => `\x1b[2m${s}\x1b[0m`,
|
|
2499
|
+
bold: s => `\x1b[1m${s}\x1b[0m`, green: s => `\x1b[32m${s}\x1b[0m`,
|
|
2500
|
+
inv: s => `\x1b[7m${s}\x1b[0m`, mag: s => `\x1b[35m${s}\x1b[0m`
|
|
2501
|
+
} : { cyan: s => s, dim: s => s, bold: s => s, green: s => s, inv: s => s, mag: s => s };
|
|
2502
|
+
return new Promise(resolve => {
|
|
2503
|
+
let idx = opts.defaultIndex || 0;
|
|
2504
|
+
if (idx < 0 || idx >= options.length) idx = 0;
|
|
2505
|
+
const render = (first) => {
|
|
2506
|
+
if (!first) {
|
|
2507
|
+
// 이전 출력 지우기: options.length + 2줄 (제목 + 안내)
|
|
2508
|
+
stdout.write(`\x1b[${options.length + 2}A`);
|
|
2509
|
+
}
|
|
2510
|
+
stdout.write(`\r${C.bold(question)}\n`);
|
|
2511
|
+
stdout.write(`${C.dim(' ↑↓ 이동, Enter 확정, q 취소')}\n`);
|
|
2512
|
+
for (let i = 0; i < options.length; i++) {
|
|
2513
|
+
const label = typeof options[i] === 'string' ? options[i] : (options[i].label || String(options[i]));
|
|
2514
|
+
const desc = typeof options[i] === 'object' && options[i].description ? C.dim(' — ' + options[i].description) : '';
|
|
2515
|
+
const cursor = i === idx ? C.cyan('❯') : ' ';
|
|
2516
|
+
const text = i === idx ? C.bold(C.green(label)) : label;
|
|
2517
|
+
stdout.write(`\x1b[2K\r ${cursor} ${text}${desc}\n`);
|
|
2518
|
+
}
|
|
2519
|
+
};
|
|
2520
|
+
render(true);
|
|
2521
|
+
stdin.setRawMode && stdin.setRawMode(true);
|
|
2522
|
+
stdin.resume(); stdin.setEncoding('utf8');
|
|
2523
|
+
const onData = (buf) => {
|
|
2524
|
+
const key = String(buf);
|
|
2525
|
+
// 화살표는 ESC [ A/B
|
|
2526
|
+
if (key === '[A' || key === 'k') { idx = (idx - 1 + options.length) % options.length; render(false); }
|
|
2527
|
+
else if (key === '[B' || key === 'j') { idx = (idx + 1) % options.length; render(false); }
|
|
2528
|
+
else if (key === '\r' || key === '\n') {
|
|
2529
|
+
cleanup();
|
|
2530
|
+
stdout.write('\n');
|
|
2531
|
+
resolve(options[idx]);
|
|
2532
|
+
} else if (key === '' || key === 'q' || key === '') {
|
|
2533
|
+
cleanup();
|
|
2534
|
+
stdout.write('\n' + C.dim(' 취소됨') + '\n');
|
|
2535
|
+
resolve(opts.defaultIndex != null ? options[opts.defaultIndex] : null);
|
|
2536
|
+
}
|
|
2537
|
+
};
|
|
2538
|
+
const cleanup = () => {
|
|
2539
|
+
stdin.setRawMode && stdin.setRawMode(false);
|
|
2540
|
+
stdin.removeListener('data', onData);
|
|
2541
|
+
stdin.pause();
|
|
2542
|
+
};
|
|
2543
|
+
stdin.on('data', onData);
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// 1.9.34: 방향키 + 스페이스 + Enter 인터랙티브 multi-select prompt (raw mode)
|
|
2548
|
+
// 비-TTY/--yes → opts.defaults 또는 빈 배열 반환
|
|
2549
|
+
async function _selectMany(question, options, opts = {}) {
|
|
2550
|
+
if (!process.stdin.isTTY || process.env.LEERNESS_NO_PROMPT === '1' || has('--yes') || has('-y')) {
|
|
2551
|
+
return (opts.defaults || []).map(d => typeof d === 'number' ? options[d] : d).filter(Boolean);
|
|
2552
|
+
}
|
|
2553
|
+
const stdin = process.stdin;
|
|
2554
|
+
const stdout = process.stdout;
|
|
2555
|
+
const isTty = stdout.isTTY;
|
|
2556
|
+
const C = isTty ? {
|
|
2557
|
+
cyan: s => `\x1b[36m${s}\x1b[0m`, dim: s => `\x1b[2m${s}\x1b[0m`,
|
|
2558
|
+
bold: s => `\x1b[1m${s}\x1b[0m`, green: s => `\x1b[32m${s}\x1b[0m`,
|
|
2559
|
+
inv: s => `\x1b[7m${s}\x1b[0m`, mag: s => `\x1b[35m${s}\x1b[0m`,
|
|
2560
|
+
yel: s => `\x1b[33m${s}\x1b[0m`
|
|
2561
|
+
} : { cyan: s => s, dim: s => s, bold: s => s, green: s => s, inv: s => s, mag: s => s, yel: s => s };
|
|
2562
|
+
return new Promise(resolve => {
|
|
2563
|
+
let idx = 0;
|
|
2564
|
+
const selected = new Set((opts.defaults || []).map(d => typeof d === 'number' ? d : options.findIndex(o => o === d || (o && o.id === d))).filter(i => i >= 0));
|
|
2565
|
+
const render = (first) => {
|
|
2566
|
+
if (!first) stdout.write(`\x1b[${options.length + 2}A`);
|
|
2567
|
+
stdout.write(`\r${C.bold(question)}\n`);
|
|
2568
|
+
stdout.write(`${C.dim(' ↑↓ 이동, Space 토글, a 전체, n 해제, Enter 확정, q 취소')}\n`);
|
|
2569
|
+
for (let i = 0; i < options.length; i++) {
|
|
2570
|
+
const opt = options[i];
|
|
2571
|
+
const label = typeof opt === 'string' ? opt : (opt.label || String(opt));
|
|
2572
|
+
const desc = typeof opt === 'object' && opt.description ? C.dim(' — ' + opt.description) : '';
|
|
2573
|
+
const mark = selected.has(i) ? C.green('◉') : C.dim('◯');
|
|
2574
|
+
const cursor = i === idx ? C.cyan('❯') : ' ';
|
|
2575
|
+
const text = i === idx ? C.bold(label) : label;
|
|
2576
|
+
stdout.write(`\x1b[2K\r ${cursor} ${mark} ${text}${desc}\n`);
|
|
2577
|
+
}
|
|
2578
|
+
};
|
|
2579
|
+
render(true);
|
|
2580
|
+
stdin.setRawMode && stdin.setRawMode(true);
|
|
2581
|
+
stdin.resume(); stdin.setEncoding('utf8');
|
|
2582
|
+
const onData = (buf) => {
|
|
2583
|
+
const key = String(buf);
|
|
2584
|
+
if (key === '[A' || key === 'k') { idx = (idx - 1 + options.length) % options.length; render(false); }
|
|
2585
|
+
else if (key === '[B' || key === 'j') { idx = (idx + 1) % options.length; render(false); }
|
|
2586
|
+
else if (key === ' ') {
|
|
2587
|
+
if (selected.has(idx)) selected.delete(idx); else selected.add(idx);
|
|
2588
|
+
render(false);
|
|
2589
|
+
} else if (key === 'a' || key === 'A') {
|
|
2590
|
+
for (let i = 0; i < options.length; i++) selected.add(i);
|
|
2591
|
+
render(false);
|
|
2592
|
+
} else if (key === 'n' || key === 'N') {
|
|
2593
|
+
selected.clear();
|
|
2594
|
+
render(false);
|
|
2595
|
+
} else if (key === '\r' || key === '\n') {
|
|
2596
|
+
cleanup();
|
|
2597
|
+
stdout.write('\n');
|
|
2598
|
+
resolve([...selected].sort((a, b) => a - b).map(i => options[i]));
|
|
2599
|
+
} else if (key === '' || key === 'q' || key === '') {
|
|
2600
|
+
cleanup();
|
|
2601
|
+
stdout.write('\n' + C.dim(' 취소됨 (기본값 사용)') + '\n');
|
|
2602
|
+
resolve((opts.defaults || []).map(d => typeof d === 'number' ? options[d] : d).filter(Boolean));
|
|
2603
|
+
}
|
|
2604
|
+
};
|
|
2605
|
+
const cleanup = () => {
|
|
2606
|
+
stdin.setRawMode && stdin.setRawMode(false);
|
|
2607
|
+
stdin.removeListener('data', onData);
|
|
2608
|
+
stdin.pause();
|
|
2609
|
+
};
|
|
2610
|
+
stdin.on('data', onData);
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2393
2614
|
// 1.9.32: .env 파일에 KEY=value 라인 누적/갱신 (이미 키가 있으면 값 교체, 없으면 append)
|
|
2394
2615
|
function _upsertEnvLine(envPath, key, value) {
|
|
2395
2616
|
let body = exists(envPath) ? read(envPath) : '';
|
|
@@ -2410,7 +2631,8 @@ function _tryInstallAgent(agent) {
|
|
|
2410
2631
|
return { ok: false, message: `exit ${r.status}` + (r.error ? ` (${r.error.code || r.error.message})` : '') };
|
|
2411
2632
|
}
|
|
2412
2633
|
|
|
2413
|
-
// 1.9.32: setup-agents 워크플로 — init 직후 또는 단독 명령
|
|
2634
|
+
// 1.9.32/1.9.34: setup-agents 워크플로 — init 직후 또는 단독 명령
|
|
2635
|
+
// 1.9.34: 방향키/스페이스 multi-select 도입 (LEERNESS_NO_INTERACTIVE=1 → 기존 yes/no 폴백)
|
|
2414
2636
|
async function setupAgentsCmd(root, opts = {}) {
|
|
2415
2637
|
root = absRoot(root || process.cwd());
|
|
2416
2638
|
_loadEnvFile(root);
|
|
@@ -2418,12 +2640,11 @@ async function setupAgentsCmd(root, opts = {}) {
|
|
|
2418
2640
|
const envPath = path.join(root, '.env');
|
|
2419
2641
|
|
|
2420
2642
|
log('');
|
|
2421
|
-
log('# 외부 AI CLI 설정 (1.9.
|
|
2643
|
+
log('# 외부 AI CLI 설정 (1.9.34)');
|
|
2422
2644
|
log('메인 에이전트가 작업을 분배할 sub-agent 후보를 선택하세요.');
|
|
2423
2645
|
log('각 CLI는 *환경변수 활성화 + PATH 존재* 둘 다 충족할 때 ready 상태가 됩니다.');
|
|
2424
2646
|
log('');
|
|
2425
2647
|
|
|
2426
|
-
// 비대화형(--yes 또는 비-TTY)이면 모든 CLI를 기존 값 유지 + 안내만
|
|
2427
2648
|
const interactive = !!process.stdin.isTTY && !has('--yes') && !has('-y') && process.env.LEERNESS_NO_PROMPT !== '1';
|
|
2428
2649
|
if (!interactive) {
|
|
2429
2650
|
log(' 비대화형 모드 — 환경변수는 변경하지 않습니다. 수동 편집:');
|
|
@@ -2432,38 +2653,57 @@ async function setupAgentsCmd(root, opts = {}) {
|
|
|
2432
2653
|
return;
|
|
2433
2654
|
}
|
|
2434
2655
|
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
log(`---`);
|
|
2439
|
-
log(`▸ ${agent.id} — ${agent.desc}`);
|
|
2440
|
-
log(` 설치 상태: ${status.installed ? '🟢 설치됨 (' + (status.version || '?') + ')' : '⚪ 미설치'}`);
|
|
2441
|
-
log(` 활성 상태: ${status.enabled ? '🟢 ' + agent.envFlag + '=1' : '🟡 ' + agent.envFlag + '=0 또는 미설정'}`);
|
|
2656
|
+
// 1.9.34: multi-select로 활성화할 CLI 일괄 선택
|
|
2657
|
+
const useInteractive = process.env.LEERNESS_NO_INTERACTIVE !== '1';
|
|
2658
|
+
const statuses = EXTERNAL_AGENTS.map(a => ({ agent: a, status: _checkAgent(a) }));
|
|
2442
2659
|
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2660
|
+
let toEnable = new Set();
|
|
2661
|
+
if (useInteractive) {
|
|
2662
|
+
const options = statuses.map(({ agent, status }) => {
|
|
2663
|
+
const inst = status.installed ? '🟢 설치됨' : '⚪ 미설치';
|
|
2664
|
+
const desc = `${inst} · ${agent.desc.slice(0, 50)}`;
|
|
2665
|
+
return { id: agent.id, label: agent.id.padEnd(8), description: desc };
|
|
2666
|
+
});
|
|
2667
|
+
// 기본 선택: 이미 활성화된 것 + claude (기본 활성)
|
|
2668
|
+
const defaults = statuses
|
|
2669
|
+
.map((s, i) => (s.status.enabled || s.agent.id === 'claude') ? i : -1)
|
|
2670
|
+
.filter(i => i >= 0);
|
|
2671
|
+
const picked = await _selectMany(
|
|
2672
|
+
'활성화할 sub-agent CLI를 선택하세요 (Space=토글, a=전체, n=해제, Enter=확정)',
|
|
2673
|
+
options,
|
|
2674
|
+
{ defaults }
|
|
2675
|
+
);
|
|
2676
|
+
toEnable = new Set(picked.map(p => p.id));
|
|
2677
|
+
} else {
|
|
2678
|
+
// 폴백: 기존 yes/no
|
|
2679
|
+
for (const { agent, status } of statuses) {
|
|
2680
|
+
const isReady = status.installed && status.enabled;
|
|
2681
|
+
log(`▸ ${agent.id} — ${agent.desc}`);
|
|
2682
|
+
log(` ${status.installed ? '🟢 설치됨' : '⚪ 미설치'} / ${status.enabled ? '🟢 활성' : '🟡 비활성'}`);
|
|
2683
|
+
const wantEnable = await _confirm(` ${agent.id}를 sub-agent로 활성화?`, isReady || agent.id === 'claude');
|
|
2684
|
+
if (wantEnable) toEnable.add(agent.id);
|
|
2448
2685
|
}
|
|
2449
|
-
|
|
2450
|
-
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
// 선택 결과 적용
|
|
2689
|
+
for (const { agent, status } of statuses) {
|
|
2690
|
+
const enable = toEnable.has(agent.id);
|
|
2691
|
+
_upsertEnvLine(envPath, agent.envFlag, enable ? '1' : '0');
|
|
2692
|
+
log(enable ? ` ✓ ${agent.envFlag}=1 (활성)` : ` ✗ ${agent.envFlag}=0 (비활성)`);
|
|
2451
2693
|
|
|
2452
|
-
|
|
2694
|
+
// 활성화했지만 미설치 → 자동 설치 prompt
|
|
2695
|
+
if (enable && !status.installed) {
|
|
2453
2696
|
log(` ⚠ ${agent.bin}이(가) 설치되어 있지 않습니다.`);
|
|
2454
2697
|
log(` 설치 명령: ${agent.installCmd}`);
|
|
2455
|
-
log(` 상세 안내: ${agent.installHint}`);
|
|
2456
2698
|
const doInstall = await _confirm(` 지금 자동 설치를 시도할까요?`, false);
|
|
2457
2699
|
if (doInstall) {
|
|
2458
2700
|
const r = _tryInstallAgent(agent);
|
|
2459
2701
|
if (r.ok) {
|
|
2460
|
-
log(` ✓ 설치 성공 — 재확인: ${agent.bin} ${agent.versionArgs.join(' ')}`);
|
|
2461
2702
|
const after = _checkAgent(agent);
|
|
2462
2703
|
if (after.installed) log(` 🟢 ${agent.id} 설치 확인 (${after.version || '?'})`);
|
|
2463
2704
|
else log(` ⚠ 설치 후에도 PATH에서 찾지 못함 — 새 셸을 열어주세요`);
|
|
2464
2705
|
} else {
|
|
2465
2706
|
log(` ✗ 설치 실패: ${r.message}`);
|
|
2466
|
-
log(` 수동 설치 후 \`leerness agents list\`로 재확인하세요.`);
|
|
2467
2707
|
}
|
|
2468
2708
|
} else {
|
|
2469
2709
|
log(` → 나중에 직접 설치 후 \`leerness setup-agents\` 재실행 가능`);
|
|
@@ -4812,7 +5052,7 @@ function viewworkInstall(root) {
|
|
|
4812
5052
|
}
|
|
4813
5053
|
|
|
4814
5054
|
function help() {
|
|
4815
|
-
log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path] [--all-apps] [--include p1,p2] [--since 24h|3d] [--compact] [--json] # 1.9.17-22 워크스페이스 (--compact: LLM 시스템 프롬프트용 1줄 요약)\n leerness orchestrate "<목표>" [--agents N] [--model qwen2.5:7b-instruct] [--retry-on-fail K] # 1.9.22 Ollama opt-in (LEERNESS_OLLAMA_BASE_URL 필요)\n leerness llm-bench record --score N --model X [--label L] [--tokens T] # 1.9.22 LLM 벤치 히스토리 누적\n leerness deps <capability> [--run-tests] [--json] # 1.9.24 depends-on 역방향 추적 + 자동 회귀 sweep\n leerness memory search "키" [--include-code] # 1.9.25 소스 코드 본문도 검색 (모순 감지 핵심)\n leerness brainstorm "주제" [--include-code] # 1.9.25 코드 본문 hits 포함\n leerness register-pending "<요청>" [--agent X] [--note Y] # 1.9.25 다중 세션 in-progress 즉시 등록\n leerness optimism-check <T-ID> [--json] # 1.9.26/27 낙관적 표시 감지 (1.9.27: 10 카테고리 + URL/메서드 매핑 + 신뢰도 점수)\n leerness persona list|show <id>|add <id> # 1.9.29 페르소나 카탈로그 (보안/성능/UX/testing/docs 5종 내장)\n leerness review <file> --persona <id1,id2,...> # 1.9.29 도메인 페르소나 리뷰 프롬프트 자동 생성\n leerness agents list|check|quota # 1.9.30/31 외부 AI CLI 가용성 + quota 추정 (claude/codex/gemini/copilot)\n leerness agents dispatch "<task>" --to <id> # 1.9.30 활성 CLI 대상 실행 명령 생성 (실 호출 X, 사용자 실행)\n leerness setup-agents [path] [--yes|--no-setup-agents] # 1.9.32 sub-agent CLI 인터랙티브 설정 (.env + 미설치 자동 설치)\n leerness verify-claim <T-ID> ... [--strict-claims] # 1.9.26 verify-claim에 낙관적 표시 자동 검사 통합\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--run-tests] [--json] # 1.9.18-20 evidence 자동 검증 (1.9.20: scenes/scripts 등 도메인 폴더 + jest/mocha 파싱)\n leerness verify-code [path] [--build] [--bench] # 1.9.20 --bench: scripts.bench 추가 실행 + evidence 누적\n leerness session close [path]\n leerness viewwork install [path]\n leerness viewwork emit [path] [--action a] [--note n] [--agent x] [--tool t]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop|fix-evidence|relink [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy
|
|
5055
|
+
log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path] [--all-apps] [--include p1,p2] [--since 24h|3d] [--compact] [--json] # 1.9.17-22 워크스페이스 (--compact: LLM 시스템 프롬프트용 1줄 요약)\n leerness orchestrate "<목표>" [--agents N] [--model qwen2.5:7b-instruct] [--retry-on-fail K] # 1.9.22 Ollama opt-in (LEERNESS_OLLAMA_BASE_URL 필요)\n leerness llm-bench record --score N --model X [--label L] [--tokens T] # 1.9.22 LLM 벤치 히스토리 누적\n leerness deps <capability> [--run-tests] [--json] # 1.9.24 depends-on 역방향 추적 + 자동 회귀 sweep\n leerness memory search "키" [--include-code] # 1.9.25 소스 코드 본문도 검색 (모순 감지 핵심)\n leerness brainstorm "주제" [--include-code] # 1.9.25 코드 본문 hits 포함\n leerness register-pending "<요청>" [--agent X] [--note Y] # 1.9.25 다중 세션 in-progress 즉시 등록\n leerness optimism-check <T-ID> [--json] # 1.9.26/27 낙관적 표시 감지 (1.9.27: 10 카테고리 + URL/메서드 매핑 + 신뢰도 점수)\n leerness persona list|show <id>|add <id> # 1.9.29 페르소나 카탈로그 (보안/성능/UX/testing/docs 5종 내장)\n leerness review <file> --persona <id1,id2,...> # 1.9.29 도메인 페르소나 리뷰 프롬프트 자동 생성\n leerness agents list|check|quota # 1.9.30/31 외부 AI CLI 가용성 + quota 추정 (claude/codex/gemini/copilot)\n leerness agents dispatch "<task>" --to <id> # 1.9.30 활성 CLI 대상 실행 명령 생성 (실 호출 X, 사용자 실행)\n leerness setup-agents [path] [--yes|--no-setup-agents] # 1.9.32 sub-agent CLI 인터랙티브 설정 (.env + 미설치 자동 설치)\n leerness init [path] [--no-stale-check] # 1.9.33 npx 캐시 함정 — 옛 버전 자동 경고 (끄려면 --no-stale-check)\n leerness verify-claim <T-ID> ... [--strict-claims] # 1.9.26 verify-claim에 낙관적 표시 자동 검사 통합\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--run-tests] [--json] # 1.9.18-20 evidence 자동 검증 (1.9.20: scenes/scripts 등 도메인 폴더 + jest/mocha 파싱)\n leerness verify-code [path] [--build] [--bench] # 1.9.20 --bench: scripts.bench 추가 실행 + evidence 누적\n leerness session close [path]\n leerness viewwork install [path]\n leerness viewwork emit [path] [--action a] [--note n] [--agent x] [--tool t]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop|fix-evidence|relink [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy
|
|
4816
5056
|
leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
|
|
4817
5057
|
leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
|
|
4818
5058
|
leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -898,7 +898,7 @@ total++;
|
|
|
898
898
|
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--language', 'ko', '--skills', 'recommended', '--no-setup-agents'], { stdio: 'ignore', timeout: 30000 });
|
|
899
899
|
const r = cp.spawnSync(process.execPath, [CLI, 'setup-agents', tmpC, '--yes'], { encoding: 'utf8', timeout: 15000 });
|
|
900
900
|
const ok = r.status === 0
|
|
901
|
-
&& /외부 AI CLI 설정 \(1\.9\.
|
|
901
|
+
&& /외부 AI CLI 설정 \(1\.9\.3\d\)/.test(r.stdout)
|
|
902
902
|
&& /(비대화형|leerness agents list)/.test(r.stdout);
|
|
903
903
|
console.log(ok ? '✓ B(1.9.32) setup-agents 비대화형: 안내만 출력 (.env 미변경)' : `✗ setup-agents 실패`);
|
|
904
904
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
@@ -931,6 +931,84 @@ total++;
|
|
|
931
931
|
if (!ok) { failed++; console.log(after); }
|
|
932
932
|
}
|
|
933
933
|
|
|
934
|
+
// 1.9.33 회귀: npx 캐시 함정 — stale 버전 실행 시 경고
|
|
935
|
+
total++;
|
|
936
|
+
{
|
|
937
|
+
// 캐시에 미래 버전을 심어 stale 시뮬레이션 → 경고 출력 + init은 계속 진행
|
|
938
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-stale-'));
|
|
939
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--language', 'ko', '--skills', 'recommended', '--no-stale-check'], { stdio: 'ignore', timeout: 30000 });
|
|
940
|
+
const cacheDir = path.join(tmpC, '.harness', 'cache');
|
|
941
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
942
|
+
fs.writeFileSync(path.join(cacheDir, 'update-check.json'), JSON.stringify({ at: Date.now(), nextLeerness: '99.99.99', runningCli: require('../package.json').version }), 'utf8');
|
|
943
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
|
|
944
|
+
const ok = r.status === 0
|
|
945
|
+
&& /옛 버전이 실행 중입니다/.test(r.stdout)
|
|
946
|
+
&& /v99\.99\.99/.test(r.stdout)
|
|
947
|
+
&& /clear-npx-cache/.test(r.stdout)
|
|
948
|
+
&& /Leerness v/.test(r.stdout); // init도 계속 진행
|
|
949
|
+
console.log(ok ? '✓ B(1.9.33) npx stale 경고: 미래 latest 캐시 시 경고 + init 계속' : `✗ stale 경고 실패`);
|
|
950
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 800)); }
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// 1.9.34 회귀: 인터랙티브 multi-select (방향키/스페이스) — 비-TTY 폴백
|
|
954
|
+
total++;
|
|
955
|
+
{
|
|
956
|
+
// 비-TTY에서는 _selectOne/_selectMany가 defaults 또는 첫 옵션 반환
|
|
957
|
+
// → init이 --yes 없이도 비대화형이면 기본 스킬셋(recommended)로 진행
|
|
958
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-multi-'));
|
|
959
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
|
|
960
|
+
// 비-TTY + --yes 시 multi-select prompt 안 띄움 → 통상 init 흐름
|
|
961
|
+
const ok = r.status === 0
|
|
962
|
+
&& /Leerness v/.test(r.stdout)
|
|
963
|
+
&& /Skills: office/.test(r.stdout);
|
|
964
|
+
console.log(ok ? '✓ B(1.9.34) multi-select 비-TTY 폴백: --yes로 default 사용' : `✗ multi-select 폴백 실패`);
|
|
965
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
total++;
|
|
969
|
+
{
|
|
970
|
+
// 1.9.34 배너 256색 그라데이션 — TTY 강제 + --banner
|
|
971
|
+
// 비-TTY일 때는 ANSI 코드 없이 순수 텍스트만 출력
|
|
972
|
+
const r = cp.spawnSync(process.execPath, [CLI, '--version', '--banner'], { encoding: 'utf8', timeout: 10000 });
|
|
973
|
+
const ok = r.status === 0
|
|
974
|
+
&& /███████╗/.test(r.stdout)
|
|
975
|
+
&& /verify · reuse-map/.test(r.stdout)
|
|
976
|
+
&& /한국어 우선 AI 개발 하네스/.test(r.stdout)
|
|
977
|
+
&& /v1\.9\.34/.test(r.stdout);
|
|
978
|
+
console.log(ok ? '✓ B(1.9.34) 배너 색상 + ASCII + 한국어' : `✗ 배너 색상 실패`);
|
|
979
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
total++;
|
|
983
|
+
{
|
|
984
|
+
// 1.9.34 LEERNESS_NO_INTERACTIVE=1: 구식 숫자 prompt 사용 (TTY 일 때만 의미 있음; 비-TTY는 어차피 --yes 폴백)
|
|
985
|
+
// 검증: --no-interactive-select 플래그가 인식되고 에러 안 남
|
|
986
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-noint-'));
|
|
987
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--no-interactive-select', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
|
|
988
|
+
const ok = r.status === 0 && /Leerness v/.test(r.stdout);
|
|
989
|
+
console.log(ok ? '✓ B(1.9.34) --no-interactive-select 플래그 인식' : `✗ --no-interactive-select 실패`);
|
|
990
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 300)); }
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
total++;
|
|
994
|
+
{
|
|
995
|
+
// --no-stale-check / LEERNESS_NO_STALE_CHECK=1: 경고 스킵
|
|
996
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-stale2-'));
|
|
997
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--language', 'ko', '--skills', 'recommended', '--no-stale-check'], { stdio: 'ignore', timeout: 30000 });
|
|
998
|
+
const cacheDir = path.join(tmpC, '.harness', 'cache');
|
|
999
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
1000
|
+
fs.writeFileSync(path.join(cacheDir, 'update-check.json'), JSON.stringify({ at: Date.now(), nextLeerness: '99.99.99' }), 'utf8');
|
|
1001
|
+
// --no-stale-check
|
|
1002
|
+
const r1 = cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
|
|
1003
|
+
// env flag
|
|
1004
|
+
const r2 = cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000, env: { ...process.env, LEERNESS_NO_STALE_CHECK: '1' } });
|
|
1005
|
+
const ok = r1.status === 0 && r2.status === 0
|
|
1006
|
+
&& !/옛 버전이 실행 중입니다/.test(r1.stdout)
|
|
1007
|
+
&& !/옛 버전이 실행 중입니다/.test(r2.stdout);
|
|
1008
|
+
console.log(ok ? '✓ B(1.9.33) stale 스킵: --no-stale-check + LEERNESS_NO_STALE_CHECK=1' : `✗ stale skip 실패`);
|
|
1009
|
+
if (!ok) { failed++; console.log(r1.stdout.slice(0, 400)); }
|
|
1010
|
+
}
|
|
1011
|
+
|
|
934
1012
|
// 1.9.22 회귀: handoff --compact + orchestrate opt-in 정책 + llm-bench record
|
|
935
1013
|
total++;
|
|
936
1014
|
{
|