leerness 1.9.268 → 1.9.269

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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.269 — 2026-06-02 — UR-0022: init 시 OS 시스템 언어 감지 → 설치 가이드 자동 언어 선택
4
+
5
+ **🌐 `npx leerness init` 등 설치 시 OS 시스템 언어를 감지해 설치 가이드/생성 문서를 알맞은 언어로 표시 (사용자 명시 UR-0022).**
6
+
7
+ ### 배경
8
+ 기존 `detectLanguageValue(root, 'auto')`는 프로젝트 파일(README/guideline 등)의 **한글 포함 여부만** 판별했음. 그 결과 빈 디렉토리에서 신규 `npx leerness init` 시 콘텐츠가 없어 한국어 OS 에서도 항상 영어(en)로 폴백됐음. 사용자 요청은 OS locale 기반으로 설치 언어를 자동 결정하는 것.
9
+
10
+ ### 구현
11
+ 1. **`_detectSystemLang(env)` 순수 함수 신설** — OS locale 감지. 우선순위: POSIX 환경변수(`LC_ALL`>`LC_CTYPE`>`LANG`>`LANGUAGE`) > Node ICU `Intl.DateTimeFormat().resolvedOptions().locale`(Windows 등 LANG 미설정 시 OS 언어 회수) > `null`. `C`/`POSIX` 로케일은 스킵. env 주입 가능(테스트).
12
+ 2. **`detectLanguageValue` auto 우선순위 개선** — ① 프로젝트 콘텐츠 한글 → ② **콘텐츠가 비어있을 때만**(신규/빈 디렉토리 init) OS 시스템 언어 → ③ en 폴백. **콘텐츠가 있는 영어 프로젝트는 en 유지**(기존 프로젝트 회귀 방지).
13
+ 3. **비대화형 설치 투명성 안내** — `npx leerness init --yes`(auto) 시 `🌐 시스템 언어 감지: KO (→ KO)` 한 줄 노출. `--language` 명시 시 미발화.
14
+ 4. **언어 선택 UI 설명 갱신** — `install.lang.auto.desc` 에 "시스템(OS) 언어 자동 판별" 반영.
15
+ 5. **selftest 20 → 22** — `_detectSystemLang` POSIX 파싱(ko_KR/en_US) + LC_ALL 우선/LANGUAGE 폴백 2종.
16
+ 6. **e2e +1 (217 → 218)** — 빈 디렉토리 init auto + `LANG=en_US`/`ko_KR` → `.harness/LANGUAGE` 가 각각 en/ko 로 결정되는지 통합 검증.
17
+
18
+ ### 검증
19
+ - **selftest 22/22 PASS** · **E2E 218/218 PASS** (회귀 0).
20
+ - 기존 init e2e 는 모두 `--language ko` 명시 → auto 변경 영향 없음(회귀 0 확인).
21
+
3
22
  ## 1.9.268 — 2026-06-02 — grok 정식 EXTERNAL_AGENTS provider 승격 (1.9.266 후속 task)
4
23
 
5
24
  **🤖 grok 을 슬래시 레지스트리 전용에서 정식 provider 로 승격 — provider cycle / setup-agents / dispatch / `--help` probe 가 grok 도 자동 처리.**
package/bin/harness.js CHANGED
@@ -7,7 +7,7 @@ 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
9
 
10
- const VERSION = '1.9.268';
10
+ const VERSION = '1.9.269';
11
11
 
12
12
  // 1.9.184: DEP0190 (child_process shell: true) deprecation warning 억제 (사용자 명시).
13
13
  // leerness 는 cross-platform PATH resolution 을 위해 shell: true 를 의도적으로 사용 (claude.cmd / ollama.cmd 등 Windows .cmd 처리).
@@ -225,13 +225,41 @@ function argAll(name) {
225
225
  return out;
226
226
  }
227
227
  function detectProjectName(root) { try { const pkg = JSON.parse(read(path.join(root, 'package.json'))); if (pkg.name) return pkg.name; } catch {} return path.basename(root); }
228
+ // 1.9.269 (UR-0022): OS 시스템 언어 감지 — 신규 init(빈 디렉토리, 프로젝트 콘텐츠 없음)에서 OS locale 로 설치 언어 판별.
229
+ // 우선순위: POSIX 환경변수(LC_ALL>LC_CTYPE>LANG>LANGUAGE) > Intl ICU resolvedOptions().locale(Windows 등 LANG 미설정 시) > null.
230
+ // 순수 함수(부작용 0) — env 주입 가능(selftest). 판별 불가 시 null 반환 → 호출부가 en 폴백.
231
+ function _detectSystemLang(env) {
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
+ }
228
247
  function detectLanguageValue(root, value = 'auto') {
229
248
  const v = String(value || 'auto').toLowerCase();
230
249
  if (v === 'ko' || v === 'en') return v;
250
+ // ① 프로젝트 콘텐츠 한글 우선 — 콘텐츠가 있으면 그 의도를 존중 (기존 동작 보존).
231
251
  const candidates = ['README.md', 'docs/guideline.md', '.harness/project-brief.md', '.harness/plan.md'];
232
252
  let text = '';
233
253
  for (const c of candidates) { const p = path.join(root, c); if (exists(p)) text += read(p).slice(0, 3000); }
234
- return /[가-힣]/.test(text) ? 'ko' : 'en';
254
+ if (/[가-힣]/.test(text)) return 'ko';
255
+ // 1.9.269 (UR-0022): ② 콘텐츠가 비어있으면(신규/빈 디렉토리 init) OS 시스템 언어로 판별.
256
+ // 콘텐츠가 있는데 한글이 아니면(영어 프로젝트) en 유지 — 기존 프로젝트 회귀 방지.
257
+ if (text.trim().length === 0) {
258
+ const sys = _detectSystemLang();
259
+ if (sys) return sys;
260
+ }
261
+ // ③ en 폴백
262
+ return 'en';
235
263
  }
236
264
  function fm(role, readWhen, updateWhen, body) {
237
265
  return `---\nleernessRole: ${role}\nreadWhen:\n${readWhen.map(x => ' - ' + x).join('\n')}\nupdateWhen:\n${updateWhen.map(x => ' - ' + x).join('\n')}\ndoNotStore:\n - 실제 토큰\n - 비밀번호\n - 운영 쿠키\n - 민감한 개인정보 원문\n---\n${MARK}\n${body}`;
@@ -792,6 +820,11 @@ async function resolveInstallOptions(root, opts = {}) {
792
820
  let lang = explicitLang ? detectLanguageValue(root, explicitLang) : detectLanguageValue(root, 'auto');
793
821
  let skills = explicitSkills ? parseSkillsValue(explicitSkills) : [];
794
822
  const shouldAsk = !has('--yes') && !opts.nonInteractive && process.stdin.isTTY && process.stdout.isTTY && !opts.migration;
823
+ // 1.9.269 (UR-0022): 비대화형 auto 설치(npx leerness init --yes 등)에서 OS 시스템 언어가 적용됐으면 투명성 안내.
824
+ // --language 명시 시 미발화 (explicitLang). 대화형은 이후 언어 prompt 가 처리.
825
+ if (!shouldAsk && !explicitLang) {
826
+ try { const sysLang = _detectSystemLang(); if (sysLang) log(`🌐 ${_t('install.lang.sysNotice', lang)}: ${sysLang.toUpperCase()} (→ ${lang.toUpperCase()})`); } catch {}
827
+ }
795
828
  // 1.9.34: 인터랙티브 multi-select (방향키 + Space + Enter) — 기존 숫자 선택 폴백 유지
796
829
  // --no-interactive-select 또는 LEERNESS_NO_INTERACTIVE=1 → 구식 숫자 선택
797
830
  const useInteractive = shouldAsk && !has('--no-interactive-select') && process.env.LEERNESS_NO_INTERACTIVE !== '1';
@@ -1852,7 +1885,8 @@ const STRINGS = {
1852
1885
  // 설치 가이드 prompt
1853
1886
  'install.lang.title': { ko: '설치 언어를 선택하세요', en: 'Select install language' },
1854
1887
  'install.lang.auto': { ko: '자동 감지', en: 'Auto detect' },
1855
- 'install.lang.auto.desc': { ko: '디렉토리/파일 분석 (한국어/영어 자동 판별)', en: 'Analyze dir/files (auto KO/EN)' },
1888
+ 'install.lang.auto.desc': { ko: '디렉토리/파일 + 시스템(OS) 언어 자동 판별 (한국어/영어)', en: 'Auto-detect from dir/files + system (OS) locale (KO/EN)' },
1889
+ 'install.lang.sysNotice': { ko: '시스템 언어 감지', en: 'System language detected' },
1856
1890
  'install.lang.ko': { ko: '한국어', en: 'Korean' },
1857
1891
  'install.lang.ko.desc': { ko: '모든 인스트럭션을 한국어로 생성', en: 'All instructions in Korean' },
1858
1892
  'install.lang.en': { ko: 'English', en: 'English' },
@@ -2912,6 +2946,8 @@ function _selfTestCases() {
2912
2946
  { name: '_parseSlashFromHelp: 슬래시 검출 + 플래그 제외 (1.9.267)', run: () => { const r = _parseSlashFromHelp(' /help show help\n /model switch model\n --version print version\n', 'slash'); return r.length === 2 && r[0].cmd === '/help' && /show help/.test(r[0].desc) && !r.some(c => /version/.test(c.cmd)); } },
2913
2947
  { name: '_parseSlashFromHelp: subcommand 모드 들여쓰기 파싱 (1.9.267)', run: () => { const r = _parseSlashFromHelp('Commands:\n suggest 제안\n explain 설명\n', 'subcommand'); return r.length >= 2 && r.some(c => c.cmd === 'suggest') && r.some(c => c.cmd === 'explain'); } },
2914
2948
  { name: 'EXTERNAL_AGENTS: grok 정식 provider 승격 (1.9.268)', run: () => { const g = EXTERNAL_AGENTS.find(a => a.id === 'grok'); return !!g && g.bin === 'grok' && g.envFlag === 'LEERNESS_ENABLE_GROK' && EXTERNAL_AGENTS.length === 6; } },
2949
+ { name: '_detectSystemLang: POSIX LANG ko_KR/en_US 판별 (1.9.269)', run: () => _detectSystemLang({ LANG: 'ko_KR.UTF-8' }) === 'ko' && _detectSystemLang({ LANG: 'en_US.UTF-8' }) === 'en' },
2950
+ { name: '_detectSystemLang: LC_ALL 우선 + LANGUAGE 폴백 (1.9.269)', run: () => _detectSystemLang({ LC_ALL: 'ko_KR.UTF-8', LANG: 'en_US.UTF-8' }) === 'ko' && _detectSystemLang({ LANGUAGE: 'en_GB' }) === 'en' },
2915
2951
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
2916
2952
  ];
2917
2953
  }
@@ -20736,5 +20772,7 @@ module.exports = {
20736
20772
  _shellFailuresPath, _loadShellFailures, _recordShellFailure, _shellEnvDrift,
20737
20773
  // 1.9.265: CLI 에이전트 슬래시 명령어 레지스트리 (UR-0021 1단계) — 단위 테스트
20738
20774
  AGENT_SLASH_COMMANDS, _agentSlashFile, _loadAgentSlashCommands, _recordAgentSlashCommands, _agentSlashHint, slashCommandsCmd,
20739
- _parseSlashFromHelp, _probeAgentSlash, _refreshAgentSlashCommands
20775
+ _parseSlashFromHelp, _probeAgentSlash, _refreshAgentSlashCommands,
20776
+ // 1.9.269: 시스템(OS) 언어 감지 (UR-0022) + 언어 판별 — 단위 테스트
20777
+ _detectSystemLang, detectLanguageValue
20740
20778
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.268",
3
+ "version": "1.9.269",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -2704,5 +2704,23 @@ run('consistency check', ['consistency', 'check', tmp]);
2704
2704
  run('--version', ['--version']);
2705
2705
  run('--help', ['--help']);
2706
2706
 
2707
+ // 1.9.269 회귀 (UR-0022): 빈 디렉토리 신규 init + auto → OS 시스템 언어로 .harness/LANGUAGE 결정
2708
+ total++;
2709
+ {
2710
+ const tmpEn = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-lang-en-'));
2711
+ const tmpKo = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-lang-ko-'));
2712
+ // LANG 명시 (POSIX) — Windows 에서도 _detectSystemLang 이 LANG 우선 읽음
2713
+ const envEn = { ...process.env, LANG: 'en_US.UTF-8', LC_ALL: 'en_US.UTF-8', LANGUAGE: '' };
2714
+ const envKo = { ...process.env, LANG: 'ko_KR.UTF-8', LC_ALL: 'ko_KR.UTF-8', LANGUAGE: '' };
2715
+ // --language 미지정(auto) + 빈 디렉토리 → 시스템 언어 적용
2716
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpEn, '--yes', '--skills', 'recommended'], { encoding: 'utf8', env: envEn, timeout: 30000 });
2717
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpKo, '--yes', '--skills', 'recommended'], { encoding: 'utf8', env: envKo, timeout: 30000 });
2718
+ const langEn = fs.existsSync(path.join(tmpEn, '.harness', 'LANGUAGE')) ? fs.readFileSync(path.join(tmpEn, '.harness', 'LANGUAGE'), 'utf8').trim() : '';
2719
+ const langKo = fs.existsSync(path.join(tmpKo, '.harness', 'LANGUAGE')) ? fs.readFileSync(path.join(tmpKo, '.harness', 'LANGUAGE'), 'utf8').trim() : '';
2720
+ const ok = langEn === 'en' && langKo === 'ko';
2721
+ console.log(ok ? '✓ B(1.9.269) init auto: 빈 디렉토리 → OS 시스템 언어 (en→en, ko→ko)' : `✗ system-lang init 실패 (en=${langEn} ko=${langKo})`);
2722
+ if (!ok) failed++;
2723
+ }
2724
+
2707
2725
  console.log(`\nE2E result: ${total - failed}/${total} passed`);
2708
2726
  if (failed > 0) process.exit(1);