leerness 1.7.0 → 1.8.0
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/README.md +43 -90
- package/bin/harness.js +149 -21
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
# Leerness
|
|
2
2
|
|
|
3
|
-
Leerness는 AI 에이전트가 프로젝트의
|
|
3
|
+
Leerness는 AI 에이전트가 프로젝트의 목적, 계획, 진행 상태, 설계 결정, 디자인/기능 일관성, 검증된 스킬을 지속적으로 참조하며 개발하도록 돕는 AX 최적화 개발 하네스입니다.
|
|
4
4
|
|
|
5
5
|
## 주요 기능
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- `progress-tracker.md` 기반
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
- AI 검증 스킬 업로드를 위한 토큰 게이트와 dry-run 우선 정책
|
|
7
|
+
- `.harness/` 기반 장기 맥락 유지
|
|
8
|
+
- `plan.md`와 `progress-tracker.md` 기반 계획/작업 추적
|
|
9
|
+
- 언어 자동 감지 또는 선택 설치
|
|
10
|
+
- 스킬 라이브러리 선택 설치와 한글 스킬 정보 표시
|
|
11
|
+
- AI 검증 스킬의 라이브러리화, 병합, 마이그레이션, 배포 흐름 지원
|
|
12
|
+
- 기존 하네스/스킬/지침 파일 비파괴 마이그레이션
|
|
13
|
+
- `.env.example`, `.gitignore`, README managed section 병합 업데이트
|
|
14
|
+
- 디자인 가이드 병합, 재사용 맵, 일관성 정책 제공
|
|
15
|
+
- 세션 종료 인수인계와 미완료 작업 추적
|
|
17
16
|
|
|
18
17
|
## 설치
|
|
19
18
|
|
|
@@ -21,112 +20,66 @@ Leerness는 AI 에이전트가 프로젝트의 목표, 계획, 작업 상태,
|
|
|
21
20
|
npx leerness init
|
|
22
21
|
```
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
언어와 스킬을 명시하려면:
|
|
25
24
|
|
|
26
25
|
```bash
|
|
27
|
-
npx leerness init --language ko
|
|
28
|
-
npx leerness init --language en
|
|
29
|
-
npx leerness init --language auto
|
|
26
|
+
npx leerness init --language ko --skills recommended
|
|
27
|
+
npx leerness init --language en --skills office,commerce-api
|
|
30
28
|
```
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
기존 프로젝트 마이그레이션:
|
|
33
31
|
|
|
34
32
|
```bash
|
|
35
|
-
npx leerness
|
|
33
|
+
npx leerness migrate --dry-run
|
|
34
|
+
npx leerness migrate
|
|
36
35
|
```
|
|
37
36
|
|
|
38
|
-
##
|
|
37
|
+
## 설치 시 선택 흐름
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
leerness route planning
|
|
45
|
-
leerness route consistency
|
|
46
|
-
leerness session close .
|
|
47
|
-
```
|
|
39
|
+
대화형 터미널에서 `--yes`, `--language`, `--skills`를 생략하면 Leerness가 설치 언어와 스킬 라이브러리 설치 여부를 묻습니다.
|
|
40
|
+
|
|
41
|
+
- 언어: 자동 감지, 한국어, English
|
|
42
|
+
- 스킬: 기본 하네스만, 추천 스킬, 전체 스킬, 직접 입력
|
|
48
43
|
|
|
49
|
-
|
|
44
|
+
CI 또는 자동화 환경에서는 프롬프트 없이 명시 옵션을 사용하세요.
|
|
50
45
|
|
|
51
46
|
```bash
|
|
52
|
-
leerness
|
|
53
|
-
leerness plan init --goal "프로젝트 목적"
|
|
54
|
-
leerness plan add "결제 기능 구현" --status planned
|
|
55
|
-
leerness plan drop "관리자 대시보드" --reason "이번 버전 범위 제외"
|
|
56
|
-
leerness plan progress
|
|
57
|
-
leerness plan sync
|
|
47
|
+
npx leerness init --yes --language ko --skills recommended
|
|
58
48
|
```
|
|
59
49
|
|
|
60
|
-
|
|
50
|
+
## 비파괴 마이그레이션 정책
|
|
61
51
|
|
|
62
|
-
|
|
63
|
-
leerness task list
|
|
64
|
-
leerness task add "사용자 요청" --status requested
|
|
65
|
-
leerness task update T-0001 --status in-progress
|
|
66
|
-
leerness task drop T-0001 --reason "사용자가 제외"
|
|
67
|
-
```
|
|
52
|
+
Leerness는 마이그레이션 전에 기존 하네스, 스킬, 지침, README, 환경 예시 파일을 `.harness/archive/`에 백업합니다.
|
|
68
53
|
|
|
69
|
-
|
|
54
|
+
덮어쓰기 위험이 있는 파일은 다음 정책을 따릅니다.
|
|
70
55
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
56
|
+
- 프로젝트 메모리 파일은 기본 보존
|
|
57
|
+
- `AGENTS.md`, `CLAUDE.md`, Copilot/Cursor 지침은 새 템플릿과 기존 내용을 병합
|
|
58
|
+
- `.env.example`, `.gitignore`는 기존 내용을 유지하고 필요한 줄만 추가
|
|
59
|
+
- README는 Leerness managed marker 구간만 갱신
|
|
60
|
+
- 작업 결과는 `.harness/migration-report.md`에 기록
|
|
74
61
|
|
|
75
|
-
|
|
62
|
+
## 주요 명령어
|
|
76
63
|
|
|
77
64
|
```bash
|
|
78
|
-
leerness
|
|
79
|
-
leerness
|
|
80
|
-
|
|
65
|
+
leerness status .
|
|
66
|
+
leerness verify .
|
|
67
|
+
leerness debug .
|
|
68
|
+
leerness route <task-type>
|
|
69
|
+
leerness session close .
|
|
81
70
|
|
|
82
|
-
|
|
71
|
+
leerness plan show
|
|
72
|
+
leerness plan add "작업명" --status planned
|
|
73
|
+
leerness task list
|
|
74
|
+
leerness task drop T-0001 --reason "사용자 요청으로 제외"
|
|
83
75
|
|
|
84
|
-
```bash
|
|
85
76
|
leerness skill list
|
|
86
77
|
leerness skill info commerce-api
|
|
87
|
-
leerness skill add ai-verified-skill-publisher --path .
|
|
88
|
-
```
|
|
89
78
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
leerness self check .
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## 생성 구조
|
|
97
|
-
|
|
98
|
-
```text
|
|
99
|
-
.harness/
|
|
100
|
-
plan.md
|
|
101
|
-
progress-tracker.md
|
|
102
|
-
guideline.md
|
|
103
|
-
plan-progress-boundary.md
|
|
104
|
-
project-brief.md
|
|
105
|
-
current-state.md
|
|
106
|
-
context-routing.md
|
|
107
|
-
writeback-policy.md
|
|
108
|
-
protected-files.md
|
|
109
|
-
consistency-policy.md
|
|
110
|
-
design-system.md
|
|
111
|
-
reuse-map.md
|
|
112
|
-
session-close-policy.md
|
|
113
|
-
anti-lazy-work-policy.md
|
|
114
|
-
leerness-maintenance.md
|
|
115
|
-
skills/
|
|
116
|
-
library/
|
|
117
|
-
archive/
|
|
79
|
+
leerness consistency check .
|
|
80
|
+
leerness readme sync .
|
|
118
81
|
```
|
|
119
82
|
|
|
120
|
-
## 계획과 진행률의 역할 분리
|
|
121
|
-
|
|
122
|
-
- `plan.md`: 프로젝트 목표, 범위, milestone, 제외/드랍 항목, 계획 변경 이력
|
|
123
|
-
- `progress-tracker.md`: 사용자 요청 단위 상태, 증거, 다음 액션
|
|
124
|
-
- `guideline.md`: 계획과 진행률을 어떻게 따라야 하는지에 대한 실행 기준
|
|
125
|
-
|
|
126
|
-
## 보호 정책
|
|
127
|
-
|
|
128
|
-
AI 에이전트는 `.harness/`, `AGENTS.md`, `CLAUDE.md`, `.cursor/rules/leerness.mdc`, `.github/copilot-instructions.md`, 루트 `README.md`의 Leerness 관리 섹션을 임의 삭제하거나 초기화하지 않아야 합니다. 정리가 필요하면 삭제보다 병합, 아카이브, deprecated 표시를 우선합니다.
|
|
129
|
-
|
|
130
83
|
## 라이선스
|
|
131
84
|
|
|
132
85
|
MIT
|
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
|
+
const VERSION = '1.8.0';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -221,17 +221,116 @@ function coreFiles(root, lang = 'ko', selectedSkills = []) {
|
|
|
221
221
|
'.harness/templates/decision.md': '# Decision\n\n## Decision\n\n## Reason\n\n## Alternatives\n\n## Impact\n'
|
|
222
222
|
};
|
|
223
223
|
}
|
|
224
|
-
function
|
|
225
|
-
const v = arg('--skills', '');
|
|
224
|
+
function parseSkillsValue(v) {
|
|
226
225
|
if (!v || v === true) return [];
|
|
227
226
|
if (v === 'all') return Object.keys(skillCatalog);
|
|
228
227
|
if (v === 'recommended') return ['office', 'commerce-api', 'ai-verified-skill-publisher'];
|
|
229
|
-
return String(v).split(',').map(s => s.trim()).filter(Boolean);
|
|
228
|
+
return String(v).split(',').map(s => s.trim()).filter(Boolean).filter(s => skillCatalog[s]);
|
|
229
|
+
}
|
|
230
|
+
function parseSkills() { return parseSkillsValue(arg('--skills', '')); }
|
|
231
|
+
function detectLanguageValue(root, value = 'auto') {
|
|
232
|
+
const v = String(value || 'auto').toLowerCase();
|
|
233
|
+
if (v === 'ko' || v === 'en') return v;
|
|
234
|
+
const candidates = ['README.md', 'docs/guideline.md', '.harness/project-brief.md', '.harness/plan.md'];
|
|
235
|
+
let text = '';
|
|
236
|
+
for (const c of candidates) { const p = path.join(root, c); if (exists(p)) text += read(p).slice(0, 3000); }
|
|
237
|
+
return /[가-힣]/.test(text) ? 'ko' : 'en';
|
|
238
|
+
}
|
|
239
|
+
function ask(question) {
|
|
240
|
+
return new Promise(resolve => {
|
|
241
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
242
|
+
rl.question(question, answer => { rl.close(); resolve(String(answer || '').trim()); });
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
async function resolveInstallOptions(root, opts = {}) {
|
|
246
|
+
const explicitLang = arg('--language', null);
|
|
247
|
+
const explicitSkills = arg('--skills', null);
|
|
248
|
+
let lang = explicitLang ? detectLanguageValue(root, explicitLang) : detectLanguageValue(root, 'auto');
|
|
249
|
+
let skills = explicitSkills ? parseSkillsValue(explicitSkills) : [];
|
|
250
|
+
const shouldAsk = !has('--yes') && !opts.nonInteractive && process.stdin.isTTY && process.stdout.isTTY && !opts.migration;
|
|
251
|
+
if (shouldAsk && !explicitLang) {
|
|
252
|
+
log('\n설치 언어를 선택하세요.');
|
|
253
|
+
log('1) 자동 감지');
|
|
254
|
+
log('2) 한국어');
|
|
255
|
+
log('3) English');
|
|
256
|
+
const a = await ask('선택 [1]: ');
|
|
257
|
+
lang = a === '2' ? 'ko' : a === '3' ? 'en' : detectLanguageValue(root, 'auto');
|
|
258
|
+
}
|
|
259
|
+
if (shouldAsk && !explicitSkills) {
|
|
260
|
+
log('\n설치할 스킬 라이브러리를 선택하세요.');
|
|
261
|
+
log('0) 기본 하네스만 설치');
|
|
262
|
+
log('1) 추천 스킬 설치: office, commerce-api, ai-verified-skill-publisher');
|
|
263
|
+
log('2) 전체 스킬 설치');
|
|
264
|
+
log('3) 직접 입력');
|
|
265
|
+
skillList();
|
|
266
|
+
const a = await ask('선택 [1]: ');
|
|
267
|
+
if (!a || a === '1') skills = parseSkillsValue('recommended');
|
|
268
|
+
else if (a === '2') skills = parseSkillsValue('all');
|
|
269
|
+
else if (a === '3') skills = parseSkillsValue(await ask('스킬 ID를 쉼표로 입력: '));
|
|
270
|
+
else if (a === '0') skills = [];
|
|
271
|
+
}
|
|
272
|
+
return { lang, skills };
|
|
273
|
+
}
|
|
274
|
+
function copyRecursiveSafe(src, dst) {
|
|
275
|
+
if (!exists(src)) return;
|
|
276
|
+
if (src.includes(path.sep + '.harness' + path.sep + 'archive')) return;
|
|
277
|
+
const st = fs.statSync(src);
|
|
278
|
+
if (st.isDirectory()) {
|
|
279
|
+
mkdirp(dst);
|
|
280
|
+
for (const e of fs.readdirSync(src)) {
|
|
281
|
+
if (e === 'node_modules' || e === '.git') continue;
|
|
282
|
+
copyRecursiveSafe(path.join(src, e), path.join(dst, e));
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
mkdirp(path.dirname(dst));
|
|
286
|
+
fs.copyFileSync(src, dst);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function migrationCandidates(root, files) {
|
|
290
|
+
const fixed = [
|
|
291
|
+
'AGENTS.md','CLAUDE.md','.cursor/rules/leerness.mdc','.cursor/rules/project-rules.mdc',
|
|
292
|
+
'.github/copilot-instructions.md','README.md','.env.example','.gitignore',
|
|
293
|
+
'docs/guideline.md','docs/history.md','guideline.md','history.md',
|
|
294
|
+
'AI_HARNESS.md','HARNESS.md','PROJECT_CONTEXT.md','CONTEXT.md','ARCHITECTURE.md','DECISIONS.md','CURRENT_STATE.md','TASK_LOG.md',
|
|
295
|
+
'.harness','.ai','harness'
|
|
296
|
+
];
|
|
297
|
+
const all = Array.from(new Set([...fixed, ...Object.keys(files)]));
|
|
298
|
+
return all.filter(f => exists(path.join(root, f)));
|
|
299
|
+
}
|
|
300
|
+
function createBackup(root, reason, files, dry = false) {
|
|
301
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
302
|
+
const ar = path.join(root, '.harness/archive', `leerness-${VERSION}-${stamp}`);
|
|
303
|
+
const candidates = migrationCandidates(root, files);
|
|
304
|
+
if (dry) return { archiveDir: ar, candidates };
|
|
305
|
+
mkdirp(ar);
|
|
306
|
+
const fileRoot = path.join(ar, 'files');
|
|
307
|
+
for (const f of candidates) copyRecursiveSafe(path.join(root, f), path.join(fileRoot, f === '.harness' ? '.harness-before-migration' : f));
|
|
308
|
+
write(path.join(ar, 'migration-manifest.json'), JSON.stringify({
|
|
309
|
+
version: VERSION,
|
|
310
|
+
reason,
|
|
311
|
+
createdAt: now(),
|
|
312
|
+
policy: 'backup-before-write; preserve-by-default; merge-managed-files; merge-env-and-gitignore',
|
|
313
|
+
candidates
|
|
314
|
+
}, null, 2) + '\n');
|
|
315
|
+
return { archiveDir: ar, candidates };
|
|
316
|
+
}
|
|
317
|
+
function managedMerge(file, next, previous, archiveDir) {
|
|
318
|
+
if (!previous || previous.trim() === next.trim()) return next;
|
|
319
|
+
const tag = '<!-- leerness:migration-preserved -->';
|
|
320
|
+
if (previous.includes(tag)) return next;
|
|
321
|
+
return next.trimEnd() + `\n\n---\n${tag}\n## Preserved previous content\n\nPrevious content was backed up before migration. Archive reference:\n\n\`${archiveDir ? path.relative(process.cwd(), archiveDir).replace(/\\/g, '/') : '.harness/archive'}\`\n\nThe content below is retained so AI agents can migrate useful custom instructions instead of losing them.\n\n<details>\n<summary>Previous ${file}</summary>\n\n\`\`\`md\n${previous.replace(/```/g, '\\`\\`\\`')}\n\`\`\`\n\n</details>\n`;
|
|
230
322
|
}
|
|
231
323
|
function writeIfSafe(root, file, content, opts = {}) {
|
|
232
324
|
const p = path.join(root, file);
|
|
233
|
-
|
|
234
|
-
|
|
325
|
+
const already = exists(p);
|
|
326
|
+
if (already && !opts.force && !opts.mergeManaged) return { action: 'preserved', file };
|
|
327
|
+
if (already && opts.mergeManaged && !opts.force) {
|
|
328
|
+
const prev = read(p);
|
|
329
|
+
write(p, managedMerge(file, content, prev, opts.archiveDir));
|
|
330
|
+
return { action: 'merged', file };
|
|
331
|
+
}
|
|
332
|
+
write(p, content);
|
|
333
|
+
return { action: already ? 'updated' : 'created', file };
|
|
235
334
|
}
|
|
236
335
|
function mergeLinesFile(p, lines) {
|
|
237
336
|
const current = exists(p) ? read(p) : '';
|
|
@@ -239,22 +338,54 @@ function mergeLinesFile(p, lines) {
|
|
|
239
338
|
for (const line of lines) if (!next.includes(line)) next += (next.endsWith('\n') || !next ? '' : '\n') + line + '\n';
|
|
240
339
|
write(p, next);
|
|
241
340
|
}
|
|
341
|
+
function writeMigrationReport(root, backup, actions) {
|
|
342
|
+
const p = path.join(root, '.harness/migration-report.md');
|
|
343
|
+
const rows = actions.map(a => `| ${a.file} | ${a.action} |`).join('\n');
|
|
344
|
+
write(p, `# Leerness Migration Report\n\nVersion: ${VERSION}\nDate: ${now()}\nBackup: ${rel(root, backup.archiveDir)}\n\n## Policy\n\n- Existing harness, skill, and instruction files are backed up before migration.\n- Project memory files are preserved by default.\n- Managed instruction files are merged with previous content instead of being blindly overwritten.\n- .env.example and .gitignore are line-merged only.\n\n## Backed Up Candidates\n\n${backup.candidates.map(x => '- ' + x).join('\n')}\n\n## File Actions\n\n| File | Action |\n|---|---|\n${rows}\n`);
|
|
345
|
+
}
|
|
242
346
|
function syncReadme(root) {
|
|
243
347
|
const p = path.join(root, 'README.md');
|
|
244
348
|
const existing = exists(p) ? read(p) : '';
|
|
245
349
|
write(p, mergeSection(existing, managedReadmeBlock(detectProjectName(root))));
|
|
246
350
|
ok('README.md Leerness section synced');
|
|
247
351
|
}
|
|
248
|
-
function install(root, opts = {}) {
|
|
352
|
+
async function install(root, opts = {}) {
|
|
249
353
|
root = absRoot(root); mkdirp(root);
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
354
|
+
const resolved = await resolveInstallOptions(root, opts);
|
|
355
|
+
const lang = resolved.lang;
|
|
356
|
+
const skills = resolved.skills;
|
|
357
|
+
log(`\nLeerness v${VERSION}`);
|
|
358
|
+
log(`Target: ${root}`);
|
|
359
|
+
log(`Language: ${lang}`);
|
|
360
|
+
log(`Skills: ${skills.length ? skills.join(', ') : 'none'}`);
|
|
253
361
|
const files = coreFiles(root, lang, skills);
|
|
362
|
+
const backup = createBackup(root, opts.force ? 'force' : (opts.migration ? 'migration' : 'init'), files, opts.dry);
|
|
363
|
+
if (opts.dry) {
|
|
364
|
+
log(`Backup target: ${backup.archiveDir}`);
|
|
365
|
+
log('Files that would be backed up:');
|
|
366
|
+
backup.candidates.forEach(f => log('- ' + f));
|
|
367
|
+
} else {
|
|
368
|
+
ok(`backup created: ${rel(root, backup.archiveDir)}`);
|
|
369
|
+
}
|
|
370
|
+
const managedOverwrite = new Set([
|
|
371
|
+
'AGENTS.md','CLAUDE.md','.cursor/rules/leerness.mdc','.github/copilot-instructions.md',
|
|
372
|
+
'.harness/HARNESS_VERSION','.harness/manifest.json','.harness/LANGUAGE','.harness/skills-lock.json',
|
|
373
|
+
'.harness/context-routing.md','.harness/writeback-policy.md','.harness/task-type-map.md',
|
|
374
|
+
'.harness/leerness-maintenance.md','.harness/protected-files.md','.harness/AX_MIGRATION_GUIDE.md',
|
|
375
|
+
'.harness/AX_NEW_PROJECT_GUIDE.md','.harness/AX_SKILL_LIBRARY_GUIDE.md'
|
|
376
|
+
]);
|
|
377
|
+
const actions = [];
|
|
254
378
|
for (const [f, c] of Object.entries(files)) {
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
379
|
+
const existsNow = exists(path.join(root, f));
|
|
380
|
+
const mergeManaged = managedOverwrite.has(f);
|
|
381
|
+
if (opts.dry) {
|
|
382
|
+
const action = existsNow ? (mergeManaged || opts.force ? 'merge/update from backup-aware template' : 'preserve') : 'create';
|
|
383
|
+
log(`[dry-run] ${action}: ${f}`);
|
|
384
|
+
actions.push({ file:f, action });
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const r = writeIfSafe(root, f, c, { force: opts.force, mergeManaged, archiveDir: backup.archiveDir });
|
|
388
|
+
actions.push(r);
|
|
258
389
|
ok(`${r.action}: ${r.file}`);
|
|
259
390
|
}
|
|
260
391
|
if (!opts.dry) {
|
|
@@ -262,12 +393,9 @@ function install(root, opts = {}) {
|
|
|
262
393
|
mergeLinesFile(path.join(root, '.env.example'), ['# Leerness uses environment variable names only. Do not store real secrets here.', 'LEERNESS_NPM_TOKEN=', 'LEERNESS_GITHUB_TOKEN=']);
|
|
263
394
|
syncReadme(root);
|
|
264
395
|
installSkills(root, skills);
|
|
396
|
+
writeMigrationReport(root, backup, actions);
|
|
265
397
|
}
|
|
266
398
|
}
|
|
267
|
-
function archive(root, reason) {
|
|
268
|
-
const ar = path.join(root, '.harness/archive', `leerness-${VERSION}-${new Date().toISOString().replace(/[:.]/g,'-')}`);
|
|
269
|
-
mkdirp(ar); write(path.join(ar, 'migration-manifest.json'), JSON.stringify({ version: VERSION, reason, createdAt: now() }, null, 2) + '\n');
|
|
270
|
-
}
|
|
271
399
|
function installSkills(root, skills) {
|
|
272
400
|
for (const name of skills) addSkill(root, name, true);
|
|
273
401
|
}
|
|
@@ -317,12 +445,12 @@ function mergeDesign(root) { root = absRoot(root); const canonical = path.join(r
|
|
|
317
445
|
function selfCheck(root) { let latest = 'unknown'; try { latest = cp.execSync('npm view leerness version', { encoding:'utf8', stdio:['ignore','pipe','ignore'], timeout:10000 }).trim(); } catch { latest = 'npm registry unavailable'; } const local = exists(path.join(root,'.harness/HARNESS_VERSION')) ? read(path.join(root,'.harness/HARNESS_VERSION')).trim() : 'not installed'; log(`CLI: ${VERSION}`); log(`Project: ${local}`); log(`NPM latest: ${latest}`); }
|
|
318
446
|
function publishLibrary(targetPath, target) { const execute = has('--execute'); if (!execute) { log('dry-run only. Add --execute to publish.'); return; } const envs = target === 'npm' ? ['LEERNESS_NPM_TOKEN','NPM_TOKEN','NODE_AUTH_TOKEN'] : ['LEERNESS_GIT_TOKEN','LEERNESS_GITHUB_TOKEN','GITHUB_TOKEN','GH_TOKEN']; const found = envs.find(e => process.env[e]); if (!found && has('--no-prompt')) { fail('publish token not found'); process.exitCode = 1; return; } if (!found) { fail(`publish token required. Set one of: ${envs.join(', ')}`); process.exitCode = 1; return; } ok(`token provided by ${found}; publish command would execute for ${targetPath}`); }
|
|
319
447
|
function help() { 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 status [path]\n leerness verify [path]\n leerness debug [path]\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 [args]\n leerness skill list|info|add <name>\n leerness session close [path]\n`); }
|
|
320
|
-
function main() {
|
|
448
|
+
async function main() {
|
|
321
449
|
const args = nonFlagArgs(); const cmd = args[0] || 'init';
|
|
322
450
|
if (has('--version') || has('-v')) return log(VERSION);
|
|
323
451
|
if (has('--help') || has('-h')) return help();
|
|
324
|
-
if (cmd === 'init') return install(args[1] || process.cwd(), { force:false, dry:false });
|
|
325
|
-
if (cmd === 'migrate') return install(args[1] || process.cwd(), { force:has('--force'), dry:has('--dry-run') });
|
|
452
|
+
if (cmd === 'init') return await install(args[1] || process.cwd(), { force:false, dry:false, migration:false });
|
|
453
|
+
if (cmd === 'migrate') return await install(args[1] || process.cwd(), { force:has('--force'), dry:has('--dry-run'), migration:true });
|
|
326
454
|
if (cmd === 'status') return status(args[1] || process.cwd());
|
|
327
455
|
if (cmd === 'verify') return verify(args[1] || process.cwd());
|
|
328
456
|
if (cmd === 'debug') return debug(args[1] || process.cwd());
|
|
@@ -341,4 +469,4 @@ function main() {
|
|
|
341
469
|
if (cmd === 'library' && args[1] === 'publish') return publishLibrary(args[2] || '.', arg('--target','npm'));
|
|
342
470
|
return help();
|
|
343
471
|
}
|
|
344
|
-
main();
|
|
472
|
+
main().catch(err => { fail(err && err.message ? err.message : String(err)); process.exitCode = 1; });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Leerness:
|
|
3
|
+
"version": "1.8.0",
|
|
4
|
+
"description": "Leerness: 비파괴 마이그레이션, 언어/스킬 선택 설치, 계획/작업 추적, 스킬 라이브러리, 디자인/기능 일관성을 관리하는 AX 최적화 AI 개발 하네스.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|
|
7
7
|
"ai",
|