leerness 1.0.0 → 1.2.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 CHANGED
@@ -49,7 +49,7 @@ npx leerness init --skills recommended
49
49
  필요한 스킬만 선택 설치:
50
50
 
51
51
  ```bash
52
- npx leerness init --skills office,commerce-api,crawling
52
+ npx leerness init --skills office,commerce-api,crawling,ai-verified-skill-publisher
53
53
  ```
54
54
 
55
55
  특정 경로에 설치:
@@ -66,6 +66,7 @@ leerness status [path]
66
66
  leerness verify [path]
67
67
 
68
68
  leerness skill list
69
+ leerness skill info <name>
69
70
  leerness skill add <name>
70
71
  leerness skill remove <name>
71
72
  leerness skill update <name>
@@ -76,7 +77,7 @@ leerness library status <path>
76
77
  leerness library validate <path> [--strict-ai]
77
78
  leerness library verify <path> --ai --reviewer leerness-ai
78
79
  leerness library build <path> [--out ./dist] [--package leerness-skill-name]
79
- leerness library update <path> --from <validated-new-skill-path> [--version 1.1.0]
80
+ leerness library update <path> --from <validated-new-skill-path> [--version 1.2.0]
80
81
  leerness library merge <source-library> --path <project>
81
82
  leerness library migrate <path> [--version 1.0.0]
82
83
  leerness library publish <built-library> --target npm|git [--execute]
@@ -113,6 +114,26 @@ your-project/
113
114
  └── archive/
114
115
  ```
115
116
 
117
+
118
+ ## 한글 스킬 라이브러리 표시
119
+
120
+ Leerness 1.2.0부터 `leerness skill list`는 스킬의 한글명, 가능한 작업, 최종 업데이트일, AI 검증 상태를 함께 표시합니다.
121
+
122
+ ```bash
123
+ leerness skill list
124
+ leerness skill info ai-verified-skill-publisher
125
+ ```
126
+
127
+ ## AI 검증 스킬 업로드 스킬
128
+
129
+ 검증된 스킬을 라이브러리화하고 npm/git으로 업로드하는 절차 자체도 스킬로 설치할 수 있습니다.
130
+
131
+ ```bash
132
+ leerness skill add ai-verified-skill-publisher
133
+ ```
134
+
135
+ 이 스킬은 성공한 구현 패턴 추출, 한글명/가능 작업/최종 업데이트일 기록, 민감정보 제거, AI 검증 메타데이터 생성, strict-ai 검증, dry-run 확인, 사용자 승인 후 `--execute` 업로드 흐름을 안내합니다.
136
+
116
137
  ## AX 최적화 가이드
117
138
 
118
139
  설치하면 AI 에이전트가 바로 읽을 수 있는 가이드가 생성됩니다.
@@ -146,6 +167,39 @@ leerness library publish ./my-skill/dist/my-skill --target npm --execute
146
167
 
147
168
  `library publish`는 기본값이 dry-run입니다. 실제 업로드는 `--execute`가 있을 때만 실행됩니다.
148
169
 
170
+
171
+ ## 검증된 스킬팩 업로드 인증
172
+
173
+ `leerness library publish ... --execute`는 실제 업로드 전에 access token을 확인합니다.
174
+
175
+ 우선순위는 다음과 같습니다.
176
+
177
+ 1. 명시한 환경변수: `--token-env LEERNESS_NPM_TOKEN`
178
+ 2. npm: `LEERNESS_NPM_TOKEN`, `NPM_TOKEN`, `NODE_AUTH_TOKEN`
179
+ 3. Git/GitHub: `LEERNESS_GIT_TOKEN`, `LEERNESS_GITHUB_TOKEN`, `GITHUB_TOKEN`, `GH_TOKEN`
180
+ 4. 프로젝트 로컬 설정: `.harness/skill-publish.local.json`
181
+ 5. 위 항목이 없으면 업로드 직전에 토큰 입력 요구
182
+
183
+ 로컬 설정 예시:
184
+
185
+ ```json
186
+ {
187
+ "publishAuth": {
188
+ "npmTokenEnv": "LEERNESS_NPM_TOKEN",
189
+ "gitTokenEnv": "LEERNESS_GITHUB_TOKEN",
190
+ "gitRemoteUrl": "https://github.com/gugu9999gu/leerness"
191
+ }
192
+ }
193
+ ```
194
+
195
+ `.harness/skill-publish.local.json`은 `.gitignore`에 포함됩니다. 실제 토큰값을 저장하기보다 토큰이 들어있는 환경변수 이름만 저장하는 방식을 권장합니다.
196
+
197
+ Git 업로드 기본 저장소 경로는 다음과 같습니다.
198
+
199
+ ```text
200
+ https://github.com/gugu9999gu/leerness
201
+ ```
202
+
149
203
  ## 스킬 메타데이터
150
204
 
151
205
  각 스킬에는 최종 업데이트 날짜와 검증 상태가 표시됩니다.
package/bin/harness.js CHANGED
@@ -5,12 +5,14 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const readline = require('readline');
7
7
  const childProcess = require('child_process');
8
+ const os = require('os');
8
9
 
9
- const VERSION = '1.0.0';
10
+ const VERSION = '1.2.0';
10
11
  const MARK = '<!-- leerness:managed -->';
11
12
  const MIGRATED = '<!-- leerness:migrated-legacy -->';
12
13
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
13
14
  const PACKS_DIR = path.join(PACKAGE_ROOT, 'skill-packs');
15
+ const DEFAULT_GIT_REPOSITORY = 'https://github.com/gugu9999gu/leerness';
14
16
  const c = { reset:'\x1b[0m', bold:'\x1b[1m', dim:'\x1b[2m', green:'\x1b[32m', yellow:'\x1b[33m', cyan:'\x1b[36m', red:'\x1b[31m', magenta:'\x1b[35m' };
15
17
 
16
18
  const legacyItems = ['AI_HARNESS.md','HARNESS.md','PROJECT_CONTEXT.md','CONTEXT.md','ARCHITECTURE.md','DECISIONS.md','CURRENT_STATE.md','TASK_LOG.md','AGENT.md','AGENTS.md','CLAUDE.md','.cursorrules','.cursor/rules/project-rules.mdc','.cursor/rules/leerness.mdc','.github/copilot-instructions.md','docs/guideline.md','docs/history.md','.ai','harness','.harness'];
@@ -20,7 +22,7 @@ const coreFiles = {
20
22
  'CLAUDE.md': [MARK,'# Claude Code Instructions','','Use AGENTS.md as the source of truth. Before editing, read .harness/current-state.md, architecture.md, context-map.md, guardrails.md, skills-lock.json, and the matching skill file.',''].join('\n'),
21
23
  '.cursor/rules/leerness.mdc': [MARK,'---','alwaysApply: true','---','Read AGENTS.md first. Follow .harness project memory, installed skills, design-system, feature-contracts, and guardrails.',''].join('\n'),
22
24
  '.github/copilot-instructions.md': [MARK,'# GitHub Copilot Instructions','','Use AGENTS.md and .harness/ as the project memory. Preserve architecture, feature contracts, security rules, and UI consistency.',''].join('\n'),
23
- '.gitignore': ['# Leerness local secrets','.env','.env.local','*.secret.json','.harness/skill-config.local.json',''].join('\n'),
25
+ '.gitignore': ['# Leerness local secrets','.env','.env.local','*.secret.json','.harness/skill-config.local.json','.harness/skill-publish.local.json',''].join('\n'),
24
26
  '.env.example': ['# Leerness environment variable examples','# Copy to .env.local and fill values locally. Never commit real secrets.','',''].join('\n'),
25
27
  '.harness/HARNESS_VERSION': '{{VERSION}}\n',
26
28
  '.harness/manifest.json': '{{MANIFEST}}\n',
@@ -41,12 +43,17 @@ const coreFiles = {
41
43
  '.harness/review-checklist.md': [MARK,'# Review Checklist','','- [ ] Existing architecture preserved','- [ ] Feature contracts respected','- [ ] Design system followed','- [ ] Secrets not exposed','- [ ] Tests or manual verification completed','- [ ] current-state/task-log/session-handoff updated',''].join('\n'),
42
44
  '.harness/release-checklist.md': [MARK,'# Release Checklist','','- [ ] Build/test passed','- [ ] Env variables confirmed','- [ ] Migration impact checked','- [ ] Rollback path known','- [ ] Release notes prepared',''].join('\n'),
43
45
  '.harness/session-handoff.md': [MARK,'# Session Handoff','','## Done','-','','## Changed Files','-','','## Decisions','-','','## Risks','-','','## Next Exact Step','-',''].join('\n'),
44
- '.harness/skill-index.md': [MARK,'# Skill Index','','Installed skill libraries are tracked in `.harness/skills-lock.json`.','','## Commands','`leerness skill list`','`leerness skill add commerce-api`','`leerness skill learn my-skill --from .harness/skills/...`','`leerness library verify .harness/library/my-skill --ai`','`leerness library build .harness/library/my-skill`','`leerness library publish .harness/library/my-skill/dist/my-skill --target npm --execute`','','## Metadata','Every skill should expose version, lastUpdated, lastUpdatedAt, and verification status.',''].join('\n'),
46
+ '.harness/skill-index.md': [MARK,'# Skill Index','','Installed skill libraries are tracked in `.harness/skills-lock.json`.','','## Commands','`leerness skill list` — 한글명/가능 작업/업데이트/검증 상태 표시','`leerness skill add commerce-api`','`leerness skill learn my-skill --from .harness/skills/...`','`leerness library verify .harness/library/my-skill --ai`','`leerness library build .harness/library/my-skill`','`leerness skill add ai-verified-skill-publisher`','`leerness library publish .harness/library/my-skill/dist/my-skill --target npm --execute`','','## Metadata','Every skill should expose version, lastUpdated, lastUpdatedAt, and verification status.',''].join('\n'),
45
47
  '.harness/AX_SKILL_LIBRARY_GUIDE.md': [MARK,
46
48
  '# Leerness AX Skill Library Guide',
47
49
  '',
48
50
  'AX는 AI eXperience입니다. AI 에이전트가 검증된 스킬 데이터를 안전하게 학습, 검증, 빌드, 업로드, 업데이트, 병합, 마이그레이션하도록 안내합니다.',
49
51
  '',
52
+ '## 스킬 라이브러리 표시 규격',
53
+ '- 모든 스킬은 name, displayNameKo, title, capabilities, lastUpdated, verification을 가진다.',
54
+ '- leerness skill list는 한글명과 가능한 작업을 먼저 보여준다.',
55
+ '- AI가 스킬을 업로드할 때는 ai-verified-skill-publisher 스킬의 절차를 따른다.',
56
+ '',
50
57
  '## 원칙',
51
58
  '- 실제 토큰, 쿠키, 비밀번호, 고객 데이터는 저장하지 않는다.',
52
59
  '- 환경변수 이름과 연결 규칙만 기록한다.',
@@ -114,7 +121,7 @@ function rel(root,p){ return path.relative(root,p).replace(/\\/g,'/') || '.'; }
114
121
  function isTextFile(p){ return /\.(md|mdc|txt|json|js|ts|tsx|jsx|yml|yaml|env|gitignore)$/i.test(p) || !path.extname(p); }
115
122
  function parseJsonSafe(s,fallback){ try { return JSON.parse(s); } catch { return fallback; } }
116
123
  function banner(){ log(''); log(c.bold+c.magenta+'Leerness v'+VERSION+c.reset); log(c.dim+'맞춤성장형 AI 개발 하네스 · context, skills, design, consistency'+c.reset); log(''); }
117
- function installGuide(){ log(c.bold+'설치 안내'+c.reset); log(' - 기존 AI 하네스/지침 파일을 감지하면 먼저 .harness/archive/ 에 백업합니다.'); log(' - .harness/ 아래에 프로젝트 메모리, 스킬, 디자인/기능 계약 문서를 생성합니다.'); log(' - 스킬 라이브러리는 실제 민감정보를 저장하지 않고 환경변수 이름만 기록합니다.'); log(' - library publish는 기본 dry-run이며, 실제 업로드는 --execute가 필요합니다.'); log(''); }
124
+ function installGuide(){ log(c.bold+'설치 안내'+c.reset); log(' - 기존 AI 하네스/지침 파일을 감지하면 먼저 .harness/archive/ 에 백업합니다.'); log(' - .harness/ 아래에 프로젝트 메모리, 스킬, 디자인/기능 계약 문서를 생성합니다.'); log(' - 스킬 라이브러리는 실제 민감정보를 저장하지 않고 환경변수 이름만 기록합니다.'); log(' - library publish는 기본 dry-run이며, 실제 업로드는 --execute가 필요합니다.'); log(' - 검증된 스킬팩 실제 업로드 시 npm/git 토큰을 환경변수·로컬 설정에서 찾고, 없으면 입력을 요구합니다.'); log(''); }
118
125
  function projectName(root){ try{ const pkg=JSON.parse(read(path.join(root,'package.json'))); if(pkg.name) return String(pkg.name).replace(/^@[^/]+\//,''); }catch{} return path.basename(root); }
119
126
 
120
127
  function detectLegacy(root){ return legacyItems.map(item=>({item,full:path.join(root,item)})).filter(e=>{ if(!exists(e.full)) return false; if(e.item==='.harness'){ const vf=path.join(root,'.harness/HARNESS_VERSION'); return !exists(vf) || read(vf).trim()!==VERSION; } try{ if(fs.statSync(e.full).isFile() && isTextFile(e.item)){ const b=read(e.full); if(b.includes(MARK)||b.includes(MIGRATED)) return false; } }catch{} return true; }); }
@@ -132,15 +139,15 @@ function makeContext(root,legacyText,selectedSkills){ const date=new Date().toIS
132
139
 
133
140
  function listSkillPacks(){ if(!exists(PACKS_DIR)) return []; return fs.readdirSync(PACKS_DIR).map(n=>getSkillMeta(n)).filter(Boolean).sort((a,b)=>a.name.localeCompare(b.name)); }
134
141
  function getSkillMeta(name){ const metaPath=path.join(PACKS_DIR,name,'skill.json'); if(!exists(metaPath)) return null; const meta=parseJsonSafe(read(metaPath),null); if(!meta||!meta.name) return null; return meta; }
135
- function updateSkillLock(root,meta,remove=false){ const lp=path.join(root,'.harness/skills-lock.json'); const lock=exists(lp)?parseJsonSafe(read(lp),{harnessVersion:VERSION,installedSkills:{}}):{harnessVersion:VERSION,installedSkills:{}}; lock.harnessVersion=VERSION; lock.updatedAt=new Date().toISOString(); lock.installedSkills=lock.installedSkills||{}; if(remove) delete lock.installedSkills[meta.name]; else lock.installedSkills[meta.name]={version:meta.version,source:meta.source||'bundled',title:meta.title,requiresEnv:meta.requiresEnv||[]}; write(lp,JSON.stringify(lock,null,2)+'\n'); }
142
+ function updateSkillLock(root,meta,remove=false){ const lp=path.join(root,'.harness/skills-lock.json'); const lock=exists(lp)?parseJsonSafe(read(lp),{harnessVersion:VERSION,installedSkills:{}}):{harnessVersion:VERSION,installedSkills:{}}; lock.harnessVersion=VERSION; lock.updatedAt=new Date().toISOString(); lock.installedSkills=lock.installedSkills||{}; if(remove) delete lock.installedSkills[meta.name]; else lock.installedSkills[meta.name]={version:meta.version,source:meta.source||'bundled',title:meta.title,displayNameKo:meta.displayNameKo||meta.title,categoryKo:meta.categoryKo||meta.category,capabilities:meta.capabilities||[],requiresEnv:meta.requiresEnv||[],lastUpdated:meta.lastUpdated,lastUpdatedAt:meta.lastUpdatedAt,verificationStatus:(meta.verification||{}).status||'unknown'}; write(lp,JSON.stringify(lock,null,2)+'\n'); }
136
143
  function appendEnvExample(root,meta){ const ep=path.join(root,'.env.example'); const existing=exists(ep)?read(ep):''; const missing=(meta.requiresEnv||[]).filter(n=>!existing.includes(n+'=')); if(!missing.length) return; write(ep,existing+'\n# '+(meta.title||meta.name)+' ('+meta.name+')\n'+missing.map(n=>n+'=').join('\n')+'\n'); }
137
144
  function installSkill(root,name,dryRun=false){ const meta=getSkillMeta(name); if(!meta){ fail('알 수 없는 스킬 라이브러리: '+name); info('사용 가능 목록: '+listSkillPacks().map(x=>x.name).join(', ')); return false; } const packRoot=path.join(PACKS_DIR,name); const destRoot=path.join(root,'.harness/skills',name); if(dryRun){ info('[dry-run] install skill: '+name); return true; } fs.mkdirSync(destRoot,{recursive:true}); for(const file of meta.files||[]){ const src=path.join(packRoot,file); const dest=path.join(destRoot,path.basename(file)); if(exists(src)){ write(dest,read(src)); ok('스킬 설치: '+rel(root,dest)); } } write(path.join(destRoot,'skill.json'),JSON.stringify(meta,null,2)+'\n'); updateSkillLock(root,meta,false); appendEnvExample(root,meta); return true; }
138
145
  function removeSkill(root,name){ const meta=getSkillMeta(name)||{name,title:name}; const dest=path.join(root,'.harness/skills',name); if(exists(dest)) fs.rmSync(dest,{recursive:true,force:true}); updateSkillLock(root,meta,true); ok('스킬 제거: '+name); }
139
146
 
140
147
  function parseArgs(argv){ const out={flags:{},positionals:[]}; const valueFlags=new Set(['skills','path','from','out','target','package','repo','version','title','description','category','source','name','registry','branch','message','reviewer','by']); for(let i=0;i<argv.length;i++){ const a=argv[i]; if(a.startsWith('--')){ const eq=a.indexOf('='); const key=eq>=0?a.slice(2,eq):a.slice(2); if(eq>=0) out.flags[key]=a.slice(eq+1); else if(valueFlags.has(key)&&argv[i+1]&&!argv[i+1].startsWith('-')) out.flags[key]=argv[++i]; else out.flags[key]=true; } else if(a.startsWith('-')) out.flags[a.slice(1)]=true; else out.positionals.push(a); } return out; }
141
- function splitSkills(value){ if(!value||value===true) return []; if(value==='recommended') return ['office','commerce-api','crawling']; if(value==='all') return listSkillPacks().map(x=>x.name); return String(value).split(',').map(x=>x.trim()).filter(Boolean); }
148
+ function splitSkills(value){ if(!value||value===true) return []; if(value==='recommended') return ['office','commerce-api','crawling','ai-verified-skill-publisher']; if(value==='all') return listSkillPacks().map(x=>x.name); return String(value).split(',').map(x=>x.trim()).filter(Boolean); }
142
149
  function ask(q){ const rl=readline.createInterface({input:process.stdin,output:process.stdout}); return new Promise(resolve=>rl.question(q,a=>{rl.close();resolve(a.trim());})); }
143
- async function chooseSkills(autoYes,provided){ if(provided!==undefined) return splitSkills(provided); if(autoYes||!process.stdin.isTTY) return []; const packs=listSkillPacks(); if(!packs.length) return []; log(c.bold+'설치할 스킬 라이브러리 선택'+c.reset); log(' 0) 기본 하네스만 설치'); packs.forEach((p,i)=>log(' '+(i+1)+') '+p.title+' ('+p.name+')')); log(' all) 전체 설치'); const ans=await ask('\n선택 (예: 1,3 또는 all, Enter=기본): '); if(!ans||ans==='0') return []; if(ans.toLowerCase()==='all') return packs.map(p=>p.name); return ans.split(',').map(s=>parseInt(s.trim(),10)).filter(n=>n>=1&&n<=packs.length).map(n=>packs[n-1].name); }
150
+ async function chooseSkills(autoYes,provided){ if(provided!==undefined) return splitSkills(provided); if(autoYes||!process.stdin.isTTY) return []; const packs=listSkillPacks(); if(!packs.length) return []; log(c.bold+'설치할 스킬 라이브러리 선택'+c.reset); log(' 0) 기본 하네스만 설치'); packs.forEach((p,i)=>{ log(' '+(i+1)+') '+(p.displayNameKo||p.title)+' ('+p.name+')'); if((p.capabilities||[]).length) log(' 가능 작업: '+p.capabilities.slice(0,4).join(', ')); }); log(' all) 전체 설치'); const ans=await ask('\n선택 (예: 1,3 또는 all, Enter=기본): '); if(!ans||ans==='0') return []; if(ans.toLowerCase()==='all') return packs.map(p=>p.name); return ans.split(',').map(s=>parseInt(s.trim(),10)).filter(n=>n>=1&&n<=packs.length).map(n=>packs[n-1].name); }
144
151
 
145
152
  async function init(root,flags){ root=path.resolve(root||process.cwd()); fs.mkdirSync(root,{recursive:true}); banner(); installGuide(); info('대상: '+root); const selectedSkills=await chooseSkills(Boolean(flags.yes||flags.y),flags.skills); const found=detectLegacy(root); const legacyText=collectLegacyText(found); if(found.length){ warn('기존 하네스/지침 파일 감지: '+found.length+'개'); found.forEach(f=>log(' - '+f.item)); } const archive=archiveLegacy(root,found,false); if(archive) info('백업 완료: '+rel(root,archive)); neutralizeLegacy(root,found,false); const ctx=makeContext(root,legacyText,selectedSkills); for(const [file,template] of Object.entries(coreFiles)){ const target=path.join(root,file); const body=fill(template,ctx); if(exists(target)&&read(target)===body){ ok('유지: '+file); continue; } const existed=exists(target); if(file==='.gitignore'&&existed){ const current=read(target); const additions=body.split('\n').filter(line=>line&&!current.includes(line)).join('\n'); if(additions) write(target,current.replace(/\s*$/,'\n')+additions+'\n'); ok('보강: .gitignore'); continue; } write(target,body); ok((existed?'업데이트: ':'생성: ')+file); } if(selectedSkills.length){ log(''); info('선택 스킬 설치 중: '+selectedSkills.join(', ')); for(const name of selectedSkills) installSkill(root,name,false); } log(''); ok('설치 완료'); log('다음 단계: .harness/project-brief.md, context-map.md, design-system.md를 프로젝트에 맞게 채우세요.'); }
146
153
  function migrate(root,flags){ root=path.resolve(root||process.cwd()); banner(); installGuide(); const dryRun=Boolean(flags['dry-run']); const found=detectLegacy(root); if(!found.length){ ok('마이그레이션할 legacy 항목이 없습니다.'); return; } warn('마이그레이션 대상: '+found.length+'개'); found.forEach(f=>log(' - '+f.item)); const archive=archiveLegacy(root,found,dryRun); info((dryRun?'[dry-run] 백업 예정: ':'백업 완료: ')+rel(root,archive)); if(!dryRun) neutralizeLegacy(root,found,false); const ctx=makeContext(root,collectLegacyText(found),[]); for(const [file,template] of Object.entries(coreFiles)){ const target=path.join(root,file); if(dryRun) info('[dry-run] create/update: '+file); else write(target,fill(template,ctx)); } if(!dryRun) ok('마이그레이션 완료'); }
@@ -161,9 +168,9 @@ function buildSkillLibrary(dir,flags){ dir=path.resolve(dir||process.cwd()); con
161
168
  function mergeSkillLibrary(root,source,flags){ root=path.resolve(root||process.cwd()); source=path.resolve(source||flags.source||''); if(!source||!exists(source)){ fail('병합할 스킬 라이브러리 경로가 필요합니다.'); process.exitCode=1; return; } const check=validateSkillLibrary(source,{silent:false}); if(!check.ok){ process.exitCode=1; return; } const meta=check.meta; const name=slugifyName(meta.name); const dest=path.join(root,'.harness/skills',name); fs.mkdirSync(dest,{recursive:true}); const srcSkills=path.join(source,'skills'); if(exists(srcSkills)) copyRecursive(srcSkills,dest); write(path.join(dest,'skill-library.json'),JSON.stringify(meta,null,2)+'\n'); updateSkillLock(root,{name,version:meta.version||'0.1.0',title:meta.title||name,requiresEnv:meta.requiresEnv||[],source:'library'},false); appendEnvExample(root,{name,title:meta.title||name,requiresEnv:meta.requiresEnv||[]}); ok('스킬 라이브러리 병합 완료: '+rel(root,dest)); }
162
169
  function migrateSkillLibrary(dir,flags){ dir=path.resolve(dir||process.cwd()); if(!exists(dir)){ fail('마이그레이션 대상 경로가 없습니다: '+dir); process.exitCode=1; return; } const meta=readSkillLibraryMeta(dir)||{}; const migrated={name:slugifyName(flags.name||meta.name||path.basename(dir)),version:String(flags.version||meta.version||'0.1.0'),title:flags.title||meta.title||meta.description||path.basename(dir),description:flags.description||meta.description||'Migrated Leerness skill library.',category:flags.category||meta.category||'custom',compatibleHarness:'>=3.2.0',sensitiveDataPolicy:'env-reference-only',requiresEnv:Array.from(new Set(meta.requiresEnv||meta.harnessSkill?.requiresEnv||[])),migratedAt:new Date().toISOString()}; const skillsDir=path.join(dir,'skills'); if(!exists(skillsDir)) fs.mkdirSync(skillsDir,{recursive:true}); const mdFiles=skillLibraryFiles(dir).filter(f=>f.endsWith('.md')&&!isInside(skillsDir,f)&&!f.includes(path.sep+'node_modules'+path.sep)); for(const f of mdFiles){ if(path.basename(f).toLowerCase()==='readme.md') continue; const dest=path.join(skillsDir,path.basename(f)); if(!exists(dest)) fs.copyFileSync(f,dest); } migrated.files=skillLibraryFiles(skillsDir).filter(f=>f.endsWith('.md')).map(f=>rel(dir,f)); write(path.join(dir,'skill-library.json'),JSON.stringify(migrated,null,2)+'\n'); if(!exists(path.join(dir,'README.md'))) write(path.join(dir,'README.md'),'# '+migrated.title+'\n\n'+migrated.description+'\n'); const check=validateSkillLibrary(dir,{silent:false}); if(!check.ok) process.exitCode=1; else ok('스킬 라이브러리 마이그레이션 완료: '+dir); }
163
170
  function publishSkillLibrary(dir,flags){ dir=path.resolve(dir||process.cwd()); const target=String(flags.target||'npm'); const execute=Boolean(flags.execute); const check=validateSkillLibrary(dir,{silent:false}); if(!check.ok){ process.exitCode=1; return; } if(target==='npm'){ if(!exists(path.join(dir,'package.json'))){ warn('package.json이 없습니다. 먼저 build를 실행하세요.'); info('leerness library build '+dir); process.exitCode=1; return; } const args=['publish','--access','public'].concat(flags.registry?['--registry',flags.registry]:[]); if(!execute){ info('[dry-run] 실행 예정: (cd '+dir+') npm '+args.join(' ')); info('실제 배포는 --execute를 붙이세요.'); return; } const r=childProcess.spawnSync('npm',args,{cwd:dir,stdio:'inherit',shell:process.platform==='win32'}); process.exitCode=r.status||0; return; } if(target==='git'){ const repo=flags.repo; const branch=flags.branch||'main'; const message=flags.message||('Publish skill library '+check.meta.name+'@'+(check.meta.version||'0.1.0')); if(!execute){ info('[dry-run] git target repo: '+(repo||'(current repo)')); info('[dry-run] branch: '+branch); info('[dry-run] commit message: '+message); info('실제 push는 --execute를 붙이세요.'); return; } const run=(cmd,args)=>{ const r=childProcess.spawnSync(cmd,args,{cwd:dir,stdio:'inherit',shell:process.platform==='win32'}); if(r.status) process.exit(r.status); }; if(repo&&!exists(path.join(dir,'.git'))){ run('git',['init']); run('git',['remote','add','origin',repo]); } run('git',['add','.']); run('git',['commit','-m',message]); run('git',['branch','-M',branch]); run('git',['push','-u','origin',branch]); return; } fail('지원하지 않는 publish target: '+target); process.exitCode=1; }
164
- function libraryCommand(args,flags){ const sub=args[1]||'help'; if(sub==='help'){ log(['Leerness Skill Library Commands','',' leerness skill learn <name> --from .harness/skills/<name> [--out ./library/<name>]',' leerness library validate <path>',' leerness library build <path> [--out ./dist] [--package leerness-skill-name]',' leerness library merge <source-library> [--path <project>]',' leerness library migrate <path> [--version 1.0.0]',' leerness library publish <built-library> --target npm|git [--execute]','','기본 publish는 dry-run입니다. 실제 npm/git 업로드는 --execute가 필요합니다.',''].join('\n')); return; } if(sub==='validate') return validateSkillLibrary(args[2]||process.cwd(),{silent:false}); if(sub==='build') return buildSkillLibrary(args[2]||process.cwd(),flags); if(sub==='merge') return mergeSkillLibrary(flags.path||process.cwd(),args[2]||flags.source,flags); if(sub==='migrate') return migrateSkillLibrary(args[2]||process.cwd(),flags); if(sub==='publish'||sub==='upload') return publishSkillLibrary(args[2]||process.cwd(),flags); fail('알 수 없는 library 명령: '+sub); process.exitCode=1; }
171
+ function libraryCommand(args,flags){ const sub=args[1]||'help'; if(sub==='help'){ log(['Leerness Skill Library Commands','',' leerness skill learn <name> --from .harness/skills/<name> [--out ./library/<name>]',' leerness library validate <path>',' leerness library build <path> [--out ./dist] [--package leerness-skill-name]',' leerness library merge <source-library> [--path <project>]',' leerness library migrate <path> [--version 1.0.0]',' leerness library publish <built-library> --target npm|git [--execute] [--repo https://github.com/gugu9999gu/leerness]','','기본 publish는 dry-run입니다. 실제 npm/git 업로드는 --execute가 필요합니다.',''].join('\n')); return; } if(sub==='validate') return validateSkillLibrary(args[2]||process.cwd(),{silent:false}); if(sub==='build') return buildSkillLibrary(args[2]||process.cwd(),flags); if(sub==='merge') return mergeSkillLibrary(flags.path||process.cwd(),args[2]||flags.source,flags); if(sub==='migrate') return migrateSkillLibrary(args[2]||process.cwd(),flags); if(sub==='publish'||sub==='upload') return publishSkillLibrary(args[2]||process.cwd(),flags); fail('알 수 없는 library 명령: '+sub); process.exitCode=1; }
165
172
  function skillCommand(args,flags){ const sub=args[1]||'list'; const root=path.resolve(flags.path||process.cwd()); if(sub==='learn'){ flags.name=args[2]||flags.name; return learnSkillLibrary(root,flags); } if(sub==='library') return libraryCommand(['library'].concat(args.slice(2)),flags); if(sub==='list'){ banner(); log('사용 가능한 스킬 라이브러리'); for(const p of listSkillPacks()){ log('- '+p.name+'@'+p.version+': '+p.title); log(' '+p.description); if((p.requiresEnv||[]).length) log(' env: '+(p.requiresEnv||[]).join(', ')); } return; } const name=args[2]; if(!name){ fail('스킬 이름이 필요합니다. 예: leerness skill add commerce-api'); return; } if(sub==='add'||sub==='install') return installSkill(root,name,Boolean(flags['dry-run'])); if(sub==='remove'||sub==='rm') return removeSkill(root,name); if(sub==='update') return installSkill(root,name,false); fail('알 수 없는 skill 명령: '+sub); }
166
- function help(){ log(['Leerness v'+VERSION,'','Usage:',' leerness init [path] [--yes] [--skills office,commerce-api|recommended|all]',' leerness migrate [path] [--dry-run]',' leerness status [path]',' leerness verify [path]','','Skills:',' leerness skill list',' leerness skill add <name> [--path <project>]',' leerness skill remove <name> [--path <project>]',' leerness skill update <name> [--path <project>]',' leerness skill learn <name> --from <validated-skill-path> [--out <library-path>]','','Skill library lifecycle:',' leerness library validate <path>',' leerness library build <path> [--out ./dist] [--package leerness-skill-name]',' leerness library merge <source-library> [--path <project>]',' leerness library migrate <path> [--version 1.0.0]',' leerness library publish <built-library> --target npm|git [--execute]',' leerness --version','','Examples:',' npx leerness init --skills recommended',' npx leerness skill learn coupang-order-sync --from .harness/skills/commerce-api/order-sync.md',' npx leerness library build .harness/library/coupang-order-sync',' npx leerness library publish .harness/library/coupang-order-sync/dist/coupang-order-sync --target npm --execute',''].join('\n')); }
173
+ function help(){ log(['Leerness v'+VERSION,'','Usage:',' leerness init [path] [--yes] [--skills office,commerce-api|recommended|all]',' leerness migrate [path] [--dry-run]',' leerness status [path]',' leerness verify [path]','','Skills:',' leerness skill list',' leerness skill info <name>',' leerness skill add <name> [--path <project>]',' leerness skill remove <name> [--path <project>]',' leerness skill update <name> [--path <project>]',' leerness skill learn <name> --from <validated-skill-path> [--out <library-path>]','','Skill library lifecycle:',' leerness library validate <path>',' leerness library build <path> [--out ./dist] [--package leerness-skill-name]',' leerness library merge <source-library> [--path <project>]',' leerness library migrate <path> [--version 1.0.0]',' leerness library publish <built-library> --target npm|git [--execute] [--repo https://github.com/gugu9999gu/leerness]',' leerness --version','','Examples:',' npx leerness init --skills recommended',' npx leerness skill learn coupang-order-sync --from .harness/skills/commerce-api/order-sync.md',' npx leerness library build .harness/library/coupang-order-sync',' npx leerness library publish .harness/library/coupang-order-sync/dist/coupang-order-sync --target npm --execute',''].join('\n')); }
167
174
 
168
175
 
169
176
  function nowIso(){ return new Date().toISOString(); }
@@ -309,11 +316,67 @@ function migrateSkillLibrary(dir,flags){
309
316
  for(const f of mdFiles){ if(path.basename(f).toLowerCase()==='readme.md') continue; const dest=path.join(skillsDir,path.basename(f)); if(!exists(dest)) fs.copyFileSync(f,dest); }
310
317
  migrated.files=skillLibraryFiles(skillsDir).filter(f=>f.endsWith('.md')).map(f=>rel(dir,f)); writeSkillLibraryMeta(dir,migrated); if(!exists(path.join(dir,'README.md'))) write(path.join(dir,'README.md'),'# '+migrated.title+'\n\n'+migrated.description+'\n'); const check=validateSkillLibrary(dir,{silent:false}); if(!check.ok) process.exitCode=1; else { warn('마이그레이션 완료. 검증 상태가 needs-review입니다.'); info('다음: leerness library verify '+dir+' --ai --reviewer leerness-ai'); }
311
318
  }
312
- function publishSkillLibrary(dir,flags){
319
+ function findProjectRootForPublish(start){
320
+ let cur=path.resolve(start||process.cwd());
321
+ if(exists(cur)&&fs.statSync(cur).isFile()) cur=path.dirname(cur);
322
+ for(;;){
323
+ if(exists(path.join(cur,'.harness'))) return cur;
324
+ const parent=path.dirname(cur);
325
+ if(parent===cur) return null;
326
+ cur=parent;
327
+ }
328
+ }
329
+ function readPublishLocalConfig(dir){
330
+ const projectRoot=findProjectRootForPublish(dir)||process.cwd();
331
+ const candidates=[path.join(projectRoot,'.harness','skill-publish.local.json'),path.join(projectRoot,'.harness','skill-config.local.json'),path.join(projectRoot,'.leerness.publish.local.json'),path.join(dir,'skill-publish.local.json')];
332
+ for(const p of candidates){ if(exists(p)){ const cfg=parseJsonSafe(read(p),null); if(cfg) return {path:p,config:cfg}; warn('로컬 publish 설정 JSON을 읽지 못했습니다: '+p); } }
333
+ return {path:null,config:{}};
334
+ }
335
+ function deepGet(obj,keys){ for(const k of keys){ if(!obj) return undefined; obj=obj[k]; } return obj; }
336
+ function tokenFromConfig(target,dir){
337
+ const loaded=readPublishLocalConfig(dir); const cfg=loaded.config||{}; const auth=cfg.publishAuth||cfg.auth||{};
338
+ const envKeys=target==='npm' ? [auth.npmTokenEnv,cfg.npmTokenEnv,'LEERNESS_NPM_TOKEN','NPM_TOKEN','NODE_AUTH_TOKEN'] : [auth.gitTokenEnv,auth.githubTokenEnv,cfg.gitTokenEnv,cfg.githubTokenEnv,'LEERNESS_GIT_TOKEN','LEERNESS_GITHUB_TOKEN','GITHUB_TOKEN','GH_TOKEN','GIT_TOKEN'];
339
+ for(const k of envKeys.filter(Boolean)){ if(process.env[k]) return {token:process.env[k],source:'env:'+k,configPath:loaded.path}; }
340
+ const direct=target==='npm' ? (auth.npmToken||cfg.npmToken||deepGet(cfg,['npm','token'])) : (auth.gitToken||auth.githubToken||cfg.gitToken||cfg.githubToken||deepGet(cfg,['git','token'])||deepGet(cfg,['github','token']));
341
+ if(direct){ warn('로컬 설정 파일의 직접 토큰을 사용합니다. 가능하면 토큰값 대신 tokenEnv 방식이 더 안전합니다: '+loaded.path); return {token:direct,source:'local-config:'+loaded.path,configPath:loaded.path}; }
342
+ return {token:null,source:null,configPath:loaded.path};
343
+ }
344
+ function repoFromConfig(dir,flags){ const loaded=readPublishLocalConfig(dir); const cfg=loaded.config||{}; const auth=cfg.publishAuth||cfg.auth||{}; return flags.repo||auth.gitRemoteUrl||cfg.gitRemoteUrl||cfg.repository||process.env.LEERNESS_GIT_REPO||process.env.GIT_REMOTE_URL||DEFAULT_GIT_REPOSITORY; }
345
+ function promptSecret(question){
346
+ return new Promise(resolve=>{
347
+ if(!process.stdin.isTTY||!process.stdout.isTTY){ resolve(''); return; }
348
+ const stdin=process.stdin; let value=''; process.stdout.write(question);
349
+ const cleanup=()=>{ stdin.removeListener('data',onData); try{ stdin.setRawMode(false); }catch{} process.stdout.write('\n'); resolve(value.trim()); };
350
+ const onData=(ch)=>{ ch=String(ch); if(ch==='\u0003'){ process.stdout.write('\n'); process.exit(130); } if(ch==='\r'||ch==='\n') return cleanup(); if(ch==='\u007f'||ch==='\b'){ if(value.length){ value=value.slice(0,-1); process.stdout.write('\b \b'); } return; } value+=ch; process.stdout.write('*'); };
351
+ try{ stdin.setRawMode(true); }catch{} stdin.resume(); stdin.setEncoding('utf8'); stdin.on('data',onData);
352
+ });
353
+ }
354
+ async function resolvePublishToken(target,dir,flags){
355
+ if(flags.token){ warn('--token 인자는 shell history에 남을 수 있습니다. 가능하면 환경변수나 입력 프롬프트를 사용하세요.'); return {token:String(flags.token),source:'--token'}; }
356
+ if(flags['token-env']&&process.env[String(flags['token-env'])]) return {token:process.env[String(flags['token-env'])],source:'env:'+flags['token-env']};
357
+ const cfgToken=tokenFromConfig(target,dir); if(cfgToken.token) return cfgToken; if(flags['no-prompt']) return {token:null,source:null};
358
+ const label=target==='npm'?'npm access token':'git/GitHub access token'; const token=await promptSecret(label+' 입력: '); return {token,source:token?'interactive-prompt':null};
359
+ }
360
+ function writeTempNpmrc(token,registry){ const host=(registry||'https://registry.npmjs.org/').replace(/^https?:/,'').replace(/\/$/,''); const file=path.join(os.tmpdir(),'leerness-npmrc-'+Date.now()+'-'+Math.random().toString(16).slice(2)); fs.writeFileSync(file,'registry='+(registry||'https://registry.npmjs.org/')+'\n'+host+'/:_authToken='+token+'\n',{encoding:'utf8',mode:0o600}); return file; }
361
+ function gitRun(dir,args,token){ const finalArgs=token?['-c','http.extraHeader=Authorization: Bearer '+token].concat(args):args; const r=childProcess.spawnSync('git',finalArgs,{cwd:dir,stdio:'inherit',shell:process.platform==='win32'}); if(r.status) process.exit(r.status); }
362
+ function ensureGitRemote(dir,repo){ const get=childProcess.spawnSync('git',['remote','get-url','origin'],{cwd:dir,encoding:'utf8',shell:process.platform==='win32'}); if(get.status===0) gitRun(dir,['remote','set-url','origin',repo]); else gitRun(dir,['remote','add','origin',repo]); }
363
+ async function publishSkillLibrary(dir,flags){
313
364
  dir=path.resolve(dir||process.cwd()); const target=String(flags.target||'npm'); const execute=Boolean(flags.execute); const check=validateSkillLibrary(dir,{silent:false,strictAi:true}); if(!check.ok){ process.exitCode=1; return; }
314
- if(!isAiVerified(check.meta)){ fail('AI 검증된 스킬만 업로드할 수 있습니다. `leerness library verify <path> --ai`를 먼저 실행하세요.'); process.exitCode=1; return; }
315
- if(target==='npm'){ if(!exists(path.join(dir,'package.json'))){ warn('package.json이 없습니다. 먼저 build를 실행하세요.'); info('leerness library build '+dir); process.exitCode=1; return; } const args=['publish','--access','public'].concat(flags.registry?['--registry',flags.registry]:[]); if(!execute){ info('[dry-run] AI 검증 통과. 실행 예정: (cd '+dir+') npm '+args.join(' ')); info('실제 배포는 --execute를 붙이세요.'); return; } const r=childProcess.spawnSync('npm',args,{cwd:dir,stdio:'inherit',shell:process.platform==='win32'}); process.exitCode=r.status||0; return; }
316
- if(target==='git'){ const repo=flags.repo; const branch=flags.branch||'main'; const message=flags.message||('Publish verified skill library '+check.meta.name+'@'+(check.meta.version||'0.1.0')); if(!execute){ info('[dry-run] AI 검증 통과. git target repo: '+(repo||'(current repo)')); info('[dry-run] branch: '+branch); info('[dry-run] commit message: '+message); info('실제 push는 --execute를 붙이세요.'); return; } const run=(cmd,args)=>{ const r=childProcess.spawnSync(cmd,args,{cwd:dir,stdio:'inherit',shell:process.platform==='win32'}); if(r.status) process.exit(r.status); }; if(repo&&!exists(path.join(dir,'.git'))){ run('git',['init']); run('git',['remote','add','origin',repo]); } run('git',['add','.']); run('git',['commit','-m',message]); run('git',['branch','-M',branch]); run('git',['push','-u','origin',branch]); return; }
365
+ if(!isAiVerified(check.meta)){ fail('AI 검증된 스킬만 업로드할 수 있습니다. leerness library verify <path> --ai 먼저 실행하세요.'); process.exitCode=1; return; }
366
+ if(target==='npm'){
367
+ if(!exists(path.join(dir,'package.json'))){ warn('package.json이 없습니다. 먼저 build를 실행하세요.'); info('leerness library build '+dir); process.exitCode=1; return; }
368
+ const args=['publish','--access','public'].concat(flags.registry?['--registry',flags.registry]:[]);
369
+ if(!execute){ info('[dry-run] AI 검증 통과. 실행 예정: (cd '+dir+') npm '+args.join(' ')); info('실제 배포는 --execute를 붙이세요. --execute 시 npm 토큰을 환경변수/로컬 설정에서 찾고 없으면 입력을 요구합니다.'); return; }
370
+ const cred=await resolvePublishToken('npm',dir,flags);
371
+ if(!cred.token){ fail('npm access token이 필요합니다. LEERNESS_NPM_TOKEN, NPM_TOKEN, NODE_AUTH_TOKEN 또는 .harness/skill-publish.local.json을 설정하거나 프롬프트에 입력하세요.'); process.exitCode=1; return; }
372
+ let userconfig=null; try{ userconfig=writeTempNpmrc(cred.token,flags.registry); info('npm 인증 소스: '+cred.source); const r=childProcess.spawnSync('npm',args.concat(['--userconfig',userconfig]),{cwd:dir,stdio:'inherit',shell:process.platform==='win32'}); process.exitCode=r.status||0; } finally { if(userconfig&&exists(userconfig)) try{ fs.rmSync(userconfig,{force:true}); }catch{} } return;
373
+ }
374
+ if(target==='git'){
375
+ const repo=repoFromConfig(dir,flags); const branch=flags.branch||'main'; const message=flags.message||('Publish verified skill library '+check.meta.name+'@'+(check.meta.version||'0.1.0'));
376
+ if(!execute){ info('[dry-run] AI 검증 통과. git target repo: '+repo); info('[dry-run] branch: '+branch); info('[dry-run] commit message: '+message); info('실제 push는 --execute를 붙이세요. --execute 시 git/GitHub 토큰을 환경변수/로컬 설정에서 찾고 없으면 입력을 요구합니다.'); return; }
377
+ const cred=await resolvePublishToken('git',dir,flags); if(!cred.token){ fail('git/GitHub access token이 필요합니다. LEERNESS_GIT_TOKEN, LEERNESS_GITHUB_TOKEN, GITHUB_TOKEN, GH_TOKEN 또는 .harness/skill-publish.local.json을 설정하거나 프롬프트에 입력하세요.'); process.exitCode=1; return; }
378
+ info('git 인증 소스: '+cred.source); if(!exists(path.join(dir,'.git'))) gitRun(dir,['init']); ensureGitRemote(dir,repo); gitRun(dir,['add','.']); const commit=childProcess.spawnSync('git',['commit','-m',message],{cwd:dir,stdio:'inherit',shell:process.platform==='win32'}); if(commit.status&&commit.status!==1) process.exit(commit.status); gitRun(dir,['branch','-M',branch]); gitRun(dir,['push','-u','origin',branch],cred.token); return;
379
+ }
317
380
  fail('지원하지 않는 publish target: '+target); process.exitCode=1;
318
381
  }
319
382
  function libraryGuide(root,flags={}){
@@ -325,7 +388,7 @@ function libraryGuide(root,flags={}){
325
388
  }
326
389
  function libraryCommand(args,flags){
327
390
  const sub=args[1]||'help';
328
- if(sub==='help'){ log(['Leerness Skill Library Commands','',' leerness library guide [project-path]',' leerness library status <path>',' leerness library validate <path> [--strict-ai]',' leerness library verify <path> --ai --reviewer leerness-ai',' leerness library build <path> [--out ./dist] [--package leerness-skill-name]',' leerness library update <path> --from <validated-new-skill-path> [--version 1.1.0]',' leerness library merge <source-library> [--path <project>]',' leerness library migrate <path> [--version 1.0.0]',' leerness library publish <built-library> --target npm|git [--execute]','','업로드는 AI 검증 메타데이터가 있는 스킬만 가능하며 기본 publish는 dry-run입니다.',''].join('\n')); return; }
391
+ if(sub==='help'){ log(['Leerness Skill Library Commands','',' leerness library guide [project-path]',' leerness library status <path>',' leerness library validate <path> [--strict-ai]',' leerness library verify <path> --ai --reviewer leerness-ai',' leerness library build <path> [--out ./dist] [--package leerness-skill-name]',' leerness library update <path> --from <validated-new-skill-path> [--version 1.2.0]',' leerness library merge <source-library> [--path <project>]',' leerness library migrate <path> [--version 1.0.0]',' leerness library publish <built-library> --target npm|git [--execute] [--repo https://github.com/gugu9999gu/leerness]','','업로드는 AI 검증 메타데이터가 있는 스킬만 가능하며 기본 publish는 dry-run입니다. --execute 시 npm/git 토큰이 필요합니다.',''].join('\n')); return; }
329
392
  if(sub==='guide') return libraryGuide(args[2]||flags.path||process.cwd(),flags);
330
393
  if(sub==='status') return libraryStatus(args[2]||process.cwd());
331
394
  if(sub==='validate') return validateSkillLibrary(args[2]||process.cwd(),{silent:false,strictAi:Boolean(flags['strict-ai']||flags.strictAi)});
@@ -337,18 +400,40 @@ function libraryCommand(args,flags){
337
400
  if(sub==='publish'||sub==='upload') return publishSkillLibrary(args[2]||process.cwd(),flags);
338
401
  fail('알 수 없는 library 명령: '+sub); process.exitCode=1;
339
402
  }
403
+
404
+ function skillDisplayName(meta){ return meta.displayNameKo || meta.titleKo || meta.title || meta.name; }
405
+ function skillCapabilities(meta){ return Array.isArray(meta.capabilities) ? meta.capabilities : []; }
406
+ function renderSkillMeta(meta){
407
+ log('- '+meta.name+'@'+meta.version+' · '+skillDisplayName(meta));
408
+ if(meta.title && meta.title!==skillDisplayName(meta)) log(' English: '+meta.title);
409
+ if(meta.categoryKo || meta.category) log(' 분류: '+(meta.categoryKo||meta.category)+' / '+(meta.category||''));
410
+ if(meta.description) log(' 설명: '+meta.description);
411
+ const caps=skillCapabilities(meta);
412
+ if(caps.length){ log(' 가능한 작업:'); caps.forEach(x=>log(' - '+x)); }
413
+ log(' 업데이트: '+(meta.lastUpdated||'unknown')+' · 검증: '+verificationLabel(meta));
414
+ if((meta.requiresEnv||[]).length) log(' 필요한 환경변수: '+meta.requiresEnv.join(', '));
415
+ }
416
+
340
417
  function skillCommand(args,flags){
341
418
  const sub=args[1]||'list'; const root=path.resolve(flags.path||process.cwd());
342
419
  if(sub==='learn'){ flags.name=args[2]||flags.name; return learnSkillLibrary(root,flags); }
343
420
  if(sub==='library') return libraryCommand(['library'].concat(args.slice(2)),flags);
344
421
  if(sub==='list'){
345
422
  banner(); log('사용 가능한 스킬 라이브러리');
346
- for(const p of listSkillPacks()){
347
- log('- '+p.name+'@'+p.version+': '+p.title);
348
- log(' '+p.description);
349
- log(' updated: '+(p.lastUpdated||'unknown')+' · verification: '+verificationLabel(p));
350
- if((p.requiresEnv||[]).length) log(' env: '+(p.requiresEnv||[]).join(', '));
351
- }
423
+ log('한글명, 가능한 작업, 최종 업데이트일, AI 검증 상태를 함께 표시합니다.');
424
+ for(const p of listSkillPacks()) renderSkillMeta(p);
425
+ log('');
426
+ info('상세 보기: leerness skill info <name>');
427
+ info('설치 예시: leerness skill add ai-verified-skill-publisher');
428
+ return;
429
+ }
430
+ if(sub==='info'||sub==='show'){
431
+ const name=args[2]; if(!name){ fail('스킬 이름이 필요합니다. 예: leerness skill info commerce-api'); return; }
432
+ const meta=getSkillMeta(name); if(!meta){ fail('알 수 없는 스킬 라이브러리: '+name); info('사용 가능 목록: '+listSkillPacks().map(x=>x.name).join(', ')); return; }
433
+ banner(); renderSkillMeta(meta);
434
+ const packRoot=path.join(PACKS_DIR,name);
435
+ const guide=path.join(packRoot,'README.md');
436
+ if(exists(guide)){ log('\n--- README ---\n'); log(read(guide)); }
352
437
  return;
353
438
  }
354
439
  const name=args[2]; if(!name){ fail('스킬 이름이 필요합니다. 예: leerness skill add commerce-api'); return; }
@@ -363,7 +448,7 @@ function status(root){
363
448
  const names=Object.keys(lock.installedSkills||{}); log('설치 스킬: '+(names.length?names.join(', '):'없음'));
364
449
  for(const n of names){ const m=lock.installedSkills[n]; log(' - '+n+'@'+(m.version||'?')+' · updated '+(m.lastUpdated||'unknown')+' · '+(m.verificationStatus||'unknown')); }
365
450
  }
366
- function help(){ log(['Leerness v'+VERSION,'','Usage:',' leerness init [path] [--yes] [--skills office,commerce-api|recommended|all]',' leerness migrate [path] [--dry-run]',' leerness status [path]',' leerness verify [path]','','Skills:',' leerness skill list',' leerness skill add <name> [--path <project>]',' leerness skill remove <name> [--path <project>]',' leerness skill update <name> [--path <project>]',' leerness skill learn <name> --from <validated-skill-path> [--out <library-path>]','','Skill library lifecycle:',' leerness library guide [path]',' leerness library status <path>',' leerness library validate <path> [--strict-ai]',' leerness library verify <path> --ai --reviewer leerness-ai',' leerness library build <path> [--out ./dist] [--package leerness-skill-name]',' leerness library update <path> --from <validated-new-skill-path> [--version 1.1.0]',' leerness library merge <source-library> [--path <project>]',' leerness library migrate <path> [--version 1.0.0]',' leerness library publish <built-library> --target npm|git [--execute]',' leerness --version','','Examples:',' npx leerness init --skills recommended',' npx leerness skill learn coupang-order-sync --from .harness/skills/commerce-api/order-sync.md',' npx leerness library verify .harness/library/coupang-order-sync --ai --reviewer leerness-ai',' npx leerness library build .harness/library/coupang-order-sync',' npx leerness library publish .harness/library/coupang-order-sync/dist/coupang-order-sync --target npm --execute',''].join('\n')); }
451
+ function help(){ log(['Leerness v'+VERSION,'','Usage:',' leerness init [path] [--yes] [--skills office,commerce-api|recommended|all]',' leerness migrate [path] [--dry-run]',' leerness status [path]',' leerness verify [path]','','Skills:',' leerness skill list',' leerness skill add <name> [--path <project>]',' leerness skill remove <name> [--path <project>]',' leerness skill update <name> [--path <project>]',' leerness skill learn <name> --from <validated-skill-path> [--out <library-path>]','','Skill library lifecycle:',' leerness library guide [path]',' leerness library status <path>',' leerness library validate <path> [--strict-ai]',' leerness library verify <path> --ai --reviewer leerness-ai',' leerness library build <path> [--out ./dist] [--package leerness-skill-name]',' leerness library update <path> --from <validated-new-skill-path> [--version 1.2.0]',' leerness library merge <source-library> [--path <project>]',' leerness library migrate <path> [--version 1.0.0]',' leerness library publish <built-library> --target npm|git [--execute] [--repo https://github.com/gugu9999gu/leerness]',' leerness --version','','Examples:',' npx leerness init --skills recommended',' npx leerness skill learn coupang-order-sync --from .harness/skills/commerce-api/order-sync.md',' npx leerness library verify .harness/library/coupang-order-sync --ai --reviewer leerness-ai',' npx leerness library build .harness/library/coupang-order-sync',' npx leerness library publish .harness/library/coupang-order-sync/dist/coupang-order-sync --target npm --execute',''].join('\n')); }
367
452
 
368
453
  async function main(){ const parsed=parseArgs(process.argv.slice(2)); const args=parsed.positionals; const flags=parsed.flags; if(flags.version||flags.v){ log(VERSION); return; } if(flags.help||flags.h){ help(); return; } const cmd=args[0]||'init'; if(cmd==='init') return init(args[1]||process.cwd(),flags); if(cmd==='migrate') return migrate(args[1]||process.cwd(),flags); if(cmd==='status') return status(args[1]||process.cwd()); if(cmd==='verify') return verify(args[1]||process.cwd()); if(cmd==='skill') return skillCommand(args,flags); if(cmd==='library') return libraryCommand(args,flags); help(); process.exitCode=1; }
369
454
  main().catch(err=>{ fail(err.stack||err.message); process.exit(1); });
@@ -1,156 +1,58 @@
1
1
  # Leerness AX Skill Library Guide
2
2
 
3
- AX는 AI eXperience 약자입니다. 이 문서는 AI 에이전트가 검증된 스킬 데이터를 안전하게 학습, 검증, 빌드, 업로드, 업데이트, 병합, 마이그레이션할 수 있도록 만든 실행 가이드입니다.
3
+ AX는 AI eXperience입니다. 이 문서는 AI 에이전트가 검증된 스킬 데이터를 안전하게 학습, 검증, 빌드, 업로드, 업데이트, 병합, 마이그레이션하도록 안내합니다.
4
4
 
5
- ## 목표
5
+ ## 스킬 표시 기준
6
6
 
7
- - 성공한 구현 방식을 재사용 가능한 스킬로 축적한다.
8
- - 민감정보는 절대 스킬에 저장하지 않는다.
9
- - 스킬 업로드는 AI 검증 게이트를 통과한 경우에만 허용한다.
10
- - 각 스킬의 최종 업데이트 날짜, 버전, 검증 상태를 명확히 표시한다.
11
- - downstream 프로젝트에 병합할 때 `.harness/skills-lock.json`에 출처와 상태를 기록한다.
7
+ `leerness skill list`는 아래 정보를 표시합니다.
12
8
 
13
- ## 전체 흐름
9
+ - 스킬 영문 id
10
+ - 한글명 `displayNameKo`
11
+ - 가능한 작업 `capabilities`
12
+ - 최종 업데이트일 `lastUpdated`
13
+ - AI 검증 상태 `verification.status`
14
+ - 필요한 환경변수 이름 `requiresEnv`
14
15
 
15
- ```text
16
- 검증된 프로젝트 작업
17
- -> leerness skill learn
18
- -> leerness library validate
19
- -> leerness library verify --ai
20
- -> leerness library build
21
- -> leerness library publish --target npm|git --execute
22
- -> 다른 프로젝트에서 library merge/update/migrate
23
- ```
24
-
25
- ## 스킬에 들어갈 수 있는 데이터
26
-
27
- 허용:
28
-
29
- ```text
30
- 반복 가능한 절차
31
- 성공한 구현 순서
32
- 검증 방법
33
- 실패 대응법
34
- 환경변수 이름
35
- 파일 구조
36
- 의사코드와 템플릿
37
- ```
38
-
39
- 금지:
40
-
41
- ```text
42
- 실제 토큰
43
- 실제 쿠키
44
- 비밀번호
45
- 운영 고객 데이터
46
- 개인정보가 포함된 API 응답 원문
47
- 비공개 인증 헤더
48
- ```
49
-
50
- ## 필수 메타데이터
51
-
52
- ```json
53
- {
54
- "name": "commerce-api-coupang",
55
- "version": "1.0.0",
56
- "title": "Coupang Commerce API Skill",
57
- "category": "commerce-api",
58
- "description": "쿠팡 커머스 API 연동을 위한 재사용 가능한 구현 가이드",
59
- "lastUpdated": "2026-04-28",
60
- "lastUpdatedAt": "2026-04-28T00:00:00.000Z",
61
- "requiresEnv": [
62
- "COUPANG_ACCESS_KEY",
63
- "COUPANG_SECRET_KEY"
64
- ],
65
- "sensitiveDataPolicy": "env-reference-only",
66
- "verification": {
67
- "status": "passed",
68
- "method": "ai-assisted-review",
69
- "verifiedBy": "leerness-ai",
70
- "verifiedAt": "2026-04-28T00:00:00.000Z",
71
- "checks": [
72
- "structure",
73
- "secret-scan",
74
- "env-reference-only",
75
- "reusability",
76
- "migration-readiness"
77
- ]
78
- }
79
- }
80
- ```
16
+ ## AI 검증 업로드 표준 흐름
81
17
 
82
- ## 업로드 규칙
18
+ 1. 성공한 구현 패턴을 스킬 후보로 추출합니다.
19
+ 2. `leerness skill learn <name> --from <path>`로 라이브러리 구조를 만듭니다.
20
+ 3. `skill-library.json`에 한글명, 가능한 작업, 최종 업데이트일을 기록합니다.
21
+ 4. 실제 토큰/쿠키/비밀번호/고객 데이터가 없는지 검사합니다.
22
+ 5. `leerness library verify <path> --ai --reviewer leerness-ai`로 AI 검증 메타데이터를 기록합니다.
23
+ 6. `leerness library validate <path> --strict-ai`로 업로드 게이트를 통과합니다.
24
+ 7. `leerness library build <path>`로 배포 구조를 만듭니다.
25
+ 8. `leerness library publish <built-path> --target npm|git`으로 dry-run을 확인합니다.
26
+ 9. 사용자가 승인한 경우에만 `--execute`로 실제 업로드합니다.
83
27
 
84
- 업로드 전:
28
+ ## 설치 가능한 운영 스킬
85
29
 
86
30
  ```bash
87
- leerness library validate ./my-skill --strict-ai
88
- leerness library verify ./my-skill --ai --reviewer leerness-ai
89
- leerness library build ./my-skill
31
+ leerness skill add ai-verified-skill-publisher
90
32
  ```
91
33
 
92
- npm 업로드:
34
+ 스킬은 AI가 검증된 스킬을 업로드·업데이트·병합·마이그레이션할 때 따라야 할 절차를 프로젝트에 설치합니다.
93
35
 
94
- ```bash
95
- leerness library publish ./my-skill/dist/my-skill --target npm --execute
96
- ```
97
-
98
- git 업로드:
36
+ ## 업로드 인증 토큰 규칙
99
37
 
100
- ```bash
101
- leerness library publish ./my-skill/dist/my-skill --target git --repo https://github.com/USER/leerness-skill-name.git --execute
102
- ```
38
+ 검증된 스킬팩을 실제 업로드할 때는 아래 순서로 access token을 확인합니다.
103
39
 
104
- `--execute`가 없으면 dry-run입니다. AI 검증 메타데이터가 없거나 `needs-review` 상태이면 업로드가 차단됩니다.
40
+ 1. `--token-env <ENV_NAME>`으로 지정한 환경변수
41
+ 2. npm: `LEERNESS_NPM_TOKEN`, `NPM_TOKEN`, `NODE_AUTH_TOKEN`
42
+ 3. Git/GitHub: `LEERNESS_GIT_TOKEN`, `LEERNESS_GITHUB_TOKEN`, `GITHUB_TOKEN`, `GH_TOKEN`
43
+ 4. `.harness/skill-publish.local.json`의 `publishAuth.npmTokenEnv`, `publishAuth.gitTokenEnv`, `publishAuth.gitRemoteUrl`
44
+ 5. 위 값이 없으면 AI/사용자에게 토큰 입력을 요구
105
45
 
106
- ## 업데이트 규칙
107
-
108
- ```bash
109
- leerness library update ./my-skill --from ./validated-new-skill --version 1.1.0
110
- ```
111
-
112
- 업데이트 후에는 검증 상태가 `needs-review`로 돌아갑니다. 다시 AI 검증을 통과해야 업로드할 수 있습니다.
113
-
114
- ## 병합 규칙
115
-
116
- ```bash
117
- leerness library merge ./dist/my-skill --path ./target-project
118
- ```
119
-
120
- 병합 결과는 아래에 기록됩니다.
121
-
122
- ```text
123
- .harness/skills-lock.json
124
- ```
125
-
126
- 기록 항목:
46
+ 권장 로컬 설정:
127
47
 
128
48
  ```json
129
49
  {
130
- "name": "commerce-api-coupang",
131
- "version": "1.0.0",
132
- "source": "local-or-package",
133
- "lastUpdated": "2026-04-28",
134
- "verificationStatus": "passed"
50
+ "publishAuth": {
51
+ "npmTokenEnv": "LEERNESS_NPM_TOKEN",
52
+ "gitTokenEnv": "LEERNESS_GITHUB_TOKEN",
53
+ "gitRemoteUrl": "https://github.com/gugu9999gu/leerness"
54
+ }
135
55
  }
136
56
  ```
137
57
 
138
- ## 마이그레이션 규칙
139
-
140
- ```bash
141
- leerness library migrate ./old-skill-folder --version 1.0.0
142
- ```
143
-
144
- 마이그레이션은 기존 스킬 폴더를 표준 메타데이터 구조로 정규화합니다. 마이그레이션된 스킬은 자동으로 `needs-review`가 되며, 업로드 전 다시 검증해야 합니다.
145
-
146
- ## AI 에이전트용 체크리스트
147
-
148
- - [ ] 스킬 목적과 사용 조건이 명확한가
149
- - [ ] 구현 절차가 재현 가능한가
150
- - [ ] 실제 비밀값이 없는가
151
- - [ ] 환경변수 이름만 기록했는가
152
- - [ ] lastUpdated, lastUpdatedAt이 있는가
153
- - [ ] verification.status가 passed인가
154
- - [ ] 업데이트 후 재검증이 필요한 상태를 표시했는가
155
- - [ ] 병합 시 skills-lock에 기록되는가
156
- - [ ] 업로드는 dry-run 후 --execute로만 수행되는가
58
+ 실제 토큰값은 저장하지 말고 환경변수나 Secret Manager에 둡니다.
package/harness.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.0.0",
4
- "description": "Leerness: AI 에이전트가 대규모 프로젝트에서도 맥락, 규율, 디자인/기능 일관성, 검증된 스킬 라이브러리를 유지하도록 돕는 AX 최적화 개발 하네스.",
3
+ "version": "1.2.0",
4
+ "description": "Leerness: 검증된 스킬팩의 npm/git 업로드 토큰 게이트, 한글 스킬 라이브러리 표시, AI 검증 업로드 스킬을 지원하는 AX 최적화 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
7
7
  "ai",
@@ -13,7 +13,11 @@
13
13
  "ai-verified-skill",
14
14
  "ax-guideline",
15
15
  "skill-migration",
16
- "developer-tools"
16
+ "developer-tools",
17
+ "korean-skill-library",
18
+ "ai-skill-publisher",
19
+ "verified-skill-upload",
20
+ "skill-capabilities"
17
21
  ],
18
22
  "bin": {
19
23
  "leerness": "./bin/harness.js"
@@ -27,7 +31,7 @@
27
31
  "LICENSE"
28
32
  ],
29
33
  "scripts": {
30
- "test": "node ./bin/harness.js --help && node ./bin/harness.js init --yes --skills office,commerce-api ./tmp-harness-test && node ./bin/harness.js status ./tmp-harness-test && node ./bin/harness.js skill list && node ./bin/harness.js library guide ./tmp-harness-test && node ./bin/harness.js verify ./tmp-harness-test",
34
+ "test": "node ./bin/harness.js --help && node ./bin/harness.js init --yes --skills office,commerce-api,ai-verified-skill-publisher ./tmp-harness-test && node ./bin/harness.js status ./tmp-harness-test && node ./bin/harness.js skill list && node ./bin/harness.js skill info ai-verified-skill-publisher && node ./bin/harness.js library guide ./tmp-harness-test && node ./bin/harness.js verify ./tmp-harness-test",
31
35
  "prepack": "node ./bin/harness.js --version"
32
36
  },
33
37
  "repository": {
@@ -4,7 +4,7 @@
4
4
  "title": "Ads and Analytics Skill Library",
5
5
  "category": "marketing",
6
6
  "description": "GA4, 광고 전환, ROAS, attribution 점검을 위한 스킬입니다.",
7
- "compatibleHarness": ">=3.1.0",
7
+ "compatibleHarness": ">=1.1.0",
8
8
  "sensitiveDataPolicy": "env-reference-only",
9
9
  "requiresEnv": [
10
10
  "GA4_PROPERTY_ID"
@@ -27,5 +27,13 @@
27
27
  "env-reference-only",
28
28
  "reusability"
29
29
  ]
30
- }
30
+ },
31
+ "displayNameKo": "광고·GA4 분석 스킬 라이브러리",
32
+ "categoryKo": "마케팅/분석",
33
+ "capabilities": [
34
+ "GA4 이벤트·전환 디버깅",
35
+ "광고 ROAS/전환매출 차이 원인 분석",
36
+ "어트리뷰션/세션 소스 점검",
37
+ "광고 리포트 해석 체크리스트"
38
+ ]
31
39
  }
@@ -0,0 +1,24 @@
1
+ # AI 검증 스킬 업로드·라이브러리화 스킬
2
+
3
+ AI 에이전트가 검증된 스킬 데이터를 안전하게 라이브러리화하고 npm 또는 git으로 업로드할 때 사용하는 운영 절차입니다.
4
+
5
+ ## 가능한 작업
6
+
7
+ - 성공한 구현 패턴을 스킬 후보로 추출
8
+ - 스킬 메타데이터에 한글명, 가능한 작업, 최종 업데이트일, 검증 상태 기록
9
+ - 민감정보를 제거하고 환경변수 이름만 남김
10
+ - AI 검증 메타데이터를 생성하고 strict-ai 검증 통과
11
+ - npm/git access token을 환경변수 또는 로컬 설정에서 확인
12
+ - 토큰이 없으면 실제 업로드 직전에 입력 요구
13
+ - npm/git 업로드 전 dry-run 확인
14
+ - 업데이트, 병합, 마이그레이션 후 재검증
15
+
16
+ ## 인증 우선순위
17
+
18
+ 1. `--token-env <ENV_NAME>`
19
+ 2. `LEERNESS_NPM_TOKEN`, `NPM_TOKEN`, `NODE_AUTH_TOKEN`
20
+ 3. `LEERNESS_GIT_TOKEN`, `LEERNESS_GITHUB_TOKEN`, `GITHUB_TOKEN`, `GH_TOKEN`
21
+ 4. `.harness/skill-publish.local.json`
22
+ 5. 없으면 프롬프트 입력
23
+
24
+ Git 기본 저장소 경로는 `https://github.com/gugu9999gu/leerness`입니다.
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "ai-verified-skill-publisher",
3
+ "version": "1.1.0",
4
+ "title": "AI Verified Skill Publisher Skill Library",
5
+ "displayNameKo": "AI 검증 스킬 업로드·라이브러리화 스킬",
6
+ "category": "skill-operations",
7
+ "categoryKo": "스킬 운영/배포",
8
+ "description": "AI가 검증된 스킬 데이터를 표준 포맷으로 정리하고, 민감정보를 제거한 뒤 npm/git 라이브러리로 업로드·업데이트·병합·마이그레이션할 때 따르는 운영 스킬입니다.",
9
+ "compatibleHarness": ">=1.2.0",
10
+ "sensitiveDataPolicy": "env-reference-only",
11
+ "requiresEnv": [
12
+ "LEERNESS_NPM_TOKEN",
13
+ "NPM_TOKEN",
14
+ "NODE_AUTH_TOKEN",
15
+ "LEERNESS_GIT_TOKEN",
16
+ "LEERNESS_GITHUB_TOKEN",
17
+ "GITHUB_TOKEN",
18
+ "GH_TOKEN",
19
+ "GIT_REMOTE_URL"
20
+ ],
21
+ "capabilities": [
22
+ "검증된 구현 패턴을 스킬 후보로 추출",
23
+ "스킬 메타데이터에 한글명·가능 작업·최종 업데이트일 기록",
24
+ "민감정보 스캔 후 환경변수 이름만 남기기",
25
+ "AI 검증 메타데이터 생성 및 strict-ai 검증",
26
+ "npm/git 업로드 dry-run과 --execute 승인 흐름 수행",
27
+ "기존 스킬 라이브러리 업데이트·병합·마이그레이션 절차화",
28
+ "npm/git access token 존재 여부 확인 후 없으면 업로드 직전 입력 요구",
29
+ "프로젝트 로컬 설정 .harness/skill-publish.local.json 기준으로 배포 인증 소스 연결",
30
+ "Git 기본 저장소 경로 https://github.com/gugu9999gu/leerness 기준 업로드 흐름 안내"
31
+ ],
32
+ "files": [
33
+ "README.md",
34
+ "skills/ai-verified-skill-upload.md",
35
+ "skills/skill-metadata-standard.md",
36
+ "skills/skill-library-release-flow.md",
37
+ "skills/publish-auth-token-gate.md"
38
+ ],
39
+ "lastUpdated": "2026-04-28",
40
+ "lastUpdatedAt": "2026-04-28T00:00:00.000Z",
41
+ "verification": {
42
+ "status": "passed",
43
+ "method": "bundled-ai-review",
44
+ "verifiedBy": "leerness-ai",
45
+ "verifiedAt": "2026-04-28T00:00:00.000Z",
46
+ "checks": [
47
+ "structure",
48
+ "secret-scan",
49
+ "env-reference-only",
50
+ "ai-publish-gate",
51
+ "metadata-display",
52
+ "publish-auth-token-gate",
53
+ "git-repository-url"
54
+ ]
55
+ }
56
+ }
@@ -0,0 +1,16 @@
1
+ # Skill: AI 검증 스킬 업로드
2
+
3
+ ## 목적
4
+ 검증된 스킬 데이터를 npm 또는 git 라이브러리로 업로드한다.
5
+
6
+ ## 절차
7
+ 1. `leerness library validate <path> --strict-ai`로 구조와 민감정보를 검사한다.
8
+ 2. `leerness library verify <path> --ai --reviewer leerness-ai`로 AI 검증 메타데이터를 기록한다.
9
+ 3. `leerness library build <path>`로 배포본을 만든다.
10
+ 4. `leerness library publish <built-path> --target npm|git`로 dry-run을 확인한다.
11
+ 5. 토큰 소스를 확인한다.
12
+ 6. `--execute`로 실제 업로드한다.
13
+
14
+ ## 금지
15
+ - 실제 토큰을 스킬 문서에 저장하지 않는다.
16
+ - 검증 상태가 `passed`가 아닌 스킬은 업로드하지 않는다.
@@ -0,0 +1,30 @@
1
+ # Skill: 업로드 인증 토큰 게이트
2
+
3
+ ## npm 토큰 소스
4
+ - `--token-env <ENV_NAME>`
5
+ - `LEERNESS_NPM_TOKEN`
6
+ - `NPM_TOKEN`
7
+ - `NODE_AUTH_TOKEN`
8
+ - `.harness/skill-publish.local.json`의 `publishAuth.npmTokenEnv`
9
+
10
+ ## Git/GitHub 토큰 소스
11
+ - `--token-env <ENV_NAME>`
12
+ - `LEERNESS_GIT_TOKEN`
13
+ - `LEERNESS_GITHUB_TOKEN`
14
+ - `GITHUB_TOKEN`
15
+ - `GH_TOKEN`
16
+ - `.harness/skill-publish.local.json`의 `publishAuth.gitTokenEnv`
17
+
18
+ ## 로컬 설정 예시
19
+
20
+ ```json
21
+ {
22
+ "publishAuth": {
23
+ "npmTokenEnv": "LEERNESS_NPM_TOKEN",
24
+ "gitTokenEnv": "LEERNESS_GITHUB_TOKEN",
25
+ "gitRemoteUrl": "https://github.com/gugu9999gu/leerness"
26
+ }
27
+ }
28
+ ```
29
+
30
+ 실제 토큰값은 환경변수 또는 Secret Manager에 둔다.
@@ -0,0 +1,10 @@
1
+ # Skill: 스킬 라이브러리 릴리즈 흐름
2
+
3
+ 1. learn
4
+ 2. validate
5
+ 3. verify
6
+ 4. build
7
+ 5. publish dry-run
8
+ 6. token gate
9
+ 7. publish --execute
10
+ 8. status 기록
@@ -0,0 +1,5 @@
1
+ # Skill: 스킬 메타데이터 표준
2
+
3
+ 각 스킬은 `name`, `displayNameKo`, `capabilities`, `lastUpdated`, `lastUpdatedAt`, `verification`을 포함한다.
4
+
5
+ `capabilities`에는 AI가 수행 가능한 작업을 한국어로 명확히 적는다.
@@ -4,7 +4,7 @@
4
4
  "title": "App Store Review Skill Library",
5
5
  "category": "mobile",
6
6
  "description": "App Store 심사 대응, 개인정보 라벨, 웹뷰 앱 검토를 위한 스킬입니다.",
7
- "compatibleHarness": ">=3.1.0",
7
+ "compatibleHarness": ">=1.1.0",
8
8
  "sensitiveDataPolicy": "env-reference-only",
9
9
  "requiresEnv": [],
10
10
  "files": [
@@ -25,5 +25,13 @@
25
25
  "env-reference-only",
26
26
  "reusability"
27
27
  ]
28
- }
28
+ },
29
+ "displayNameKo": "앱스토어 심사 대응 스킬 라이브러리",
30
+ "categoryKo": "모바일/심사",
31
+ "capabilities": [
32
+ "App Store 심사 답변 초안 작성",
33
+ "개인정보 라벨·수집 목적 점검",
34
+ "웹뷰 앱 심사 이슈 대응",
35
+ "버그픽스 제출/리젝 사유 정리"
36
+ ]
29
37
  }
@@ -4,7 +4,7 @@
4
4
  "title": "Commerce API Integration Skill Library",
5
5
  "category": "commerce",
6
6
  "description": "쿠팡, 롯데온, 스마트스토어 등 커머스 API 연동 패턴과 보안 규칙을 담은 스킬 모음입니다.",
7
- "compatibleHarness": ">=3.1.0",
7
+ "compatibleHarness": ">=1.1.0",
8
8
  "sensitiveDataPolicy": "env-reference-only",
9
9
  "requiresEnv": [
10
10
  "COMMERCE_API_BASE_URL",
@@ -36,5 +36,13 @@
36
36
  "env-reference-only",
37
37
  "reusability"
38
38
  ]
39
- }
39
+ },
40
+ "displayNameKo": "커머스 API 연동 스킬 라이브러리",
41
+ "categoryKo": "커머스/외부 API",
42
+ "capabilities": [
43
+ "쿠팡·롯데온·스마트스토어 API 인증 구조 설계",
44
+ "주문/상품/매출 동기화 플로우 구현",
45
+ "환경변수 기반 민감정보 분리",
46
+ "API 오류·재시도·레이트리밋 대응"
47
+ ]
40
48
  }
@@ -4,7 +4,7 @@
4
4
  "title": "Crawling and Browser Automation Skill Library",
5
5
  "category": "automation",
6
6
  "description": "API가 없거나 제한적일 때 브라우저 자동화와 다운로드 자동화를 안전하게 구현하기 위한 스킬입니다.",
7
- "compatibleHarness": ">=3.1.0",
7
+ "compatibleHarness": ">=1.1.0",
8
8
  "sensitiveDataPolicy": "env-reference-only",
9
9
  "requiresEnv": [
10
10
  "CRAWLER_USER_ID",
@@ -28,5 +28,13 @@
28
28
  "env-reference-only",
29
29
  "reusability"
30
30
  ]
31
- }
31
+ },
32
+ "displayNameKo": "크롤링·브라우저 자동화 스킬 라이브러리",
33
+ "categoryKo": "자동화/수집",
34
+ "capabilities": [
35
+ "Playwright 기반 브라우저 자동화",
36
+ "로그인 세션·다운로드 자동화 설계",
37
+ "클라우드 서버 실행 제약 점검",
38
+ "CAPTCHA/2FA/이용약관 리스크 분리"
39
+ ]
32
40
  }
@@ -4,7 +4,7 @@
4
4
  "title": "Firebase and Cloud Functions Skill Library",
5
5
  "category": "backend",
6
6
  "description": "Firebase Functions, Firestore, Hosting, Cloud Run 연동 개발을 위한 스킬입니다.",
7
- "compatibleHarness": ">=3.1.0",
7
+ "compatibleHarness": ">=1.1.0",
8
8
  "sensitiveDataPolicy": "env-reference-only",
9
9
  "requiresEnv": [
10
10
  "FIREBASE_PROJECT_ID"
@@ -27,5 +27,13 @@
27
27
  "env-reference-only",
28
28
  "reusability"
29
29
  ]
30
- }
30
+ },
31
+ "displayNameKo": "Firebase·Cloud Functions 스킬 라이브러리",
32
+ "categoryKo": "백엔드/클라우드",
33
+ "capabilities": [
34
+ "Firebase Functions 배포 오류 진단",
35
+ "Firestore 인덱스·규칙 점검",
36
+ "Secret Manager/환경변수 분리",
37
+ "Cloud Run/Hosting 연동 검증"
38
+ ]
31
39
  }
@@ -4,7 +4,7 @@
4
4
  "title": "Microsoft Office Automation Skill Library",
5
5
  "category": "productivity",
6
6
  "description": "Excel, Word, PowerPoint 문서 자동화와 템플릿 기반 산출물 생성을 위한 스킬 모음입니다.",
7
- "compatibleHarness": ">=3.1.0",
7
+ "compatibleHarness": ">=1.1.0",
8
8
  "sensitiveDataPolicy": "env-reference-only",
9
9
  "requiresEnv": [],
10
10
  "files": [
@@ -25,5 +25,13 @@
25
25
  "env-reference-only",
26
26
  "reusability"
27
27
  ]
28
- }
28
+ },
29
+ "displayNameKo": "오피스 자동화 스킬 라이브러리",
30
+ "categoryKo": "생산성/문서 자동화",
31
+ "capabilities": [
32
+ "엑셀 데이터 정리·서식·검증 자동화",
33
+ "워드/문서 템플릿 기반 산출물 생성",
34
+ "파워포인트 발표자료 구조화·생성",
35
+ "문서 산출물 검증 체크리스트 작성"
36
+ ]
29
37
  }