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 +56 -2
- package/bin/harness.js +106 -21
- package/docs/AX_SKILL_LIBRARY_GUIDE.md +36 -134
- package/harness.js +0 -0
- package/package.json +8 -4
- package/skill-packs/ads-analytics/skill.json +10 -2
- package/skill-packs/ai-verified-skill-publisher/README.md +24 -0
- package/skill-packs/ai-verified-skill-publisher/skill.json +56 -0
- package/skill-packs/ai-verified-skill-publisher/skills/ai-verified-skill-upload.md +16 -0
- package/skill-packs/ai-verified-skill-publisher/skills/publish-auth-token-gate.md +30 -0
- package/skill-packs/ai-verified-skill-publisher/skills/skill-library-release-flow.md +10 -0
- package/skill-packs/ai-verified-skill-publisher/skills/skill-metadata-standard.md +5 -0
- package/skill-packs/appstore-review/skill.json +10 -2
- package/skill-packs/commerce-api/skill.json +10 -2
- package/skill-packs/crawling/skill.json +10 -2
- package/skill-packs/firebase/skill.json +10 -2
- package/skill-packs/office/skill.json +10 -2
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.
|
|
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.
|
|
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
|
|
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 검증된 스킬만 업로드할 수 있습니다.
|
|
315
|
-
if(target==='npm'){
|
|
316
|
-
|
|
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.
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
34
|
+
이 스킬은 AI가 검증된 스킬을 업로드·업데이트·병합·마이그레이션할 때 따라야 할 절차를 프로젝트에 설치합니다.
|
|
93
35
|
|
|
94
|
-
|
|
95
|
-
leerness library publish ./my-skill/dist/my-skill --target npm --execute
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
git 업로드:
|
|
36
|
+
## 업로드 인증 토큰 규칙
|
|
99
37
|
|
|
100
|
-
|
|
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
|
-
`--
|
|
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
|
-
"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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.
|
|
4
|
-
"description": "Leerness:
|
|
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": ">=
|
|
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에 둔다.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"title": "App Store Review Skill Library",
|
|
5
5
|
"category": "mobile",
|
|
6
6
|
"description": "App Store 심사 대응, 개인정보 라벨, 웹뷰 앱 검토를 위한 스킬입니다.",
|
|
7
|
-
"compatibleHarness": ">=
|
|
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": ">=
|
|
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": ">=
|
|
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": ">=
|
|
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": ">=
|
|
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
|
}
|