leerness 1.3.0 → 1.3.1
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/bin/harness.js +185 -439
- package/package.json +1 -1
package/bin/harness.js
CHANGED
|
@@ -5,9 +5,8 @@ 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');
|
|
9
8
|
|
|
10
|
-
const VERSION = '1.3.
|
|
9
|
+
const VERSION = '1.3.1';
|
|
11
10
|
const MARK = '<!-- leerness:managed -->';
|
|
12
11
|
const MIGRATED = '<!-- leerness:migrated-legacy -->';
|
|
13
12
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
@@ -17,103 +16,6 @@ const c = { reset:'\x1b[0m', bold:'\x1b[1m', dim:'\x1b[2m', green:'\x1b[32m', ye
|
|
|
17
16
|
|
|
18
17
|
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'];
|
|
19
18
|
|
|
20
|
-
const coreFiles = {
|
|
21
|
-
'AGENTS.md': [MARK,'# {{PROJECT}} AI Agent Harness','','Agent = Model + Leerness Harness.','','## Core Rule','Before editing, route the task. Read `.harness/context-routing.md` and use `leerness route <task-type>` when the task type is unclear.','','## Universal Read Order','1. .harness/project-brief.md','2. .harness/current-state.md','3. .harness/context-routing.md','4. .harness/writeback-policy.md','5. .harness/task-type-map.md','6. .harness/context-map.md','7. .harness/guardrails.md','8. .harness/skills-lock.json','','## Task Routing','- Feature/API work: read architecture.md, feature-contracts.md, context-map.md, skills/feature-implementation.md.','- UI/design work: read design-system.md, feature-contracts.md, skills/ui-consistency.md.','- Debugging: read task-log.md, current-state.md, skills/debugging.md, related feature contract.','- Refactoring: read architecture.md, decisions.md, guardrails.md, skills/refactoring.md.','- Release/deploy: read release-checklist.md, testing-strategy.md, current-state.md, decisions.md.','- Documentation/context work: read writeback-policy.md and update the correct memory files.','- Skill/library work: read AX_SKILL_LIBRARY_GUIDE.md and skills/ai-verified-skill-publisher when installed.','','## Writeback Rules','- Always update current-state.md, task-log.md, and session-handoff.md after meaningful work.','- Update decisions.md when a structural, technology, API, schema, deployment, or irreversible decision is made.','- Update feature-contracts.md when input/output/state/error behavior changes.','- Update design-system.md when UI rules, components, layout, spacing, or states change.','- Update release-checklist.md when deployment, environment variables, rollback, CI, npm, or git release requirements change.','- Update context-map.md when important files, modules, routes, commands, or ownership areas change.','- Update project-brief.md only when product purpose, target users, success criteria, or project direction changes.','','## Operating Rules','- Preserve existing architecture, feature contracts, and design system unless the user explicitly asks to change them.','- Use the matching skill from .harness/skills when a task fits a known pattern.','- Keep secrets, tokens, cookies, credentials, and customer private data out of harness files.','- Record variable names only; store real values in .env.local, CI secrets, or cloud secret manager.','','## Response Contract','- Task type and files consulted','- Summary','- Files changed','- Verification','- Memory files updated','- Risks or assumptions','- Next step','{{LEGACY_AGENT}}',''].join('\n'),
|
|
22
|
-
'CLAUDE.md': [MARK,'# Claude Code Instructions','','Use AGENTS.md as the source of truth. Route every task through .harness/context-routing.md and .harness/task-type-map.md. Before editing, read current-state.md, writeback-policy.md, context-map.md, guardrails.md, skills-lock.json, and the matching skill file.',''].join('\n'),
|
|
23
|
-
'.cursor/rules/leerness.mdc': [MARK,'---','alwaysApply: true','---','Read AGENTS.md first. Follow .harness/context-routing.md, writeback-policy.md, installed skills, design-system, feature-contracts, and guardrails.',''].join('\n'),
|
|
24
|
-
'.github/copilot-instructions.md': [MARK,'# GitHub Copilot Instructions','','Use AGENTS.md and .harness/ as the project memory. Route the task with context-routing.md, then preserve architecture, feature contracts, security rules, and UI consistency.',''].join('\n'),
|
|
25
|
-
'.gitignore': ['# Leerness local secrets','.env','.env.local','*.secret.json','.harness/skill-config.local.json','.harness/skill-publish.local.json',''].join('\n'),
|
|
26
|
-
'.env.example': ['# Leerness environment variable examples','# Copy to .env.local and fill values locally. Never commit real secrets.','',''].join('\n'),
|
|
27
|
-
'.harness/HARNESS_VERSION': '{{VERSION}}\n',
|
|
28
|
-
'.harness/manifest.json': '{{MANIFEST}}\n',
|
|
29
|
-
'.harness/skills-lock.json': '{{SKILLS_LOCK}}\n',
|
|
30
|
-
'.harness/skill-config.schema.json': [MARK,'{',' "$schema": "https://json-schema.org/draft/2020-12/schema",',' "title": "Leerness Skill Config",',' "type": "object",',' "properties": {',' "envSource": { "type": "string", "default": ".env.local" },',' "installedSkills": { "type": "object" }',' },',' "additionalProperties": true','}',''].join('\n'),
|
|
31
|
-
'.harness/secret-policy.md': [MARK,'# Secret Policy','','## Never store in harness files','- API keys','- Access tokens','- Refresh tokens','- Passwords','- Cookies','- Private customer data','- Payment credentials','','## Allowed in harness files','- Environment variable names','- Secret manager key names','- Redacted examples','- Fake fixtures','','## Default locations','- Local: .env.local','- CI/CD: GitHub Actions Secrets or provider secrets','- Cloud: Secret Manager or runtime environment variables',''].join('\n'),
|
|
32
|
-
'.harness/project-brief.md': [MARK,'---','leernessRole: project-brief','readWhen: [every-task, planning, product-direction, onboarding]','updateWhen: [purpose-change, user-change, success-criteria-change, product-direction-change]','doNotStore: [secrets, tokens, credentials, raw-customer-data]','---','','# Project Brief: {{PROJECT}}','','## When to read','Read this before meaningful work to understand why the project exists and what success means.','','## When to update','Update only when product purpose, target users, success criteria, scope, or strategic direction changes.','','## Purpose','','## Success Criteria','','## Users','','## Product Direction','{{LEGACY_BRIEF}}',''].join('\n'),
|
|
33
|
-
'.harness/current-state.md': [MARK,'---','leernessRole: current-state','readWhen: [every-task, resume-work, planning, debugging, release]','updateWhen: [after-meaningful-work, blocker-change, next-step-change, status-change]','doNotStore: [secrets, tokens, credentials]','---','','# Current State','','Updated: {{DATE}}','','## When to read','Read at the start of every session to know where the project currently stands.','','## When to update','Update after meaningful work, changed blockers, or changed next steps.','','## Now','- Leerness v{{VERSION}} installed or migrated.','','## Next','- Fill project-brief, context-map, design-system, and feature-contracts.','','## Blockers','- None recorded.','{{LEGACY_STATE}}',''].join('\n'),
|
|
34
|
-
'.harness/architecture.md': [MARK,'---','leernessRole: architecture','readWhen: [feature, refactor, integration, api, database, deployment]','updateWhen: [module-change, data-flow-change, integration-change, boundary-change]','doNotStore: [secrets, credentials]','---','','# Architecture','','## When to read','Read before structural changes, feature work touching multiple modules, integrations, API, database, or deployment changes.','','## When to update','Update when module boundaries, data flow, external services, or ownership changes.','','## Overview','','## Main Modules','','## Data Flow','','## External Services','','## Boundaries','{{LEGACY_ARCH}}',''].join('\n'),
|
|
35
|
-
'.harness/context-map.md': [MARK,'---','leernessRole: context-map','readWhen: [every-task, file-discovery, impact-analysis]','updateWhen: [new-important-file, moved-module, new-route, new-service, ownership-change]','doNotStore: [secrets, tokens]','---','','# Context Map','','## When to read','Read when deciding which files or modules are relevant to a task.','','## When to update','Update when important files, routes, modules, services, or ownership areas change.','','| Area | Files | Notes |','|---|---|---|','| UI | src/components/**, app/** | Check design-system.md first. |','| API | src/api/**, server/**, functions/** | Preserve response contracts. |','| Data | db/**, firestore/**, prisma/** | Confirm migrations. |','| Tests | test/**, tests/**, __tests__/** | Add or update checks. |',''].join('\n'),
|
|
36
|
-
'.harness/decisions.md': [MARK,'---','leernessRole: decisions','readWhen: [architecture, refactor, release, dependency-change, irreversible-change]','updateWhen: [important-decision, tradeoff, architecture-change, dependency-change, rollback-relevant-change]','doNotStore: [secrets, credentials]','---','','# Decision Log','','## When to read','Read before changing architecture, dependencies, deployment, data model, API contracts, or irreversible behavior.','','## When to update','Update when a decision affects future work or explains why an approach was chosen.','','## Template','','### YYYY-MM-DD — Title','- Decision:','- Reason:','- Alternatives:','- Impact:','{{LEGACY_DECISIONS}}',''].join('\n'),
|
|
37
|
-
'.harness/task-log.md': [MARK,'---','leernessRole: task-log','readWhen: [debugging, regression, audit, resume-work]','updateWhen: [after-meaningful-work, bug-fix, release, migration, skill-learning]','doNotStore: [secrets, tokens, private-customer-data]','---','','# Task Log','','## When to read','Read to understand previous work, regressions, attempted fixes, and release/migration history.','','## When to update','Update after meaningful work with what changed and how it was verified.','','## {{DATE}}','- Installed Leerness v{{VERSION}}.',''].join('\n'),
|
|
38
|
-
'.harness/constraints.md': [MARK,'# Constraints','','- Runtime/framework constraints','- Deployment constraints','- Security/privacy constraints','- Business rules',''].join('\n'),
|
|
39
|
-
'.harness/guardrails.md': [MARK,'---','leernessRole: guardrails','readWhen: [every-task, security, auth, payment, db, refactor, release]','updateWhen: [new-risk, repeated-ai-mistake, policy-change, sensitive-area-change]','doNotStore: [secrets, tokens, credentials]','---','','# Guardrails','','## When to read','Read before every task, especially auth, payment, database, security, refactoring, release, or external integration work.','','## When to update','Update when a new risk, repeated mistake, project policy, or sensitive area rule is discovered.','','## Never','- Do not perform unrequested large rewrites.','- Do not change public API, database schema, auth, payment, or environment variable names without identifying impact.','- Do not hardcode secrets.','- Do not create a new design pattern when an existing one fits.','','## Always','- Inspect current structure first.','- Make the smallest safe change.','- Verify behavior.','- Update project memory after meaningful changes.',''].join('\n'),
|
|
40
|
-
'.harness/design-system.md': [MARK,'---','leernessRole: design-system','readWhen: [ui, ux, component, styling, layout, accessibility]','updateWhen: [new-ui-pattern, changed-component-rule, visual-standard-change, state-pattern-change]','doNotStore: [secrets]','---','','# Design System Memory','','## When to read','Read before UI, UX, component, layout, responsive, accessibility, or styling changes.','','## When to update','Update when reusable UI rules, component variants, spacing, states, or accessibility patterns change.','','## Layout','- Reuse existing spacing, component variants, typography, and breakpoints.','','## Components','| Component | Purpose | Rules |','|---|---|---|','| Button | Primary actions | Reuse existing variants. |','| Card | Grouped content | Keep spacing and radius consistent. |','| Form | Input flows | Include loading, error, and empty states. |',''].join('\n'),
|
|
41
|
-
'.harness/feature-contracts.md': [MARK,'---','leernessRole: feature-contracts','readWhen: [feature, api, ui-state, integration, debugging, regression]','updateWhen: [input-change, output-change, state-change, error-change, api-contract-change]','doNotStore: [secrets, raw-private-data]','---','','# Feature Contracts','','## When to read','Read before implementing or changing a feature, API, UI state, integration, or bug fix that touches behavior.','','## When to update','Update when inputs, outputs, states, errors, API contracts, or validation rules change.','','## Template','- Feature:','- Entry point:','- Input:','- Output:','- Error states:','- UI states:','- Related files:','- Tests:',''].join('\n'),
|
|
42
|
-
'.harness/testing-strategy.md': [MARK,'---','leernessRole: testing-strategy','readWhen: [feature, debugging, release, refactor, migration]','updateWhen: [new-test-pattern, changed-critical-flow, recurring-bug, release-risk]','doNotStore: [secrets, private-test-data]','---','','# Testing Strategy','','## When to read','Read before feature, bug fix, refactor, migration, or release work to choose verification scope.','','## When to update','Update when a new test pattern, critical flow, regression risk, or verification standard appears.','','- Unit: pure logic and adapters','- Integration: API, DB, third-party providers','- E2E/manual: key user flows','- Regression: previously fixed bugs and successful skills',''].join('\n'),
|
|
43
|
-
'.harness/review-checklist.md': [MARK,'---','leernessRole: review-checklist','readWhen: [before-final-answer, pull-request, code-review, handoff]','updateWhen: [new-review-risk, repeated-defect, quality-standard-change]','doNotStore: [secrets]','---','','# Review Checklist','','## When to read','Read before finalizing meaningful changes, PRs, reviews, or handoffs.','','## When to update','Update when recurring review failures or new quality gates are discovered.','','- [ ] 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'),
|
|
44
|
-
'.harness/release-checklist.md': [MARK,'---','leernessRole: release-checklist','readWhen: [release, deploy, npm-publish, git-push, ci-cd, env-change]','updateWhen: [deployment-failure, new-env-var, rollback-change, ci-change, publish-rule-change]','doNotStore: [access-tokens, passwords, cookies, private-keys]','---','','# Release Checklist','','## When to read','Read before deployment, npm publish, git push release, CI/CD change, environment variable change, or migration.','','## When to update','Update when a release fails, a new release condition appears, an env var changes, CI/CD changes, or rollback steps change.','','- [ ] Build/test passed','- [ ] Env variables confirmed','- [ ] Migration impact checked','- [ ] Rollback path known','- [ ] Release notes prepared',''].join('\n'),
|
|
45
|
-
'.harness/session-handoff.md': [MARK,'---','leernessRole: session-handoff','readWhen: [resume-work, new-session, handoff]','updateWhen: [end-of-session, interrupted-work, unresolved-risk, next-step-change]','doNotStore: [secrets, tokens]','---','','# Session Handoff','','## When to read','Read at the start of a new AI session or when continuing interrupted work.','','## When to update','Update at the end of meaningful work with the exact next step.','','## Done','-','','## Changed Files','-','','## Decisions','-','','## Risks','-','','## Next Exact Step','-',''].join('\n'),
|
|
46
|
-
'.harness/skill-index.md': [MARK,'---','leernessRole: skill-index','readWhen: [skill, automation, integration, repeated-task, library]','updateWhen: [skill-installed, skill-removed, skill-updated, new-skill-learned]','doNotStore: [secrets, tokens]','---','','# Skill Index','','Installed skill libraries are tracked in `.harness/skills-lock.json`.','','## When to read','Read when a task resembles a known repeated pattern or domain workflow.','','## When to update','Update when skills are installed, removed, updated, learned, or migrated.','','## Commands','`leerness skill list` — 한글명/가능 작업/업데이트/검증 상태 표시','`leerness skill info <name>`','`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, displayNameKo, capabilities, lastUpdated, lastUpdatedAt, and verification status.',''].join('\n'),
|
|
47
|
-
'.harness/context-routing.md': [MARK,'---','leernessRole: context-routing','readWhen: [every-task, task-classification, before-editing]','updateWhen: [new-task-type, changed-routing, new-required-file, repeated-missed-context]','doNotStore: [secrets, tokens]','---','','# Context Routing','','Use this file to decide which memory files to read before work and which files to update after work.','','## Universal baseline','','Read before every meaningful task:','- project-brief.md','- current-state.md','- context-routing.md','- writeback-policy.md','- task-type-map.md','- context-map.md','- guardrails.md','','## Task routes','','### feature','Read: architecture.md, feature-contracts.md, context-map.md, skills/feature-implementation.md, testing-strategy.md','Update: current-state.md, task-log.md, session-handoff.md, feature-contracts.md, context-map.md when file map changes','','### ui','Read: design-system.md, feature-contracts.md, context-map.md, skills/ui-consistency.md','Update: design-system.md, feature-contracts.md, current-state.md, task-log.md, session-handoff.md','','### debugging','Read: current-state.md, task-log.md, feature-contracts.md, testing-strategy.md, skills/debugging.md','Update: task-log.md, current-state.md, session-handoff.md, testing-strategy.md when regression coverage changes','','### refactor','Read: architecture.md, decisions.md, guardrails.md, testing-strategy.md, skills/refactoring.md','Update: architecture.md, decisions.md when reasoning matters, task-log.md, session-handoff.md','','### release','Read: release-checklist.md, testing-strategy.md, current-state.md, decisions.md, secret-policy.md','Update: release-checklist.md, task-log.md, current-state.md, session-handoff.md','','### migration','Read: AX_MIGRATION_GUIDE.md, architecture.md, decisions.md, release-checklist.md, testing-strategy.md','Update: current-state.md, task-log.md, session-handoff.md, context-map.md, release-checklist.md, decisions.md when needed','','### new-install','Read: AX_NEW_PROJECT_GUIDE.md, project-brief.md, context-map.md, guardrails.md','Update: project-brief.md, architecture.md, context-map.md, design-system.md, feature-contracts.md, current-state.md','','### skill-library','Read: AX_SKILL_LIBRARY_GUIDE.md, skill-index.md, skills-lock.json, secret-policy.md','Update: skill-index.md, skills-lock.json, task-log.md, session-handoff.md','',''].join('\n'),
|
|
48
|
-
'.harness/writeback-policy.md': [MARK,'---','leernessRole: writeback-policy','readWhen: [every-task, documentation, memory-update, handoff]','updateWhen: [new-memory-rule, repeated-missing-update, changed-document-role]','doNotStore: [secrets, tokens]','---','','# Writeback Policy','','This file defines where project knowledge must be recorded.','','| Information | Write to | Notes |','|---|---|---|','| Current progress, blockers, next step | current-state.md | Update after meaningful work. |','| Work performed and verification | task-log.md | Date-based factual log. |','| Next-session handoff | session-handoff.md | Keep exact next action. |','| Structural decisions and tradeoffs | decisions.md | Include reason and impact. |','| File/module map | context-map.md | Update when files/routes/services change. |','| Feature behavior | feature-contracts.md | Inputs, outputs, states, errors, tests. |','| UI/UX rules | design-system.md | Reusable component and state rules. |','| Release/env/rollback gates | release-checklist.md | Update after publish/deploy findings. |','| Testing patterns | testing-strategy.md | Regression and verification strategy. |','| Repeated successful workflows | skill-index.md and .harness/skills/ | Never include secret values. |','','## Do not record','- Actual access tokens, passwords, cookies, private keys, customer private data','- Raw production exports unless redacted and explicitly allowed','- Temporary guesses that were disproved; move those to task-log as failed attempts if useful','',''].join('\n'),
|
|
49
|
-
'.harness/task-type-map.md': [MARK,'---','leernessRole: task-type-map','readWhen: [every-task, task-classification, ambiguous-request]','updateWhen: [new-task-wording, new-domain-workflow, repeated-misclassification]','doNotStore: [secrets, tokens]','---','','# Task Type Map','','Map user requests to task types, then follow context-routing.md.','','| User request pattern | Task type | First files to consult |','|---|---|---|','| 새 기능, 구현, API 추가 | feature | feature-contracts.md, architecture.md |','| 디자인 맞춰줘, UI 수정, 반응형 | ui | design-system.md, feature-contracts.md |','| 오류, 에러, 안 됨, 원인 | debugging | task-log.md, current-state.md, debugging skill |','| 구조 개선, 리팩토링 | refactor | architecture.md, decisions.md, guardrails.md |','| 배포, npm publish, git push, 릴리즈 | release | release-checklist.md, testing-strategy.md |','| 구버전 하네스 반영, 이전 파일 정리 | migration | AX_MIGRATION_GUIDE.md, context-routing.md |','| 처음 설치, 기존 프로젝트 반영 | new-install | AX_NEW_PROJECT_GUIDE.md, project-brief.md |','| 스킬로 저장, 스킬 업로드, 라이브러리화 | skill-library | AX_SKILL_LIBRARY_GUIDE.md, skill-index.md |','| 문서 갱신, 맥락 정리 | documentation | writeback-policy.md, context-routing.md |','',''].join('\n'),
|
|
50
|
-
'.harness/AX_MIGRATION_GUIDE.md': [MARK,'---','leernessRole: ax-migration-guide','readWhen: [migration, legacy-harness, old-version, upgrade]','updateWhen: [migration-rule-change, legacy-pattern-change, failed-migration]','doNotStore: [secrets, tokens, credentials]','---','','# AX Migration Guide for AI Agents','','Use this guide when upgrading an existing project that already has AGENTS.md, CLAUDE.md, .harness/, docs/guideline.md, old project-harness files, or older Leerness output.','','## Goal','Migrate without losing project memory. Preserve useful legacy content, remove ambiguity about source-of-truth files, and create routing/writeback rules so future work references the right files.','','## Required steps','1. Run `leerness migrate --dry-run` and inspect detected legacy files.','2. Back up existing harness files under `.harness/archive/legacy-migration-*`.','3. Generate or update context-routing.md, writeback-policy.md, task-type-map.md, AGENTS.md, and AX guides.','4. Move useful old content into the correct files: project purpose to project-brief.md, architecture to architecture.md, task history to task-log.md/current-state.md, release notes to release-checklist.md.','5. Replace stale instruction files with pointers to AGENTS.md and .harness/context-routing.md when safe.','6. Run `leerness status` and `leerness verify`.','7. Update session-handoff.md with what was migrated and what remains.','','## Mapping legacy content','| Legacy content | New destination |','|---|---|','| Project goal / product context | project-brief.md |','| Architecture / module map | architecture.md, context-map.md |','| Old task history | task-log.md, current-state.md |','| Release/deploy notes | release-checklist.md |','| AI rules / prompts | AGENTS.md, guardrails.md, context-routing.md |','| Successful repeated workflow | .harness/skills/ or skill library candidate |','','## Safety rules','- Do not delete legacy files before archive exists.','- Do not copy secret values into harness files. Replace with environment variable names.','- Do not merge contradictory rules silently; record conflict in decisions.md or session-handoff.md.','- Keep the current active source of truth obvious: AGENTS.md + .harness/context-routing.md.','',''].join('\n'),
|
|
51
|
-
'.harness/AX_NEW_PROJECT_GUIDE.md': [MARK,'---','leernessRole: ax-new-project-guide','readWhen: [new-install, onboarding, first-run, existing-project-analysis]','updateWhen: [onboarding-rule-change, project-discovery-change, new-baseline-file]','doNotStore: [secrets, tokens, credentials]','---','','# AX New Project Installation Guide for AI Agents','','Use this guide after installing Leerness into a new or ongoing project. The goal is to reflect the real project, not leave generic templates.','','## Required project discovery','1. Identify package/framework/runtime from package.json, lockfiles, config files, and folder structure.','2. Identify entry points, routes, API handlers, DB/schema files, deployment config, test commands, and UI system.','3. Fill project-brief.md with purpose, users, success criteria, and product direction.','4. Fill architecture.md with modules, boundaries, data flow, and external services.','5. Fill context-map.md with exact file paths for UI, API, data, auth, deploy, tests, and docs.','6. Fill design-system.md from existing UI components and patterns.','7. Fill feature-contracts.md for important flows and APIs.','8. Fill release-checklist.md from actual deployment/npm/git/CI requirements.','9. Fill testing-strategy.md from actual scripts and manual flows.','10. Update current-state.md and session-handoff.md with the next exact action.','','## AI behavior','- Do not leave placeholders when the project contains enough information.','- Do not invent architecture; mark unknowns explicitly.','- Prefer exact file paths over vague folder names.','- Store environment variable names only, never values.','- When a repeated successful workflow is found, propose a skill candidate.','','## Completion checklist','- [ ] `leerness route feature` returns useful guidance.','- [ ] project-brief.md is specific to this project.','- [ ] context-map.md contains real paths.','- [ ] release-checklist.md matches actual deploy/publish flow.','- [ ] AGENTS.md directs future AI work to the right files.','- [ ] session-handoff.md has the next exact step.','',''].join('\n'),
|
|
52
|
-
'.harness/AX_SKILL_LIBRARY_GUIDE.md': [MARK,
|
|
53
|
-
'# Leerness AX Skill Library Guide',
|
|
54
|
-
'',
|
|
55
|
-
'AX는 AI eXperience입니다. AI 에이전트가 검증된 스킬 데이터를 안전하게 학습, 검증, 빌드, 업로드, 업데이트, 병합, 마이그레이션하도록 안내합니다.',
|
|
56
|
-
'',
|
|
57
|
-
'## 스킬 라이브러리 표시 규격',
|
|
58
|
-
'- 모든 스킬은 name, displayNameKo, title, capabilities, lastUpdated, verification을 가진다.',
|
|
59
|
-
'- leerness skill list는 한글명과 가능한 작업을 먼저 보여준다.',
|
|
60
|
-
'- AI가 스킬을 업로드할 때는 ai-verified-skill-publisher 스킬의 절차를 따른다.',
|
|
61
|
-
'',
|
|
62
|
-
'## 원칙',
|
|
63
|
-
'- 실제 토큰, 쿠키, 비밀번호, 고객 데이터는 저장하지 않는다.',
|
|
64
|
-
'- 환경변수 이름과 연결 규칙만 기록한다.',
|
|
65
|
-
'- 스킬 업로드는 AI 검증 메타데이터가 있을 때만 허용한다.',
|
|
66
|
-
'- 업데이트 또는 마이그레이션 후에는 verification.status를 needs-review로 되돌린다.',
|
|
67
|
-
'- 각 스킬에는 lastUpdated, lastUpdatedAt, verification 정보를 둔다.',
|
|
68
|
-
'',
|
|
69
|
-
'## 표준 흐름',
|
|
70
|
-
'1. 성공한 구현 결과를 스킬 후보로 정리한다.',
|
|
71
|
-
'2. leerness skill learn <name> --from <path> 로 재사용 가능한 절차를 추출한다.',
|
|
72
|
-
'3. leerness library validate <path> --strict-ai 로 구조와 민감정보를 검사한다.',
|
|
73
|
-
'4. leerness library verify <path> --ai --reviewer leerness-ai 로 AI 검증 메타데이터를 기록한다.',
|
|
74
|
-
'5. leerness library build <path> 로 배포 가능한 라이브러리를 만든다.',
|
|
75
|
-
'6. leerness library publish <built-path> --target npm|git --execute 로 검증된 라이브러리만 업로드한다.',
|
|
76
|
-
'',
|
|
77
|
-
'## 필수 메타데이터',
|
|
78
|
-
'- name',
|
|
79
|
-
'- version',
|
|
80
|
-
'- title',
|
|
81
|
-
'- category',
|
|
82
|
-
'- lastUpdated',
|
|
83
|
-
'- lastUpdatedAt',
|
|
84
|
-
'- sensitiveDataPolicy',
|
|
85
|
-
'- requiresEnv',
|
|
86
|
-
'- verification.status',
|
|
87
|
-
'- verification.verifiedAt',
|
|
88
|
-
'- verification.method',
|
|
89
|
-
'',
|
|
90
|
-
'## 업로드 차단 조건',
|
|
91
|
-
'- verification.status가 passed가 아니다.',
|
|
92
|
-
'- verification.method에 ai가 없다.',
|
|
93
|
-
'- 민감정보 의심 패턴이 발견됐다.',
|
|
94
|
-
'- lastUpdated 또는 lastUpdatedAt이 없다.',
|
|
95
|
-
'- --execute 없이 실제 업로드를 시도했다.',
|
|
96
|
-
'',
|
|
97
|
-
'## AI 에이전트 체크리스트',
|
|
98
|
-
'- 스킬 목적과 사용 조건이 명확한가',
|
|
99
|
-
'- 구현 절차가 재현 가능한가',
|
|
100
|
-
'- 민감정보가 아니라 환경변수 이름만 있는가',
|
|
101
|
-
'- 검증 방법과 실패 대응법이 있는가',
|
|
102
|
-
'- 병합 후 skills-lock.json에 출처와 검증 상태가 기록되는가',
|
|
103
|
-
'',
|
|
104
|
-
].join('\n'),
|
|
105
|
-
'.harness/skills/core/codebase-analysis.md': [MARK,'# Skill: Codebase Analysis','','1. Read current-state and context-map.','2. Locate related files.','3. Identify data flow and ownership.','4. Summarize risks before editing.',''].join('\n'),
|
|
106
|
-
'.harness/skills/core/feature-implementation.md': [MARK,'# Skill: Feature Implementation','','1. Convert the requirement into a feature contract.','2. Find existing patterns.','3. Implement the smallest safe change.','4. Verify behavior.','5. Update harness memory.',''].join('\n'),
|
|
107
|
-
'.harness/skills/core/refactoring.md': [MARK,'# Skill: Refactoring','','Keep behavior unchanged. Move in small steps. Preserve public contracts. Verify after each meaningful change.',''].join('\n'),
|
|
108
|
-
'.harness/skills/core/debugging.md': [MARK,'# Skill: Debugging','','Separate symptom, reproduction, root cause, fix, and prevention. Record repeated failure patterns in guardrails.',''].join('\n'),
|
|
109
|
-
'.harness/skills/core/ui-consistency.md': [MARK,'# Skill: UI Consistency','','Read design-system.md and existing similar screens before changing UI. Include loading, empty, error, desktop, and mobile states.',''].join('\n'),
|
|
110
|
-
'.harness/skills/core/security-review.md': [MARK,'# Skill: Security Review','','Check secrets, auth boundaries, input validation, logging, dependency risk, and permission scope.',''].join('\n'),
|
|
111
|
-
'.harness/skills/core/release-check.md': [MARK,'# Skill: Release Check','','Verify build, tests, env vars, migration impact, and rollback path.',''].join('\n'),
|
|
112
|
-
'.harness/skills/core/documentation-update.md': [MARK,'# Skill: Documentation Update','','Update current-state, task-log, decisions, context-map, feature-contracts, design-system, and session-handoff when relevant.',''].join('\n'),
|
|
113
|
-
'.harness/templates/session-summary.md': [MARK,'# Session Summary','','## Done','','## Files Changed','','## Verification','','## Next',''].join('\n'),
|
|
114
|
-
'.harness/templates/decision.md': [MARK,'# Decision','','## Decision','','## Reason','','## Alternatives','','## Impact',''].join('\n')
|
|
115
|
-
};
|
|
116
|
-
|
|
117
19
|
function log(m=''){ console.log(m); }
|
|
118
20
|
function ok(m){ log(c.green+'✓'+c.reset+' '+m); }
|
|
119
21
|
function info(m){ log(c.cyan+'ℹ'+c.reset+' '+m); }
|
|
@@ -123,364 +25,208 @@ function exists(p){ return fs.existsSync(p); }
|
|
|
123
25
|
function read(p){ return fs.readFileSync(p,'utf8'); }
|
|
124
26
|
function write(p,s){ fs.mkdirSync(path.dirname(p),{recursive:true}); fs.writeFileSync(p,s,'utf8'); }
|
|
125
27
|
function rel(root,p){ return path.relative(root,p).replace(/\\/g,'/') || '.'; }
|
|
126
|
-
function isTextFile(p){ return /\.(md|mdc|txt|json|js|ts|tsx|jsx|yml|yaml|env|gitignore)$/i.test(p) || !path.extname(p); }
|
|
127
28
|
function parseJsonSafe(s,fallback){ try { return JSON.parse(s); } catch { return fallback; } }
|
|
128
|
-
function
|
|
129
|
-
function
|
|
29
|
+
function isTextFile(p){ return /\.(md|mdc|txt|json|js|ts|tsx|jsx|yml|yaml|env|gitignore)$/i.test(p) || !path.extname(p); }
|
|
30
|
+
function banner(){ log(''); log(c.bold+c.magenta+'Leerness v'+VERSION+c.reset); log(c.dim+'비파괴 마이그레이션 · context routing · skill library'+c.reset); log(''); }
|
|
31
|
+
function installGuide(){
|
|
32
|
+
log(c.bold+'설치/마이그레이션 안내'+c.reset);
|
|
33
|
+
log(' - 기존 파일은 먼저 .harness/archive/ 에 백업합니다.');
|
|
34
|
+
log(' - project-brief/current-state/release-checklist 등 기존 프로젝트 메모리는 기본 보존합니다.');
|
|
35
|
+
log(' - .env.example과 .gitignore는 덮어쓰지 않고 필요한 항목만 병합합니다.');
|
|
36
|
+
log(' - AGENTS/CLAUDE/Cursor/Copilot 지침과 AX 라우팅 가이드는 최신 기준으로 갱신합니다.');
|
|
37
|
+
log(' - 기존 메모리 파일까지 강제로 템플릿 재생성하려면 --force를 명시하세요.');
|
|
38
|
+
log('');
|
|
39
|
+
}
|
|
130
40
|
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); }
|
|
131
|
-
|
|
132
|
-
function
|
|
133
|
-
function
|
|
41
|
+
function now(){ return new Date().toISOString(); }
|
|
42
|
+
function today(){ return now().slice(0,10); }
|
|
43
|
+
function fill(t,ctx){ return t.replace(/{{([A-Z_]+)}}/g,(_,k)=>ctx[k]||''); }
|
|
44
|
+
function slug(s){ return String(s||'skill').toLowerCase().replace(/[^a-z0-9._-]+/g,'-').replace(/^-+|-+$/g,'') || 'skill'; }
|
|
45
|
+
|
|
46
|
+
function copyRecursive(src,dst,ignoreAbs=[]){
|
|
47
|
+
const abs=path.resolve(src); if(ignoreAbs.some(i=>abs===i||abs.startsWith(i+path.sep))) return;
|
|
48
|
+
const st=fs.statSync(src);
|
|
49
|
+
if(st.isDirectory()){ fs.mkdirSync(dst,{recursive:true}); for(const n of fs.readdirSync(src)) copyRecursive(path.join(src,n),path.join(dst,n),ignoreAbs); }
|
|
50
|
+
else { fs.mkdirSync(path.dirname(dst),{recursive:true}); fs.copyFileSync(src,dst); }
|
|
51
|
+
}
|
|
52
|
+
function detectLegacy(root){
|
|
53
|
+
return legacyItems.map(item=>({item,full:path.join(root,item)})).filter(e=>{
|
|
54
|
+
if(!exists(e.full)) return false;
|
|
55
|
+
if(e.item==='.harness'){
|
|
56
|
+
const vf=path.join(root,'.harness/HARNESS_VERSION');
|
|
57
|
+
return !exists(vf) || read(vf).trim()!==VERSION;
|
|
58
|
+
}
|
|
59
|
+
try{
|
|
60
|
+
if(fs.statSync(e.full).isFile() && isTextFile(e.item)){
|
|
61
|
+
const b=read(e.full);
|
|
62
|
+
if(b.includes(MARK)||b.includes(MIGRATED)) return false;
|
|
63
|
+
}
|
|
64
|
+
}catch{}
|
|
65
|
+
return true;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
134
68
|
function collectLegacyText(found){ const out={}; for(const f of found){ try{ if(fs.statSync(f.full).isFile() && isTextFile(f.item)) out[f.item]=read(f.full); }catch{} } return out; }
|
|
135
69
|
function pick(obj,keys){ const out={}; for(const k of keys) if(obj[k]) out[k]=obj[k]; return out; }
|
|
136
70
|
function legacyBlock(title,obj){ const entries=Object.entries(obj).filter(([,v])=>String(v).trim()); if(!entries.length) return ''; return '\n---\n## Migrated legacy notes: '+title+'\n\n'+entries.map(([k,v])=>'### '+k+'\n\n'+String(v).trim()+'\n').join('\n'); }
|
|
137
|
-
function archiveLegacy(root,found,dryRun){
|
|
71
|
+
function archiveLegacy(root,found,dryRun){
|
|
72
|
+
if(!found.length) return null;
|
|
73
|
+
const stamp=now().replace(/[:.]/g,'-');
|
|
74
|
+
const archive=path.join(root,'.harness/archive/legacy-migration-'+stamp);
|
|
75
|
+
if(dryRun) return archive;
|
|
76
|
+
fs.mkdirSync(archive,{recursive:true});
|
|
77
|
+
const archiveRoot=path.resolve(path.join(root,'.harness/archive'));
|
|
78
|
+
for(const f of found){
|
|
79
|
+
try{ copyRecursive(f.full,path.join(archive,f.item==='.harness'?'.harness-before-v'+VERSION:f.item),[archiveRoot]); }
|
|
80
|
+
catch(e){ warn('백업 실패: '+f.item+' ('+e.message+')'); }
|
|
81
|
+
}
|
|
82
|
+
write(path.join(archive,'migration-manifest.json'),JSON.stringify({version:VERSION,archivedAt:now(),items:found.map(x=>x.item)},null,2)+'\n');
|
|
83
|
+
return archive;
|
|
84
|
+
}
|
|
138
85
|
function targetForLegacy(item){ if(/ARCHITECTURE/i.test(item)) return '.harness/architecture.md'; if(/DECISION/i.test(item)) return '.harness/decisions.md'; if(/CURRENT|TASK_LOG|history/i.test(item)) return '.harness/current-state.md'; if(/AGENT|CLAUDE|cursor|copilot|cursorrules/i.test(item)) return 'AGENTS.md'; return '.harness/project-brief.md'; }
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
86
|
+
function noteLegacyPreserved(root,found,dryRun){
|
|
87
|
+
for(const f of found){
|
|
88
|
+
if(f.item==='.harness'||coreFiles[f.item]) continue;
|
|
89
|
+
try{ if(!fs.statSync(f.full).isFile()) continue; }catch{ continue; }
|
|
90
|
+
const target=targetForLegacy(f.item);
|
|
91
|
+
if(dryRun) info('[dry-run] legacy file preserved: '+f.item+' (suggested source: '+target+')');
|
|
92
|
+
else info('보존: '+f.item+' (참조 권장: '+target+')');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const coreFiles = {
|
|
97
|
+
'AGENTS.md': `${MARK}\n# {{PROJECT}} AI Agent Harness\n\nAgent = Model + Leerness Harness.\n\n## Core Rule\nBefore editing, route the task. Read .harness/context-routing.md and use \`leerness route <task-type>\` when the task type is unclear.\n\n## Universal Read Order\n1. .harness/project-brief.md\n2. .harness/current-state.md\n3. .harness/context-routing.md\n4. .harness/writeback-policy.md\n5. .harness/task-type-map.md\n6. .harness/context-map.md\n7. .harness/guardrails.md\n8. .harness/skills-lock.json\n\n## Task Routing\n- Feature/API work: architecture.md, feature-contracts.md, context-map.md, skills/feature-implementation.md.\n- UI/design work: design-system.md, feature-contracts.md, skills/ui-consistency.md.\n- Debugging: task-log.md, current-state.md, skills/debugging.md, related feature contract.\n- Refactoring: architecture.md, decisions.md, guardrails.md, skills/refactoring.md.\n- Release/deploy: release-checklist.md, testing-strategy.md, current-state.md, decisions.md.\n- Migration: AX_MIGRATION_GUIDE.md, context-routing.md, writeback-policy.md.\n- New install: AX_NEW_PROJECT_GUIDE.md and actual project config/source files.\n- Skill/library work: AX_SKILL_LIBRARY_GUIDE.md and ai-verified-skill-publisher when installed.\n\n## Writeback Rules\n- Always update current-state.md, task-log.md, and session-handoff.md after meaningful work.\n- Update decisions.md when a structural, technology, API, schema, deployment, or irreversible decision is made.\n- Update feature-contracts.md when input/output/state/error behavior changes.\n- Update design-system.md when UI rules, components, layout, spacing, or states change.\n- Update release-checklist.md when deployment, environment variables, rollback, CI, npm, or git release requirements change.\n- Update context-map.md when important files, modules, routes, commands, or ownership areas change.\n- Update project-brief.md only when product purpose, target users, success criteria, or project direction changes.\n\n## Non-Destructive Migration Policy\n- Never overwrite existing project memory files unless the user explicitly requests --force.\n- Preserve .env.example and .gitignore; append missing Leerness entries only.\n- Keep secrets, tokens, cookies, credentials, and customer private data out of harness files.\n\n## Response Contract\n- Task type and files consulted\n- Summary\n- Files changed\n- Verification\n- Memory files updated\n- Risks or assumptions\n- Next step\n{{LEGACY_AGENT}}\n`,
|
|
98
|
+
'CLAUDE.md': `${MARK}\n# Claude Code Instructions\n\nUse AGENTS.md as the source of truth. Route every task through .harness/context-routing.md and .harness/task-type-map.md. Do not overwrite existing project memory during migration unless --force is explicit.\n`,
|
|
99
|
+
'.cursor/rules/leerness.mdc': `${MARK}\n---\nalwaysApply: true\n---\nRead AGENTS.md first. Follow .harness/context-routing.md, writeback-policy.md, installed skills, design-system, feature-contracts, and guardrails.\n`,
|
|
100
|
+
'.github/copilot-instructions.md': `${MARK}\n# GitHub Copilot Instructions\n\nUse AGENTS.md and .harness/ as the project memory. Preserve existing project memory files unless --force is explicit.\n`,
|
|
101
|
+
'.gitignore': `# Leerness local-only files\n.env\n.env.local\n*.secret.json\n.harness/skill-config.local.json\n.harness/skill-publish.local.json\n`,
|
|
102
|
+
'.env.example': `# Leerness examples only. Copy to .env.local and fill locally. Never commit real secrets.\n`,
|
|
103
|
+
'.harness/HARNESS_VERSION': '{{VERSION}}\n',
|
|
104
|
+
'.harness/manifest.json': '{{MANIFEST}}\n',
|
|
105
|
+
'.harness/skills-lock.json': '{{SKILLS_LOCK}}\n',
|
|
106
|
+
'.harness/skill-config.schema.json': `${MARK}\n{\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "title": "Leerness Skill Config",\n "type": "object",\n "additionalProperties": true\n}\n`,
|
|
107
|
+
'.harness/project-brief.md': `${MARK}\n---\nleernessRole: project-brief\nreadWhen: [every-task, planning, product-direction, onboarding]\nupdateWhen: [purpose-change, user-change, success-criteria-change, product-direction-change]\ndoNotStore: [secrets, tokens, credentials, raw-customer-data]\n---\n\n# Project Brief: {{PROJECT}}\n\n## Purpose\n\n## Success Criteria\n\n## Users\n\n## Product Direction\n{{LEGACY_BRIEF}}\n`,
|
|
108
|
+
'.harness/current-state.md': `${MARK}\n---\nleernessRole: current-state\nreadWhen: [every-task, resume-work, planning, debugging, release]\nupdateWhen: [after-meaningful-work, blocker-change, next-step-change, status-change]\ndoNotStore: [secrets, tokens, credentials]\n---\n\n# Current State\n\nUpdated: {{DATE}}\n\n## Now\n- Leerness v{{VERSION}} installed or migrated.\n\n## Next\n- Fill project-brief, context-map, design-system, and feature-contracts.\n\n## Blockers\n- None recorded.\n{{LEGACY_STATE}}\n`,
|
|
109
|
+
'.harness/architecture.md': `${MARK}\n---\nleernessRole: architecture\nreadWhen: [feature, refactor, integration, api, database, deployment]\nupdateWhen: [module-change, data-flow-change, integration-change, boundary-change]\ndoNotStore: [secrets, credentials]\n---\n\n# Architecture\n\n## Overview\n\n## Main Modules\n\n## Data Flow\n\n## External Services\n\n## Boundaries\n{{LEGACY_ARCH}}\n`,
|
|
110
|
+
'.harness/context-map.md': `${MARK}\n---\nleernessRole: context-map\nreadWhen: [every-task, file-discovery, impact-analysis]\nupdateWhen: [new-important-file, moved-module, new-route, new-service, ownership-change]\ndoNotStore: [secrets, tokens]\n---\n\n# Context Map\n\n| Area | Files | Notes |\n|---|---|---|\n| UI | src/components/**, app/** | Check design-system.md first. |\n| API | src/api/**, server/**, functions/** | Preserve response contracts. |\n| Data | db/**, firestore/**, prisma/** | Confirm migrations. |\n| Tests | test/**, tests/**, __tests__/** | Add or update checks. |\n`,
|
|
111
|
+
'.harness/decisions.md': `${MARK}\n---\nleernessRole: decisions\nreadWhen: [architecture, refactor, release, dependency-change, irreversible-change]\nupdateWhen: [important-decision, tradeoff, architecture-change, dependency-change, rollback-relevant-change]\ndoNotStore: [secrets, credentials]\n---\n\n# Decision Log\n\n## Template\n\n### YYYY-MM-DD — Title\n- Decision:\n- Reason:\n- Alternatives:\n- Impact:\n{{LEGACY_DECISIONS}}\n`,
|
|
112
|
+
'.harness/task-log.md': `${MARK}\n---\nleernessRole: task-log\nreadWhen: [debugging, audit, handoff, regression]\nupdateWhen: [after-meaningful-work, failed-attempt, verification-result]\ndoNotStore: [secrets, tokens]\n---\n\n# Task Log\n\n## {{DATE}}\n- Leerness v{{VERSION}} installed or migrated.\n`,
|
|
113
|
+
'.harness/constraints.md': `${MARK}\n# Constraints\n\n- Runtime/framework/deployment constraints\n- Security/privacy/business constraints\n`,
|
|
114
|
+
'.harness/guardrails.md': `${MARK}\n---\nleernessRole: guardrails\nreadWhen: [every-task, security, integration, refactor]\nupdateWhen: [new-risk, repeated-error, policy-change]\ndoNotStore: [secrets, tokens, private-data]\n---\n\n# Guardrails\n\n## Never\n- Store real secrets in code or harness files.\n- Overwrite existing project memory during migration without --force.\n- Change API responses, DB schema, env names, or auth flow without impact review.\n- Perform broad refactoring unless requested.\n\n## Always\n- Preserve architecture and contracts.\n- Record decisions and update writeback files.\n`,
|
|
115
|
+
'.harness/design-system.md': `${MARK}\n---\nleernessRole: design-system\nreadWhen: [ui, layout, component, style, visual-consistency]\nupdateWhen: [new-component-pattern, style-rule-change, state-pattern-change]\ndoNotStore: [secrets, user-private-data]\n---\n\n# Design System Memory\n\n## Layout\n\n## Components\n\n## States\n- Loading\n- Empty\n- Error\n- Success\n`,
|
|
116
|
+
'.harness/feature-contracts.md': `${MARK}\n---\nleernessRole: feature-contracts\nreadWhen: [feature, api, ui-state, debugging, refactor]\nupdateWhen: [input-change, output-change, state-change, error-change, contract-change]\ndoNotStore: [secrets, raw-private-data]\n---\n\n# Feature Contracts\n\n## Template\n- Feature:\n- Entry point:\n- Input:\n- Output:\n- UI states:\n- Error states:\n- Related files:\n- Tests:\n`,
|
|
117
|
+
'.harness/testing-strategy.md': `${MARK}\n---\nleernessRole: testing-strategy\nreadWhen: [feature, debugging, refactor, release]\nupdateWhen: [test-command-change, new-critical-flow, regression-added]\ndoNotStore: [secrets]\n---\n\n# Testing Strategy\n\n## Commands\n\n## Critical Flows\n\n## Regression Notes\n`,
|
|
118
|
+
'.harness/review-checklist.md': `${MARK}\n# Review Checklist\n\n- [ ] Architecture preserved\n- [ ] Feature contract preserved or updated\n- [ ] Design system followed\n- [ ] No secrets stored\n- [ ] Writeback files updated\n`,
|
|
119
|
+
'.harness/release-checklist.md': `${MARK}\n---\nleernessRole: release-checklist\nreadWhen: [release, deploy, ci, npm-publish, git-push, env-change]\nupdateWhen: [deploy-failure, new-env-var, ci-change, rollback-change, release-rule-change]\ndoNotStore: [secrets, tokens, passwords, cookies]\n---\n\n# Release Checklist\n\n## Commands\n\n## Required Environment Variables\n\n## Verification\n\n## Rollback\n`,
|
|
120
|
+
'.harness/session-handoff.md': `${MARK}\n---\nleernessRole: session-handoff\nreadWhen: [resume-work, every-new-session]\nupdateWhen: [end-of-session, handoff, blocked-work]\ndoNotStore: [secrets, tokens]\n---\n\n# Session Handoff\n\n## Done\n\n## Changed Files\n\n## Verification\n\n## Risks\n\n## Next Exact Step\n`,
|
|
121
|
+
'.harness/skill-index.md': `${MARK}\n# Skill Index\n\n| Task | Skill |\n|---|---|\n| Codebase analysis | skills/codebase-analysis.md |\n| Feature implementation | skills/feature-implementation.md |\n| Debugging | skills/debugging.md |\n| UI consistency | skills/ui-consistency.md |\n| Release | skills/release-check.md |\n`,
|
|
122
|
+
'.harness/context-routing.md': `${MARK}\n# Context Routing\n\nUse this file to decide what to read before work and what to update afterward.\n\n## feature\nRead: project-brief, current-state, architecture, context-map, feature-contracts, skills/feature-implementation.\nUpdate: current-state, task-log, session-handoff, feature-contracts, context-map when paths change.\n\n## ui\nRead: design-system, feature-contracts, context-map, skills/ui-consistency.\nUpdate: design-system, feature-contracts, current-state, task-log, session-handoff.\n\n## debugging\nRead: current-state, task-log, feature-contracts, testing-strategy, skills/debugging.\nUpdate: task-log, current-state, session-handoff, testing-strategy when regression coverage changes.\n\n## release\nRead: release-checklist, testing-strategy, current-state, decisions, secret-policy.\nUpdate: release-checklist, task-log, current-state, session-handoff.\n\n## migration\nRead: AX_MIGRATION_GUIDE, writeback-policy, task-type-map.\nUpdate: only missing files by default; preserve project memory unless --force.\n`,
|
|
123
|
+
'.harness/writeback-policy.md': `${MARK}\n# Writeback Policy\n\n## current-state.md\nCurrent progress, blockers, next work.\n\n## task-log.md\nWhat changed, when, and verification result.\n\n## session-handoff.md\nEnough context for the next AI session to continue.\n\n## decisions.md\nImportant choices and tradeoffs.\n\n## release-checklist.md\nDeploy commands, env requirements, rollback, failures.\n\n## design-system.md\nUI rules and reusable patterns.\n\n## feature-contracts.md\nInput/output/state/error contracts.\n\n## project-brief.md\nProduct purpose and success criteria only.\n`,
|
|
124
|
+
'.harness/task-type-map.md': `${MARK}\n# Task Type Map\n\n| User request | Task type | First files |\n|---|---|---|\n| 새 기능 | feature | feature-contracts, architecture |\n| 디자인/UI | ui | design-system |\n| 오류 수정 | debugging | task-log, debugging skill |\n| 구조 개선 | refactor | architecture, decisions |\n| 배포 | release | release-checklist |\n| 하네스 전환 | migration | AX_MIGRATION_GUIDE |\n| 신규 적용 | new-install | AX_NEW_PROJECT_GUIDE |\n| 스킬 저장/배포 | skill-library | AX_SKILL_LIBRARY_GUIDE |\n`,
|
|
125
|
+
'.harness/AX_MIGRATION_GUIDE.md': `${MARK}\n# AX Migration Guide\n\n## Goal\nMigrate old harness files without losing project memory.\n\n## Procedure\n1. Run: leerness migrate --dry-run\n2. Confirm archive target.\n3. Run: leerness migrate\n4. Check .env.example and .gitignore were merged, not replaced.\n5. Check project memory files were preserved.\n6. Fill only missing context using archived legacy files.\n7. Run: leerness status && leerness verify.\n\n## Critical Rule\nDo not overwrite existing project-brief, current-state, architecture, decisions, release-checklist, feature-contracts, or design-system unless the user explicitly asks for --force.\n`,
|
|
126
|
+
'.harness/AX_NEW_PROJECT_GUIDE.md': `${MARK}\n# AX New Project Guide\n\n## Goal\nAfter initial installation, populate Leerness memory from the actual project.\n\n## Read actual project files\n- package/config files\n- app/routes/pages\n- API/server/functions\n- DB/schema/rules\n- deploy/CI files\n- tests\n\n## Fill memory files\n- project-brief.md: purpose and success criteria\n- architecture.md: modules and data flow\n- context-map.md: important files and routes\n- design-system.md: existing UI patterns\n- feature-contracts.md: major features and states\n- release-checklist.md: real deploy commands and env requirements\n`,
|
|
127
|
+
'.harness/AX_SKILL_LIBRARY_GUIDE.md': `${MARK}\n# AX Skill Library Guide\n\n## AI-verified skill lifecycle\n1. Learn from a validated implementation.\n2. Remove secrets and keep env variable names only.\n3. Add displayNameKo, capabilities, lastUpdated, verification metadata.\n4. Run validate.\n5. Run verify --ai.\n6. Build.\n7. Publish dry-run.\n8. Publish with --execute only after token gate passes.\n`,
|
|
128
|
+
'.harness/skills/codebase-analysis.md': `${MARK}\n# Skill: Codebase Analysis\n\nRead context-map, architecture, current-state, and related source files before proposing changes.\n`,
|
|
129
|
+
'.harness/skills/feature-implementation.md': `${MARK}\n# Skill: Feature Implementation\n\nDefine contract, inspect existing patterns, implement minimal change, verify, update memory.\n`,
|
|
130
|
+
'.harness/skills/refactoring.md': `${MARK}\n# Skill: Refactoring\n\nPreserve behavior and contracts. Record important decisions.\n`,
|
|
131
|
+
'.harness/skills/debugging.md': `${MARK}\n# Skill: Debugging\n\nReproduce, isolate cause, patch minimally, verify, add regression note.\n`,
|
|
132
|
+
'.harness/skills/ui-consistency.md': `${MARK}\n# Skill: UI Consistency\n\nRead design-system and existing adjacent screens before styling.\n`,
|
|
133
|
+
'.harness/skills/security-review.md': `${MARK}\n# Skill: Security Review\n\nCheck secrets, auth, permissions, logging, and sensitive data exposure.\n`,
|
|
134
|
+
'.harness/skills/release-check.md': `${MARK}\n# Skill: Release Check\n\nCheck tests, build, env vars, migration, rollback, publish token gate.\n`,
|
|
135
|
+
'.harness/skills/documentation-update.md': `${MARK}\n# Skill: Documentation Update\n\nFollow writeback-policy and update the specific memory file.\n`,
|
|
136
|
+
'.harness/templates/session-summary.md': `${MARK}\n# Session Summary\n\n## Done\n\n## Files Changed\n\n## Verification\n\n## Next\n`,
|
|
137
|
+
'.harness/templates/decision.md': `${MARK}\n# Decision\n\n## Decision\n\n## Reason\n\n## Alternatives\n\n## Impact\n`
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const memoryFiles = new Set(['.harness/project-brief.md','.harness/current-state.md','.harness/architecture.md','.harness/context-map.md','.harness/decisions.md','.harness/task-log.md','.harness/constraints.md','.harness/guardrails.md','.harness/design-system.md','.harness/feature-contracts.md','.harness/testing-strategy.md','.harness/review-checklist.md','.harness/release-checklist.md','.harness/session-handoff.md','.harness/skill-index.md','.harness/secret-policy.md']);
|
|
141
|
+
const refreshableFiles = new Set(['AGENTS.md','CLAUDE.md','.cursor/rules/leerness.mdc','.github/copilot-instructions.md','.harness/context-routing.md','.harness/writeback-policy.md','.harness/task-type-map.md','.harness/AX_SKILL_LIBRARY_GUIDE.md','.harness/AX_MIGRATION_GUIDE.md','.harness/AX_NEW_PROJECT_GUIDE.md','.harness/manifest.json','.harness/HARNESS_VERSION']);
|
|
142
|
+
function uniqueLinesAppend(current, addition){
|
|
143
|
+
const lines=current.split(/\r?\n/); const seen=new Set(lines.map(x=>x.trim()).filter(Boolean));
|
|
144
|
+
const add=addition.split(/\r?\n/).filter(line=>{ const t=line.trim(); if(!t||seen.has(t)) return false; seen.add(t); return true; });
|
|
145
|
+
if(!add.length) return current;
|
|
146
|
+
return current.replace(/\s*$/,'\n')+add.join('\n')+'\n';
|
|
147
|
+
}
|
|
148
|
+
function mergeSkillLockJson(current,incoming){ const a=parseJsonSafe(current,{installedSkills:{}}), b=parseJsonSafe(incoming,{installedSkills:{}}); const merged={...b,...a}; merged.harnessVersion=VERSION; merged.updatedAt=now(); merged.installedSkills={...(b.installedSkills||{}),...(a.installedSkills||{})}; return JSON.stringify(merged,null,2)+'\n'; }
|
|
149
|
+
function writeCoreSafely(root,file,body,opts={}){
|
|
150
|
+
const target=path.join(root,file), dryRun=Boolean(opts.dryRun), force=Boolean(opts.force), existed=exists(target);
|
|
151
|
+
if(!existed){ if(dryRun) info('[dry-run] 생성: '+file); else { write(target,body); ok('생성: '+file); } return 'created'; }
|
|
152
|
+
const current=read(target); if(current===body){ if(dryRun) info('[dry-run] 유지: '+file); else ok('유지: '+file); return 'same'; }
|
|
153
|
+
if(file==='.gitignore'||file==='.env.example'){
|
|
154
|
+
const merged=uniqueLinesAppend(current,body); if(merged===current){ if(dryRun) info('[dry-run] 보존: '+file); else ok('보존: '+file); return 'preserved'; }
|
|
155
|
+
if(dryRun) info('[dry-run] 병합: '+file); else { write(target,merged); ok('병합: '+file); } return 'merged';
|
|
156
|
+
}
|
|
157
|
+
if(file==='.harness/skills-lock.json'){
|
|
158
|
+
const merged=mergeSkillLockJson(current,body); if(dryRun) info('[dry-run] 병합: '+file); else { write(target,merged); ok('병합: '+file); } return 'merged';
|
|
159
|
+
}
|
|
160
|
+
if(force || refreshableFiles.has(file)){
|
|
161
|
+
if(dryRun) info('[dry-run] '+(force?'강제 ':'')+'갱신: '+file); else { write(target,body); ok((force?'강제 ':'')+'갱신: '+file); } return 'updated';
|
|
162
|
+
}
|
|
163
|
+
if(memoryFiles.has(file)){
|
|
164
|
+
if(dryRun) info('[dry-run] 보존: '+file+' (기존 프로젝트 메모리 유지)'); else ok('보존: '+file+' (기존 프로젝트 메모리 유지)'); return 'preserved';
|
|
165
|
+
}
|
|
166
|
+
if(dryRun) info('[dry-run] 보존: '+file+' (덮어쓰려면 --force)'); else ok('보존: '+file+' (덮어쓰려면 --force)'); return 'preserved';
|
|
167
|
+
}
|
|
168
|
+
function manifest(root,selectedSkills){ return JSON.stringify({name:projectName(root),harnessVersion:VERSION,installedAt:now(),managedFiles:Object.keys(coreFiles),selectedSkills,nonDestructiveMigration:true},null,2); }
|
|
169
|
+
function skillsLock(root,selectedSkills){ const lock={harnessVersion:VERSION,installedAt:now(),installedSkills:{}}; for(const name of selectedSkills){ const meta=getSkillMeta(name); if(meta) lock.installedSkills[name]={version:meta.version,source:'bundled',title:meta.title,displayNameKo:meta.displayNameKo||meta.title,lastUpdated:meta.lastUpdated,verificationStatus:(meta.verification||{}).status||'unknown'}; } return JSON.stringify(lock,null,2); }
|
|
170
|
+
function makeContext(root,legacyText,selectedSkills){ return { PROJECT:projectName(root), DATE:today(), VERSION, LEGACY_AGENT:legacyBlock('agent instructions',pick(legacyText,['AGENTS.md','AGENT.md','CLAUDE.md','.cursorrules','.cursor/rules/project-rules.mdc','.cursor/rules/leerness.mdc','.github/copilot-instructions.md'])), LEGACY_BRIEF:legacyBlock('project context',pick(legacyText,['PROJECT_CONTEXT.md','CONTEXT.md','docs/guideline.md','AI_HARNESS.md','HARNESS.md'])), LEGACY_STATE:legacyBlock('state',pick(legacyText,['CURRENT_STATE.md','TASK_LOG.md','docs/history.md'])), LEGACY_ARCH:legacyBlock('architecture',pick(legacyText,['ARCHITECTURE.md'])), LEGACY_DECISIONS:legacyBlock('decisions',pick(legacyText,['DECISIONS.md'])), MANIFEST:manifest(root,selectedSkills), SKILLS_LOCK:skillsLock(root,selectedSkills) }; }
|
|
144
171
|
|
|
145
172
|
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)); }
|
|
146
173
|
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; }
|
|
147
|
-
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=
|
|
148
|
-
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'); }
|
|
174
|
+
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=now(); 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'); }
|
|
175
|
+
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.replace(/\s*$/,'\n')+'\n# '+(meta.title||meta.name)+' ('+meta.name+')\n'+missing.map(n=>n+'=').join('\n')+'\n'); }
|
|
149
176
|
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; }
|
|
150
177
|
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); }
|
|
151
178
|
|
|
152
|
-
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; }
|
|
179
|
+
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','token-env']); 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; }
|
|
153
180
|
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); }
|
|
154
181
|
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());})); }
|
|
155
182
|
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); }
|
|
156
183
|
|
|
157
|
-
async function init(root,flags){
|
|
158
|
-
|
|
184
|
+
async function init(root,flags){
|
|
185
|
+
root=path.resolve(root||process.cwd()); fs.mkdirSync(root,{recursive:true}); banner(); installGuide(); info('대상: '+root);
|
|
186
|
+
const selectedSkills=await chooseSkills(Boolean(flags.yes||flags.y),flags.skills);
|
|
187
|
+
const found=detectLegacy(root), legacyText=collectLegacyText(found);
|
|
188
|
+
if(found.length){ warn('기존 하네스/지침 파일 감지: '+found.length+'개'); found.forEach(f=>log(' - '+f.item)); }
|
|
189
|
+
const archive=archiveLegacy(root,found,false); if(archive) info('백업 완료: '+rel(root,archive));
|
|
190
|
+
noteLegacyPreserved(root,found,false);
|
|
191
|
+
const ctx=makeContext(root,legacyText,selectedSkills);
|
|
192
|
+
for(const [file,template] of Object.entries(coreFiles)) writeCoreSafely(root,file,fill(template,ctx),{force:Boolean(flags.force)});
|
|
193
|
+
if(selectedSkills.length){ log(''); info('선택 스킬 설치 중: '+selectedSkills.join(', ')); for(const name of selectedSkills) installSkill(root,name,false); }
|
|
194
|
+
ok('설치 완료'); info('신규 프로젝트라면 .harness/AX_NEW_PROJECT_GUIDE.md를 따라 실제 프로젝트 내용을 반영하세요.');
|
|
195
|
+
}
|
|
196
|
+
function migrate(root,flags){
|
|
197
|
+
root=path.resolve(root||process.cwd()); banner(); installGuide(); const dryRun=Boolean(flags['dry-run']); const force=Boolean(flags.force);
|
|
198
|
+
const found=detectLegacy(root);
|
|
199
|
+
if(!found.length) ok('마이그레이션할 legacy 항목이 없습니다. 누락/라우팅 파일만 점검합니다.');
|
|
200
|
+
else { warn('마이그레이션 대상: '+found.length+'개'); found.forEach(f=>log(' - '+f.item)); }
|
|
201
|
+
const archive=archiveLegacy(root,found,dryRun); if(archive) info((dryRun?'[dry-run] 백업 예정: ':'백업 완료: ')+rel(root,archive));
|
|
202
|
+
noteLegacyPreserved(root,found,dryRun);
|
|
203
|
+
const ctx=makeContext(root,collectLegacyText(found),[]);
|
|
204
|
+
for(const [file,template] of Object.entries(coreFiles)) writeCoreSafely(root,file,fill(template,ctx),{dryRun,force});
|
|
205
|
+
if(!dryRun){ ok('마이그레이션 완료'); info('기존 프로젝트 메모리 파일은 보존되었습니다. 템플릿 재생성이 필요하면 --force를 명시하세요.'); }
|
|
206
|
+
}
|
|
159
207
|
function status(root){ root=path.resolve(root||process.cwd()); const vf=path.join(root,'.harness/HARNESS_VERSION'); const version=exists(vf)?read(vf).trim():'not installed'; const missing=Object.keys(coreFiles).filter(f=>!exists(path.join(root,f))); const lp=path.join(root,'.harness/skills-lock.json'); const lock=exists(lp)?parseJsonSafe(read(lp),{installedSkills:{}}):{installedSkills:{}}; banner(); log('대상: '+root); log('버전: '+version); log('파일: '+(Object.keys(coreFiles).length-missing.length)+'/'+Object.keys(coreFiles).length); if(missing.length){ warn('누락 파일'); missing.forEach(x=>log(' - '+x)); } else ok('필수 파일 모두 존재'); const names=Object.keys(lock.installedSkills||{}); log('설치 스킬: '+(names.length?names.join(', '):'없음')); }
|
|
208
|
+
function scanSensitivePath(root){ const out=[]; if(!exists(root)) return out; const patterns=[{type:'npm token',re:/npm_[A-Za-z0-9]{20,}/},{type:'github token',re:/gh[pousr]_[A-Za-z0-9_]{20,}/},{type:'private key',re:/-----BEGIN [A-Z ]*PRIVATE KEY-----/},{type:'password assignment',re:/(password|secret|token|api[_-]?key)\s*[:=]\s*['\"][^'\"]{8,}/i}]; function walk(p){ const st=fs.statSync(p); if(st.isDirectory()){ const b=path.basename(p); if(['node_modules','.git','archive','dist'].includes(b)) return; for(const n of fs.readdirSync(p)) walk(path.join(p,n)); } else if(st.isFile()&&isTextFile(p)){ const body=read(p); for(const pat of patterns){ const m=body.match(pat.re); if(m) out.push({file:p,type:pat.type,sample:m[0].slice(0,60)}); } } } try{ walk(root); }catch{} return out; }
|
|
160
209
|
function verify(root){ root=path.resolve(root||process.cwd()); let failures=0; banner(); for(const file of Object.keys(coreFiles)){ const target=path.join(root,file); if(!exists(target)){ failures++; warn('누락: '+file); continue; } const body=read(target); if(/{{[A-Z_]+}}/.test(body)){ failures++; warn('플레이스홀더 남음: '+file); } } const suspicious=[]; for(const x of ['.harness','AGENTS.md','CLAUDE.md']) for(const f of scanSensitivePath(path.join(root,x))) suspicious.push(f); if(suspicious.length){ failures+=suspicious.length; suspicious.forEach(x=>warn('민감정보 의심: '+rel(root,x.file)+' · '+x.type)); } if(failures){ fail('검증 실패: '+failures); process.exitCode=1; } else ok('검증 완료'); }
|
|
161
210
|
|
|
162
|
-
function slugifyName(value){ return String(value||'').trim().toLowerCase().replace(/^@[^/]+\//,'').replace(/[^a-z0-9._-]+/g,'-').replace(/^-+|-+$/g,'')||'custom-skill'; }
|
|
163
|
-
function packageSafeName(value){ const raw=String(value||'').trim(); if(raw.startsWith('@')) return raw; return 'leerness-skill-'+slugifyName(raw).replace(/^leerness-skill-/,'').replace(/^harness-skill-/,''); }
|
|
164
|
-
function isInside(parent,child){ const r=path.relative(path.resolve(parent),path.resolve(child)); return r&&!r.startsWith('..')&&!path.isAbsolute(r); }
|
|
165
|
-
function scanSensitiveText(body){ const patterns=[{name:'OpenAI style API key',re:/sk-[a-zA-Z0-9_-]{20,}/g},{name:'GitHub token',re:/gh[pousr]_[a-zA-Z0-9_]{20,}/g},{name:'npm token',re:/npm_[a-zA-Z0-9]{20,}/g},{name:'AWS access key',re:/AKIA[0-9A-Z]{16}/g},{name:'private key block',re:/-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g},{name:'password assignment',re:/(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]/ig},{name:'secret assignment',re:/(?:secret|token|api[_-]?key)\s*[:=]\s*['"][^'"]{12,}['"]/ig}]; const findings=[]; for(const p of patterns){ let m; while((m=p.re.exec(body))) findings.push({type:p.name,index:m.index,sample:m[0].slice(0,16)+'...'}); } return findings; }
|
|
166
|
-
function scanSensitivePath(target){ const findings=[]; function scan(p){ if(!exists(p)) return; const st=fs.statSync(p); if(st.isDirectory()){ for(const n of fs.readdirSync(p)){ if(['node_modules','.git','dist','coverage'].includes(n)) continue; scan(path.join(p,n)); } } else if(st.isFile()&&isTextFile(p)){ for(const f of scanSensitiveText(read(p))) findings.push({file:p,...f}); } } scan(target); return findings; }
|
|
167
|
-
function skillLibraryFiles(dir){ const files=[]; function walk(p){ if(!exists(p)) return; const st=fs.statSync(p); if(st.isDirectory()){ for(const n of fs.readdirSync(p)){ if(['node_modules','.git','dist','coverage'].includes(n)) continue; walk(path.join(p,n)); } } else if(st.isFile()) files.push(p); } walk(dir); return files; }
|
|
168
|
-
function readSkillLibraryMeta(dir){ for(const cnd of [path.join(dir,'skill-library.json'),path.join(dir,'skill.json'),path.join(dir,'package.json')]){ if(!exists(cnd)) continue; const data=parseJsonSafe(read(cnd),null); if(!data) continue; if(path.basename(cnd)==='package.json') return { name:data.harnessSkill?.name||data.name, version:data.version||'0.1.0', title:data.harnessSkill?.title||data.description||data.name, packageName:data.name, description:data.description||'', requiresEnv:data.harnessSkill?.requiresEnv||[], files:data.harnessSkill?.files||[] }; return data; } return null; }
|
|
169
|
-
function validateSkillLibrary(dir,options={}){ dir=path.resolve(dir); let failures=0; const meta=readSkillLibraryMeta(dir); if(!meta||!meta.name){ fail('skill-library.json 또는 skill.json에 name이 필요합니다.'); failures++; } const sd=path.join(dir,'skills'); if(!exists(sd)){ fail('skills/ 폴더가 필요합니다.'); failures++; } else if(!skillLibraryFiles(sd).some(f=>f.endsWith('.md'))){ fail('skills/*.md 파일이 최소 1개 필요합니다.'); failures++; } const findings=scanSensitivePath(dir); if(findings.length){ fail('민감정보 의심 패턴 감지. 배포/빌드가 차단됩니다.'); findings.slice(0,20).forEach(f=>warn(rel(dir,f.file)+' · '+f.type+' · '+f.sample)); if(findings.length>20) warn('추가 '+(findings.length-20)+'건 생략'); failures+=findings.length; } for(const env of new Set(meta?.requiresEnv||[])){ if(!/^[A-Z][A-Z0-9_]*$/.test(env)) warn('환경변수 이름 형식 확인 필요: '+env); } if(!options.silent){ if(failures) fail('스킬 라이브러리 검증 실패: '+failures); else ok('스킬 라이브러리 검증 완료: '+(meta?.name||path.basename(dir))); } return {ok:failures===0,failures,meta,findings}; }
|
|
170
|
-
function inferEnvNamesFromText(body){ const names=new Set(); const re=/\b[A-Z][A-Z0-9_]{3,}\b/g; let m; while((m=re.exec(body))){ const v=m[0]; if(/(KEY|TOKEN|SECRET|PASSWORD|CLIENT|VENDOR|ID|URL|HOST|BUCKET|PROJECT)/.test(v)) names.add(v); } return Array.from(names).sort(); }
|
|
171
|
-
function learnSkillLibrary(root,flags){ root=path.resolve(root||process.cwd()); const from=path.resolve(flags.from||path.join(root,'.harness/skills')); const name=slugifyName(flags.name||flags.category||path.basename(from)); const version=String(flags.version||'0.1.0'); const outRoot=path.resolve(flags.out||path.join(root,'.harness/library',name)); if(!exists(from)){ fail('학습할 스킬 경로가 없습니다: '+from); process.exitCode=1; return; } const sourceFiles=skillLibraryFiles(from).filter(f=>isTextFile(f)&&!f.includes(path.sep+'archive'+path.sep)); if(!sourceFiles.length){ fail('학습 가능한 텍스트 스킬 파일이 없습니다.'); process.exitCode=1; return; } fs.mkdirSync(path.join(outRoot,'skills'),{recursive:true}); const requiresEnv=new Set(); const copied=[]; for(const f of sourceFiles){ const body=read(f); for(const e of inferEnvNamesFromText(body)) requiresEnv.add(e); const base=path.basename(f).replace(/[^a-zA-Z0-9._-]/g,'-'); const destName=base.endsWith('.md')?base:base+'.md'; const dest=path.join(outRoot,'skills',destName); const header=[MARK,'# Learned Skill: '+destName.replace(/\.md$/,''),'','Source: '+rel(root,f),'Learned: '+new Date().toISOString(),''].join('\n'); write(dest,body.includes(MARK)?body:header+'\n'+body); copied.push('skills/'+destName); } const meta={name,version,title:flags.title||name,description:flags.description||'Verified project skill library extracted from a successful implementation.',category:flags.category||'custom',compatibleHarness:'>=3.2.0',sensitiveDataPolicy:'env-reference-only',requiresEnv:Array.from(requiresEnv).sort(),files:copied,learnedFrom:rel(root,from),learnedAt:new Date().toISOString()}; write(path.join(outRoot,'skill-library.json'),JSON.stringify(meta,null,2)+'\n'); write(path.join(outRoot,'README.md'),'# '+meta.title+'\n\n'+meta.description+'\n\n## Policy\n\nThis library stores environment variable names only. Do not commit real secrets.\n\n## Required env\n\n'+(meta.requiresEnv.map(e=>'- '+e).join('\n')||'- None')+'\n'); write(path.join(outRoot,'env.example'),meta.requiresEnv.map(e=>e+'=').join('\n')+(meta.requiresEnv.length?'\n':'')); const result=validateSkillLibrary(outRoot,{silent:true}); if(!result.ok){ fail('학습 결과에 민감정보 또는 구조 문제가 있어 확인이 필요합니다: '+outRoot); process.exitCode=1; return; } ok('검증된 스킬 라이브러리 학습 완료: '+outRoot); info('다음: leerness library build '+outRoot); }
|
|
172
|
-
function buildSkillLibrary(dir,flags){ dir=path.resolve(dir||process.cwd()); const check=validateSkillLibrary(dir,{silent:false}); if(!check.ok){ process.exitCode=1; return; } const meta=check.meta; const out=path.resolve(flags.out||path.join(dir,'dist')); const packageName=flags.package||meta.packageName||packageSafeName(meta.name); const libRoot=path.join(out,slugifyName(meta.name)); if(exists(libRoot)) fs.rmSync(libRoot,{recursive:true,force:true}); fs.mkdirSync(libRoot,{recursive:true}); for(const item of ['README.md','skill-library.json','skill.json','env.example','skills','examples','migrations']){ const src=path.join(dir,item); if(exists(src)) copyRecursive(src,path.join(libRoot,item)); } if(!exists(path.join(libRoot,'skill-library.json'))&&exists(path.join(libRoot,'skill.json'))) fs.copyFileSync(path.join(libRoot,'skill.json'),path.join(libRoot,'skill-library.json')); const pkg={name:packageName,version:meta.version||'0.1.0',description:meta.description||meta.title||meta.name,type:'commonjs',files:['skill-library.json','README.md','env.example','skills/','examples/','migrations/'],keywords:['leerness','harness-skill','ai-skill-library',meta.category||'custom'].filter(Boolean),license:'MIT',publishConfig:{access:'public'},harnessSkill:{name:meta.name,version:meta.version,title:meta.title,requiresEnv:meta.requiresEnv||[],sensitiveDataPolicy:meta.sensitiveDataPolicy||'env-reference-only',compatibleHarness:meta.compatibleHarness||'>=3.2.0'}}; write(path.join(libRoot,'package.json'),JSON.stringify(pkg,null,2)+'\n'); ok('스킬 라이브러리 빌드 완료: '+libRoot); info('npm 배포: leerness library publish '+libRoot+' --target npm --execute'); info('git 배포: leerness library publish '+libRoot+' --target git --repo <git-url> --execute'); }
|
|
173
|
-
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)); }
|
|
174
|
-
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); }
|
|
175
|
-
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; }
|
|
176
|
-
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; }
|
|
177
|
-
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); }
|
|
178
|
-
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]',' leerness route <feature|ui|debugging|refactor|release|migration|new-install|skill-library|documentation>','','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')); }
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
function nowIso(){ return new Date().toISOString(); }
|
|
182
|
-
function dateOnly(iso){ return String(iso||nowIso()).slice(0,10); }
|
|
183
|
-
function normalizeSkillMeta(meta, fallbackName){
|
|
184
|
-
meta = meta || {};
|
|
185
|
-
const updated = meta.lastUpdatedAt || meta.updatedAt || meta.learnedAt || meta.migratedAt || nowIso();
|
|
186
|
-
meta.name = slugifyName(meta.name || fallbackName || 'custom-skill');
|
|
187
|
-
meta.version = String(meta.version || '0.1.0');
|
|
188
|
-
meta.title = meta.title || meta.description || meta.name;
|
|
189
|
-
meta.category = meta.category || 'custom';
|
|
190
|
-
meta.compatibleHarness = meta.compatibleHarness || '>=1.0.0';
|
|
191
|
-
meta.sensitiveDataPolicy = meta.sensitiveDataPolicy || 'env-reference-only';
|
|
192
|
-
meta.requiresEnv = Array.from(new Set(meta.requiresEnv || meta.harnessSkill?.requiresEnv || [])).sort();
|
|
193
|
-
meta.lastUpdatedAt = updated;
|
|
194
|
-
meta.lastUpdated = meta.lastUpdated || dateOnly(updated);
|
|
195
|
-
meta.verification = meta.verification || { status:'needs-review', method:'none', verifiedBy:null, verifiedAt:null, checks:[] };
|
|
196
|
-
return meta;
|
|
197
|
-
}
|
|
198
|
-
function isAiVerified(meta){
|
|
199
|
-
const v = (meta||{}).verification || {};
|
|
200
|
-
return v.status === 'passed' && /ai/i.test(String(v.method||'')) && Boolean(v.verifiedAt);
|
|
201
|
-
}
|
|
202
|
-
function verificationLabel(meta){
|
|
203
|
-
const v=(meta||{}).verification||{};
|
|
204
|
-
if(isAiVerified(meta)) return 'AI verified '+String(v.verifiedAt).slice(0,10);
|
|
205
|
-
return v.status || 'needs-review';
|
|
206
|
-
}
|
|
207
|
-
function readSkillLibraryMeta(dir){
|
|
208
|
-
for(const cnd of [path.join(dir,'skill-library.json'),path.join(dir,'skill.json'),path.join(dir,'package.json')]){
|
|
209
|
-
if(!exists(cnd)) continue;
|
|
210
|
-
const data=parseJsonSafe(read(cnd),null); if(!data) continue;
|
|
211
|
-
if(path.basename(cnd)==='package.json') return normalizeSkillMeta({ name:data.harnessSkill?.name||data.name, version:data.version||'0.1.0', title:data.harnessSkill?.title||data.description||data.name, packageName:data.name, description:data.description||'', requiresEnv:data.harnessSkill?.requiresEnv||[], files:data.harnessSkill?.files||[], lastUpdated:data.harnessSkill?.lastUpdated, lastUpdatedAt:data.harnessSkill?.lastUpdatedAt, verification:data.harnessSkill?.verification }, path.basename(dir));
|
|
212
|
-
return normalizeSkillMeta(data, path.basename(dir));
|
|
213
|
-
}
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
function writeSkillLibraryMeta(dir,meta){ write(path.join(dir,'skill-library.json'),JSON.stringify(normalizeSkillMeta(meta,path.basename(dir)),null,2)+'\n'); }
|
|
217
|
-
function validateSkillLibrary(dir,options={}){
|
|
218
|
-
dir=path.resolve(dir); let failures=0; const meta=readSkillLibraryMeta(dir);
|
|
219
|
-
if(!meta||!meta.name){ fail('skill-library.json 또는 skill.json에 name이 필요합니다.'); failures++; }
|
|
220
|
-
const sd=path.join(dir,'skills');
|
|
221
|
-
if(!exists(sd)){ fail('skills/ 폴더가 필요합니다.'); failures++; }
|
|
222
|
-
else if(!skillLibraryFiles(sd).some(f=>f.endsWith('.md'))){ fail('skills/*.md 파일이 최소 1개 필요합니다.'); failures++; }
|
|
223
|
-
const findings=scanSensitivePath(dir);
|
|
224
|
-
if(findings.length){ fail('민감정보 의심 패턴 감지. 업로드/빌드/병합이 차단됩니다.'); findings.slice(0,20).forEach(f=>warn(rel(dir,f.file)+' · '+f.type+' · '+f.sample)); if(findings.length>20) warn('추가 '+(findings.length-20)+'건 생략'); failures+=findings.length; }
|
|
225
|
-
for(const env of new Set(meta?.requiresEnv||[])){ if(!/^[A-Z][A-Z0-9_]*$/.test(env)) warn('환경변수 이름 형식 확인 필요: '+env); }
|
|
226
|
-
if(meta && !meta.lastUpdated) warn('lastUpdated 메타데이터가 없습니다. v1.0.0에서는 표시를 권장합니다.');
|
|
227
|
-
if((options.strictAi||options['strict-ai']) && !isAiVerified(meta)){ fail('AI 검증 메타데이터가 없습니다. `leerness library verify <path> --ai`가 필요합니다.'); failures++; }
|
|
228
|
-
if(!options.silent){ if(failures) fail('스킬 라이브러리 검증 실패: '+failures); else ok('스킬 라이브러리 검증 완료: '+(meta?.name||path.basename(dir))+' · '+verificationLabel(meta)); }
|
|
229
|
-
return {ok:failures===0,failures,meta,findings};
|
|
230
|
-
}
|
|
231
|
-
function verifySkillLibrary(dir,flags={}){
|
|
232
|
-
dir=path.resolve(dir||process.cwd());
|
|
233
|
-
const check=validateSkillLibrary(dir,{silent:false});
|
|
234
|
-
if(!check.ok){ process.exitCode=1; return; }
|
|
235
|
-
if(!flags.ai){ fail('업로드 가능한 검증 기록은 AI 검증으로만 생성됩니다. `--ai`를 붙여 AI 검증 게이트를 명시하세요.'); process.exitCode=1; return; }
|
|
236
|
-
const meta=normalizeSkillMeta(check.meta,path.basename(dir));
|
|
237
|
-
const reviewedAt=nowIso();
|
|
238
|
-
meta.lastUpdatedAt = meta.lastUpdatedAt || reviewedAt;
|
|
239
|
-
meta.lastUpdated = meta.lastUpdated || dateOnly(meta.lastUpdatedAt);
|
|
240
|
-
meta.verification = { status:'passed', method:'ai-assisted-review', verifiedBy:String(flags.reviewer||flags.by||'leerness-ai'), verifiedAt:reviewedAt, checks:['structure','secret-scan','env-reference-only','reusability','migration-readiness','metadata-completeness'] };
|
|
241
|
-
writeSkillLibraryMeta(dir,meta);
|
|
242
|
-
write(path.join(dir,'ai-verification.json'),JSON.stringify({skill:meta.name,version:meta.version,status:'passed',method:'ai-assisted-review',verifiedBy:meta.verification.verifiedBy,verifiedAt:reviewedAt,checks:meta.verification.checks},null,2)+'\n');
|
|
243
|
-
ok('AI 검증 완료: '+meta.name+'@'+meta.version+' · '+dateOnly(reviewedAt));
|
|
244
|
-
}
|
|
245
|
-
function libraryStatus(dir){
|
|
246
|
-
dir=path.resolve(dir||process.cwd()); const meta=readSkillLibraryMeta(dir);
|
|
247
|
-
if(!meta){ fail('스킬 라이브러리 메타데이터를 찾지 못했습니다.'); process.exitCode=1; return; }
|
|
248
|
-
banner();
|
|
249
|
-
log('스킬: '+meta.name+'@'+meta.version);
|
|
250
|
-
log('제목: '+(meta.title||''));
|
|
251
|
-
log('카테고리: '+(meta.category||''));
|
|
252
|
-
log('최종 업데이트: '+(meta.lastUpdated||'unknown')+' ('+(meta.lastUpdatedAt||'unknown')+')');
|
|
253
|
-
log('검증: '+verificationLabel(meta));
|
|
254
|
-
if((meta.requiresEnv||[]).length) log('환경변수: '+meta.requiresEnv.join(', '));
|
|
255
|
-
}
|
|
256
|
-
function updateSkillLock(root,meta,remove=false){
|
|
257
|
-
const lp=path.join(root,'.harness/skills-lock.json');
|
|
258
|
-
const lock=exists(lp)?parseJsonSafe(read(lp),{harnessVersion:VERSION,installedSkills:{}}):{harnessVersion:VERSION,installedSkills:{}};
|
|
259
|
-
lock.harnessVersion=VERSION; lock.updatedAt=nowIso(); lock.installedSkills=lock.installedSkills||{};
|
|
260
|
-
if(remove) delete lock.installedSkills[meta.name];
|
|
261
|
-
else lock.installedSkills[meta.name]={version:meta.version,source:meta.source||'bundled',title:meta.title,requiresEnv:meta.requiresEnv||[],lastUpdated:meta.lastUpdated||dateOnly(meta.lastUpdatedAt),lastUpdatedAt:meta.lastUpdatedAt||nowIso(),verificationStatus:(meta.verification||{}).status||'unknown',verifiedAt:(meta.verification||{}).verifiedAt||null};
|
|
262
|
-
write(lp,JSON.stringify(lock,null,2)+'\n');
|
|
263
|
-
}
|
|
264
|
-
function getSkillMeta(name){
|
|
265
|
-
const metaPath=path.join(PACKS_DIR,name,'skill.json'); if(!exists(metaPath)) return null;
|
|
266
|
-
const meta=parseJsonSafe(read(metaPath),null); if(!meta||!meta.name) return null;
|
|
267
|
-
return normalizeSkillMeta(meta,name);
|
|
268
|
-
}
|
|
269
|
-
function installSkill(root,name,dryRun=false){
|
|
270
|
-
const meta=getSkillMeta(name); if(!meta){ fail('알 수 없는 스킬 라이브러리: '+name); info('사용 가능 목록: '+listSkillPacks().map(x=>x.name).join(', ')); return false; }
|
|
271
|
-
const packRoot=path.join(PACKS_DIR,name); const destRoot=path.join(root,'.harness/skills',name);
|
|
272
|
-
if(dryRun){ info('[dry-run] install skill: '+name+' · updated '+(meta.lastUpdated||'unknown')+' · '+verificationLabel(meta)); return true; }
|
|
273
|
-
fs.mkdirSync(destRoot,{recursive:true});
|
|
274
|
-
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)); } }
|
|
275
|
-
write(path.join(destRoot,'skill.json'),JSON.stringify(meta,null,2)+'\n');
|
|
276
|
-
updateSkillLock(root,meta,false); appendEnvExample(root,meta); return true;
|
|
277
|
-
}
|
|
278
|
-
function learnSkillLibrary(root,flags){
|
|
279
|
-
root=path.resolve(root||process.cwd()); const from=path.resolve(flags.from||path.join(root,'.harness/skills')); const name=slugifyName(flags.name||flags.category||path.basename(from)); const version=String(flags.version||'0.1.0'); const outRoot=path.resolve(flags.out||path.join(root,'.harness/library',name));
|
|
280
|
-
if(!exists(from)){ fail('학습할 스킬 경로가 없습니다: '+from); process.exitCode=1; return; }
|
|
281
|
-
const sourceFiles=skillLibraryFiles(from).filter(f=>isTextFile(f)&&!f.includes(path.sep+'archive'+path.sep));
|
|
282
|
-
if(!sourceFiles.length){ fail('학습 가능한 텍스트 스킬 파일이 없습니다.'); process.exitCode=1; return; }
|
|
283
|
-
fs.mkdirSync(path.join(outRoot,'skills'),{recursive:true}); const requiresEnv=new Set(); const copied=[];
|
|
284
|
-
for(const f of sourceFiles){ const body=read(f); for(const e of inferEnvNamesFromText(body)) requiresEnv.add(e); const base=path.basename(f).replace(/[^a-zA-Z0-9._-]/g,'-'); const destName=base.endsWith('.md')?base:base+'.md'; const dest=path.join(outRoot,'skills',destName); const header=[MARK,'# Learned Skill: '+destName.replace(/\.md$/,''),'','Source: '+rel(root,f),'Learned: '+nowIso(),'Verification: needs-review',''].join('\n'); write(dest,body.includes(MARK)?body:header+'\n'+body); copied.push('skills/'+destName); }
|
|
285
|
-
const t=nowIso(); const meta=normalizeSkillMeta({name,version,title:flags.title||name,description:flags.description||'Verified project skill library extracted from a successful implementation.',category:flags.category||'custom',compatibleHarness:'>=1.0.0',sensitiveDataPolicy:'env-reference-only',requiresEnv:Array.from(requiresEnv).sort(),files:copied,learnedFrom:rel(root,from),learnedAt:t,lastUpdated:dateOnly(t),lastUpdatedAt:t,verification:{status:'needs-review',method:'none',verifiedBy:null,verifiedAt:null,checks:[]}}, name);
|
|
286
|
-
writeSkillLibraryMeta(outRoot,meta);
|
|
287
|
-
write(path.join(outRoot,'README.md'),'# '+meta.title+'\n\n'+meta.description+'\n\n## Metadata\n\n- Version: '+meta.version+'\n- Last updated: '+meta.lastUpdated+'\n- Verification: '+verificationLabel(meta)+'\n\n## Policy\n\nThis library stores environment variable names only. Do not commit real secrets.\n\n## Required env\n\n'+(meta.requiresEnv.map(e=>'- '+e).join('\n')||'- None')+'\n');
|
|
288
|
-
write(path.join(outRoot,'env.example'),meta.requiresEnv.map(e=>e+'=').join('\n')+(meta.requiresEnv.length?'\n':''));
|
|
289
|
-
const result=validateSkillLibrary(outRoot,{silent:true}); if(!result.ok){ fail('학습 결과에 민감정보 또는 구조 문제가 있어 확인이 필요합니다: '+outRoot); process.exitCode=1; return; }
|
|
290
|
-
ok('스킬 라이브러리 학습 완료: '+outRoot); warn('아직 업로드 검증 전입니다. 다음 명령으로 AI 검증을 완료하세요.'); info('leerness library verify '+outRoot+' --ai --reviewer leerness-ai');
|
|
291
|
-
}
|
|
292
|
-
function buildSkillLibrary(dir,flags){
|
|
293
|
-
dir=path.resolve(dir||process.cwd()); const check=validateSkillLibrary(dir,{silent:false}); if(!check.ok){ process.exitCode=1; return; }
|
|
294
|
-
const meta=normalizeSkillMeta(check.meta,path.basename(dir)); const out=path.resolve(flags.out||path.join(dir,'dist')); const packageName=flags.package||meta.packageName||packageSafeName(meta.name); const libRoot=path.join(out,slugifyName(meta.name));
|
|
295
|
-
if(exists(libRoot)) fs.rmSync(libRoot,{recursive:true,force:true}); fs.mkdirSync(libRoot,{recursive:true});
|
|
296
|
-
for(const item of ['README.md','skill-library.json','skill.json','ai-verification.json','env.example','skills','examples','migrations']){ const src=path.join(dir,item); if(exists(src)) copyRecursive(src,path.join(libRoot,item)); }
|
|
297
|
-
writeSkillLibraryMeta(libRoot,meta);
|
|
298
|
-
const pkg={name:packageName,version:meta.version||'0.1.0',description:meta.description||meta.title||meta.name,type:'commonjs',files:['skill-library.json','ai-verification.json','README.md','env.example','skills/','examples/','migrations/'],keywords:['leerness','harness-skill','ai-skill-library',meta.category||'custom'].filter(Boolean),license:'MIT',publishConfig:{access:'public'},harnessSkill:{name:meta.name,version:meta.version,title:meta.title,requiresEnv:meta.requiresEnv||[],sensitiveDataPolicy:meta.sensitiveDataPolicy||'env-reference-only',compatibleHarness:meta.compatibleHarness||'>=1.0.0',lastUpdated:meta.lastUpdated,lastUpdatedAt:meta.lastUpdatedAt,verification:meta.verification}};
|
|
299
|
-
write(path.join(libRoot,'package.json'),JSON.stringify(pkg,null,2)+'\n');
|
|
300
|
-
ok('스킬 라이브러리 빌드 완료: '+libRoot); info('상태 확인: leerness library status '+libRoot); info('npm 배포: leerness library publish '+libRoot+' --target npm --execute');
|
|
301
|
-
}
|
|
302
|
-
function updateSkillLibrary(dir,flags){
|
|
303
|
-
dir=path.resolve(dir||process.cwd()); const from=path.resolve(flags.from||flags.source||''); if(!from||!exists(from)){ fail('업데이트 원본 경로가 필요합니다: --from <path>'); process.exitCode=1; return; }
|
|
304
|
-
const old=readSkillLibraryMeta(dir)||normalizeSkillMeta({name:path.basename(dir)},path.basename(dir)); const sourceFiles=skillLibraryFiles(from).filter(f=>isTextFile(f)&&!f.includes(path.sep+'archive'+path.sep)); if(!sourceFiles.length){ fail('업데이트할 텍스트 스킬 파일이 없습니다.'); process.exitCode=1; return; }
|
|
305
|
-
fs.mkdirSync(path.join(dir,'skills'),{recursive:true}); const copied=[]; const envs=new Set(old.requiresEnv||[]);
|
|
306
|
-
for(const f of sourceFiles){ const body=read(f); for(const e of inferEnvNamesFromText(body)) envs.add(e); const base=path.basename(f).replace(/[^a-zA-Z0-9._-]/g,'-'); const destName=base.endsWith('.md')?base:base+'.md'; write(path.join(dir,'skills',destName),body); copied.push('skills/'+destName); }
|
|
307
|
-
const t=nowIso(); const meta=normalizeSkillMeta({...old,version:String(flags.version||old.version||'0.1.0'),requiresEnv:Array.from(envs).sort(),files:Array.from(new Set([...(old.files||[]),...copied])),lastUpdated:dateOnly(t),lastUpdatedAt:t,updatedFrom:from,verification:{status:'needs-review',method:'updated-after-verification',verifiedBy:null,verifiedAt:null,checks:[]}}, old.name);
|
|
308
|
-
writeSkillLibraryMeta(dir,meta); warn('스킬 라이브러리 업데이트 완료. 검증 상태가 needs-review로 초기화되었습니다.'); info('다음: leerness library verify '+dir+' --ai --reviewer leerness-ai');
|
|
309
|
-
}
|
|
310
|
-
function mergeSkillLibrary(root,source,flags){
|
|
311
|
-
root=path.resolve(root||process.cwd()); source=path.resolve(source||flags.source||''); if(!source||!exists(source)){ fail('병합할 스킬 라이브러리 경로가 필요합니다.'); process.exitCode=1; return; }
|
|
312
|
-
const check=validateSkillLibrary(source,{silent:false,strictAi:true}); if(!check.ok){ process.exitCode=1; return; }
|
|
313
|
-
const meta=normalizeSkillMeta(check.meta,path.basename(source)); const name=slugifyName(meta.name); const dest=path.join(root,'.harness/skills',name); fs.mkdirSync(dest,{recursive:true});
|
|
314
|
-
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'); if(exists(path.join(source,'ai-verification.json'))) fs.copyFileSync(path.join(source,'ai-verification.json'),path.join(dest,'ai-verification.json'));
|
|
315
|
-
updateSkillLock(root,{...meta,source:'library'},false); appendEnvExample(root,{name,title:meta.title||name,requiresEnv:meta.requiresEnv||[]}); ok('검증된 스킬 라이브러리 병합 완료: '+rel(root,dest));
|
|
316
|
-
}
|
|
317
|
-
function migrateSkillLibrary(dir,flags){
|
|
318
|
-
dir=path.resolve(dir||process.cwd()); if(!exists(dir)){ fail('마이그레이션 대상 경로가 없습니다: '+dir); process.exitCode=1; return; }
|
|
319
|
-
const old=readSkillLibraryMeta(dir)||{}; const t=nowIso(); const migrated=normalizeSkillMeta({name:slugifyName(flags.name||old.name||path.basename(dir)),version:String(flags.version||old.version||'0.1.0'),title:flags.title||old.title||old.description||path.basename(dir),description:flags.description||old.description||'Migrated Leerness skill library.',category:flags.category||old.category||'custom',compatibleHarness:'>=1.0.0',sensitiveDataPolicy:'env-reference-only',requiresEnv:Array.from(new Set(old.requiresEnv||old.harnessSkill?.requiresEnv||[])),migratedAt:t,lastUpdated:dateOnly(t),lastUpdatedAt:t,verification:{status:'needs-review',method:'migrated-after-verification',verifiedBy:null,verifiedAt:null,checks:[]}}, path.basename(dir));
|
|
320
|
-
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));
|
|
321
|
-
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); }
|
|
322
|
-
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'); }
|
|
323
|
-
}
|
|
324
|
-
function findProjectRootForPublish(start){
|
|
325
|
-
let cur=path.resolve(start||process.cwd());
|
|
326
|
-
if(exists(cur)&&fs.statSync(cur).isFile()) cur=path.dirname(cur);
|
|
327
|
-
for(;;){
|
|
328
|
-
if(exists(path.join(cur,'.harness'))) return cur;
|
|
329
|
-
const parent=path.dirname(cur);
|
|
330
|
-
if(parent===cur) return null;
|
|
331
|
-
cur=parent;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
function readPublishLocalConfig(dir){
|
|
335
|
-
const projectRoot=findProjectRootForPublish(dir)||process.cwd();
|
|
336
|
-
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')];
|
|
337
|
-
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); } }
|
|
338
|
-
return {path:null,config:{}};
|
|
339
|
-
}
|
|
340
|
-
function deepGet(obj,keys){ for(const k of keys){ if(!obj) return undefined; obj=obj[k]; } return obj; }
|
|
341
|
-
function tokenFromConfig(target,dir){
|
|
342
|
-
const loaded=readPublishLocalConfig(dir); const cfg=loaded.config||{}; const auth=cfg.publishAuth||cfg.auth||{};
|
|
343
|
-
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'];
|
|
344
|
-
for(const k of envKeys.filter(Boolean)){ if(process.env[k]) return {token:process.env[k],source:'env:'+k,configPath:loaded.path}; }
|
|
345
|
-
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']));
|
|
346
|
-
if(direct){ warn('로컬 설정 파일의 직접 토큰을 사용합니다. 가능하면 토큰값 대신 tokenEnv 방식이 더 안전합니다: '+loaded.path); return {token:direct,source:'local-config:'+loaded.path,configPath:loaded.path}; }
|
|
347
|
-
return {token:null,source:null,configPath:loaded.path};
|
|
348
|
-
}
|
|
349
|
-
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; }
|
|
350
|
-
function promptSecret(question){
|
|
351
|
-
return new Promise(resolve=>{
|
|
352
|
-
if(!process.stdin.isTTY||!process.stdout.isTTY){ resolve(''); return; }
|
|
353
|
-
const stdin=process.stdin; let value=''; process.stdout.write(question);
|
|
354
|
-
const cleanup=()=>{ stdin.removeListener('data',onData); try{ stdin.setRawMode(false); }catch{} process.stdout.write('\n'); resolve(value.trim()); };
|
|
355
|
-
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('*'); };
|
|
356
|
-
try{ stdin.setRawMode(true); }catch{} stdin.resume(); stdin.setEncoding('utf8'); stdin.on('data',onData);
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
async function resolvePublishToken(target,dir,flags){
|
|
360
|
-
if(flags.token){ warn('--token 인자는 shell history에 남을 수 있습니다. 가능하면 환경변수나 입력 프롬프트를 사용하세요.'); return {token:String(flags.token),source:'--token'}; }
|
|
361
|
-
if(flags['token-env']&&process.env[String(flags['token-env'])]) return {token:process.env[String(flags['token-env'])],source:'env:'+flags['token-env']};
|
|
362
|
-
const cfgToken=tokenFromConfig(target,dir); if(cfgToken.token) return cfgToken; if(flags['no-prompt']) return {token:null,source:null};
|
|
363
|
-
const label=target==='npm'?'npm access token':'git/GitHub access token'; const token=await promptSecret(label+' 입력: '); return {token,source:token?'interactive-prompt':null};
|
|
364
|
-
}
|
|
365
|
-
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; }
|
|
366
|
-
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); }
|
|
367
|
-
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]); }
|
|
368
|
-
async function publishSkillLibrary(dir,flags){
|
|
369
|
-
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; }
|
|
370
|
-
if(!isAiVerified(check.meta)){ fail('AI 검증된 스킬만 업로드할 수 있습니다. leerness library verify <path> --ai 를 먼저 실행하세요.'); process.exitCode=1; return; }
|
|
371
|
-
if(target==='npm'){
|
|
372
|
-
if(!exists(path.join(dir,'package.json'))){ warn('package.json이 없습니다. 먼저 build를 실행하세요.'); info('leerness library build '+dir); process.exitCode=1; return; }
|
|
373
|
-
const args=['publish','--access','public'].concat(flags.registry?['--registry',flags.registry]:[]);
|
|
374
|
-
if(!execute){ info('[dry-run] AI 검증 통과. 실행 예정: (cd '+dir+') npm '+args.join(' ')); info('실제 배포는 --execute를 붙이세요. --execute 시 npm 토큰을 환경변수/로컬 설정에서 찾고 없으면 입력을 요구합니다.'); return; }
|
|
375
|
-
const cred=await resolvePublishToken('npm',dir,flags);
|
|
376
|
-
if(!cred.token){ fail('npm access token이 필요합니다. LEERNESS_NPM_TOKEN, NPM_TOKEN, NODE_AUTH_TOKEN 또는 .harness/skill-publish.local.json을 설정하거나 프롬프트에 입력하세요.'); process.exitCode=1; return; }
|
|
377
|
-
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;
|
|
378
|
-
}
|
|
379
|
-
if(target==='git'){
|
|
380
|
-
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'));
|
|
381
|
-
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; }
|
|
382
|
-
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; }
|
|
383
|
-
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;
|
|
384
|
-
}
|
|
385
|
-
fail('지원하지 않는 publish target: '+target); process.exitCode=1;
|
|
386
|
-
}
|
|
387
|
-
function libraryGuide(root,flags={}){
|
|
388
|
-
root=path.resolve(root||flags.path||process.cwd()); const target=path.join(root,'.harness/AX_SKILL_LIBRARY_GUIDE.md');
|
|
389
|
-
if(exists(target)){ ok('AX 가이드 위치: '+target); log(read(target)); return; }
|
|
390
|
-
const bundled=path.join(PACKAGE_ROOT,'docs','AX_SKILL_LIBRARY_GUIDE.md');
|
|
391
|
-
if(exists(bundled)){ log(read(bundled)); return; }
|
|
392
|
-
warn('AX 가이드 파일을 찾지 못했습니다. init을 먼저 실행하세요.');
|
|
393
|
-
}
|
|
394
|
-
function libraryCommand(args,flags){
|
|
395
|
-
const sub=args[1]||'help';
|
|
396
|
-
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.3.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; }
|
|
397
|
-
if(sub==='guide') return libraryGuide(args[2]||flags.path||process.cwd(),flags);
|
|
398
|
-
if(sub==='status') return libraryStatus(args[2]||process.cwd());
|
|
399
|
-
if(sub==='validate') return validateSkillLibrary(args[2]||process.cwd(),{silent:false,strictAi:Boolean(flags['strict-ai']||flags.strictAi)});
|
|
400
|
-
if(sub==='verify') return verifySkillLibrary(args[2]||process.cwd(),flags);
|
|
401
|
-
if(sub==='build') return buildSkillLibrary(args[2]||process.cwd(),flags);
|
|
402
|
-
if(sub==='update') return updateSkillLibrary(args[2]||process.cwd(),flags);
|
|
403
|
-
if(sub==='merge') return mergeSkillLibrary(flags.path||process.cwd(),args[2]||flags.source,flags);
|
|
404
|
-
if(sub==='migrate') return migrateSkillLibrary(args[2]||process.cwd(),flags);
|
|
405
|
-
if(sub==='publish'||sub==='upload') return publishSkillLibrary(args[2]||process.cwd(),flags);
|
|
406
|
-
fail('알 수 없는 library 명령: '+sub); process.exitCode=1;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
211
|
function skillDisplayName(meta){ return meta.displayNameKo || meta.titleKo || meta.title || meta.name; }
|
|
410
|
-
function
|
|
411
|
-
function renderSkillMeta(meta){
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
log('');
|
|
431
|
-
info('상세 보기: leerness skill info <name>');
|
|
432
|
-
info('설치 예시: leerness skill add ai-verified-skill-publisher');
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
if(sub==='info'||sub==='show'){
|
|
436
|
-
const name=args[2]; if(!name){ fail('스킬 이름이 필요합니다. 예: leerness skill info commerce-api'); return; }
|
|
437
|
-
const meta=getSkillMeta(name); if(!meta){ fail('알 수 없는 스킬 라이브러리: '+name); info('사용 가능 목록: '+listSkillPacks().map(x=>x.name).join(', ')); return; }
|
|
438
|
-
banner(); renderSkillMeta(meta);
|
|
439
|
-
const packRoot=path.join(PACKS_DIR,name);
|
|
440
|
-
const guide=path.join(packRoot,'README.md');
|
|
441
|
-
if(exists(guide)){ log('\n--- README ---\n'); log(read(guide)); }
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
const name=args[2]; if(!name){ fail('스킬 이름이 필요합니다. 예: leerness skill add commerce-api'); return; }
|
|
445
|
-
if(sub==='add'||sub==='install') return installSkill(root,name,Boolean(flags['dry-run']));
|
|
446
|
-
if(sub==='remove'||sub==='rm') return removeSkill(root,name);
|
|
447
|
-
if(sub==='update') return installSkill(root,name,false);
|
|
448
|
-
fail('알 수 없는 skill 명령: '+sub);
|
|
449
|
-
}
|
|
450
|
-
function status(root){
|
|
451
|
-
root=path.resolve(root||process.cwd()); const vf=path.join(root,'.harness/HARNESS_VERSION'); const version=exists(vf)?read(vf).trim():'not installed'; const missing=Object.keys(coreFiles).filter(f=>!exists(path.join(root,f))); const lp=path.join(root,'.harness/skills-lock.json'); const lock=exists(lp)?parseJsonSafe(read(lp),{installedSkills:{}}):{installedSkills:{}};
|
|
452
|
-
banner(); log('대상: '+root); log('버전: '+version); log('파일: '+(Object.keys(coreFiles).length-missing.length)+'/'+Object.keys(coreFiles).length); if(missing.length){ warn('누락 파일'); missing.forEach(x=>log(' - '+x)); } else ok('필수 파일 모두 존재');
|
|
453
|
-
const names=Object.keys(lock.installedSkills||{}); log('설치 스킬: '+(names.length?names.join(', '):'없음'));
|
|
454
|
-
for(const n of names){ const m=lock.installedSkills[n]; log(' - '+n+'@'+(m.version||'?')+' · updated '+(m.lastUpdated||'unknown')+' · '+(m.verificationStatus||'unknown')); }
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const taskRouteData = {
|
|
458
|
-
feature: {read:['project-brief.md','current-state.md','context-routing.md','architecture.md','context-map.md','feature-contracts.md','skills/feature-implementation.md','testing-strategy.md'], update:['current-state.md','task-log.md','session-handoff.md','feature-contracts.md','context-map.md when important paths change']},
|
|
459
|
-
ui: {read:['project-brief.md','current-state.md','design-system.md','feature-contracts.md','context-map.md','skills/ui-consistency.md'], update:['design-system.md','feature-contracts.md','current-state.md','task-log.md','session-handoff.md']},
|
|
460
|
-
debugging: {read:['current-state.md','task-log.md','feature-contracts.md','testing-strategy.md','skills/debugging.md','context-map.md'], update:['task-log.md','current-state.md','session-handoff.md','testing-strategy.md when regression coverage changes']},
|
|
461
|
-
refactor: {read:['architecture.md','decisions.md','guardrails.md','testing-strategy.md','skills/refactoring.md'], update:['architecture.md when structure changes','decisions.md when reasoning matters','task-log.md','session-handoff.md']},
|
|
462
|
-
release: {read:['release-checklist.md','testing-strategy.md','current-state.md','decisions.md','secret-policy.md'], update:['release-checklist.md','task-log.md','current-state.md','session-handoff.md']},
|
|
463
|
-
migration: {read:['AX_MIGRATION_GUIDE.md','context-routing.md','writeback-policy.md','architecture.md','decisions.md','release-checklist.md'], update:['current-state.md','task-log.md','session-handoff.md','context-map.md','release-checklist.md','decisions.md when needed']},
|
|
464
|
-
'new-install': {read:['AX_NEW_PROJECT_GUIDE.md','project-brief.md','context-map.md','guardrails.md','package/config files'], update:['project-brief.md','architecture.md','context-map.md','design-system.md','feature-contracts.md','release-checklist.md','testing-strategy.md','current-state.md','session-handoff.md']},
|
|
465
|
-
'skill-library': {read:['AX_SKILL_LIBRARY_GUIDE.md','skill-index.md','skills-lock.json','secret-policy.md'], update:['skill-index.md','skills-lock.json','task-log.md','session-handoff.md']},
|
|
466
|
-
documentation: {read:['writeback-policy.md','context-routing.md','task-type-map.md'], update:['the specific memory file defined by writeback-policy.md','task-log.md','session-handoff.md']}
|
|
467
|
-
};
|
|
468
|
-
function routeCommand(task){
|
|
469
|
-
banner();
|
|
470
|
-
if(!task||task==='list'){
|
|
471
|
-
log('사용 가능한 task type: '+Object.keys(taskRouteData).join(', '));
|
|
472
|
-
log('예: leerness route release');
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
const r=taskRouteData[task];
|
|
476
|
-
if(!r){ fail('알 수 없는 task type: '+task); log('사용 가능: '+Object.keys(taskRouteData).join(', ')); process.exitCode=1; return; }
|
|
477
|
-
log('Task route: '+task);
|
|
478
|
-
log('\nRead before work:'); r.read.forEach(x=>log(' - .harness/'+x));
|
|
479
|
-
log('\nUpdate after work:'); r.update.forEach(x=>log(' - .harness/'+x));
|
|
480
|
-
log('\nPolicy: read AGENTS.md, context-routing.md, writeback-policy.md, then the files above. Do not store secrets in harness files.');
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
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]',' leerness route <feature|ui|debugging|refactor|release|migration|new-install|skill-library|documentation>','','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.3.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')); }
|
|
484
|
-
|
|
485
|
-
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==='route') return routeCommand(args[1]||'list'); if(cmd==='skill') return skillCommand(args,flags); if(cmd==='library') return libraryCommand(args,flags); help(); process.exitCode=1; }
|
|
212
|
+
function verificationLabel(meta){ const v=(meta||{}).verification||{}; return v.status ? (v.status+(v.verifiedAt?' '+String(v.verifiedAt).slice(0,10):'')) : 'unknown'; }
|
|
213
|
+
function renderSkillMeta(meta){ log('- '+meta.name+'@'+meta.version+' · '+skillDisplayName(meta)); if(meta.categoryKo||meta.category) log(' 분류: '+(meta.categoryKo||meta.category)); if(meta.description) log(' 설명: '+meta.description); if(Array.isArray(meta.capabilities)&&meta.capabilities.length){ log(' 가능한 작업:'); meta.capabilities.forEach(x=>log(' - '+x)); } log(' 업데이트: '+(meta.lastUpdated||'unknown')+' · 검증: '+verificationLabel(meta)); if((meta.requiresEnv||[]).length) log(' 필요한 환경변수: '+meta.requiresEnv.join(', ')); }
|
|
214
|
+
function skillCommand(args,flags){ const sub=args[1]||'list'; const root=path.resolve(flags.path||process.cwd()); if(sub==='list'){ banner(); log('사용 가능한 스킬 라이브러리'); for(const p of listSkillPacks()) renderSkillMeta(p); return; } if(sub==='info'||sub==='show'){ const name=args[2]; if(!name) return fail('스킬 이름이 필요합니다.'); const meta=getSkillMeta(name); if(!meta) return fail('알 수 없는 스킬: '+name); banner(); renderSkillMeta(meta); const rp=path.join(PACKS_DIR,name,'README.md'); if(exists(rp)){ log('\n--- README ---\n'); log(read(rp)); } return; } const name=args[2]; if(!name) return fail('스킬 이름이 필요합니다.'); if(sub==='add'||sub==='install'||sub==='update') return installSkill(root,name,Boolean(flags['dry-run'])); if(sub==='remove'||sub==='rm') return removeSkill(root,name); if(sub==='learn') return learnSkillLibrary(root,{...flags,name}); fail('알 수 없는 skill 명령: '+sub); }
|
|
215
|
+
|
|
216
|
+
function skillLibraryFiles(dir){ const out=[]; if(!exists(dir)) return out; function walk(p){ const st=fs.statSync(p); if(st.isDirectory()){ const b=path.basename(p); if(['node_modules','.git','dist'].includes(b)) return; for(const n of fs.readdirSync(p)) walk(path.join(p,n)); } else if(st.isFile()) out.push(p); } walk(dir); return out; }
|
|
217
|
+
function inferEnvNames(body){ const set=new Set(); const re=/\b[A-Z][A-Z0-9_]{3,}\b/g; let m; while((m=re.exec(body))){ const v=m[0]; if(/(KEY|TOKEN|SECRET|PASSWORD|CLIENT|VENDOR|ID|URL|HOST|BUCKET|PROJECT)/.test(v)) set.add(v); } return Array.from(set).sort(); }
|
|
218
|
+
function readSkillLibraryMeta(dir){ for(const cnd of [path.join(dir,'skill-library.json'),path.join(dir,'skill.json'),path.join(dir,'package.json')]){ if(!exists(cnd)) continue; const data=parseJsonSafe(read(cnd),null); if(!data) continue; if(path.basename(cnd)==='package.json') return {name:data.harnessSkill?.name||data.name,version:data.version||'0.1.0',title:data.harnessSkill?.title||data.description||data.name,description:data.description||'',requiresEnv:data.harnessSkill?.requiresEnv||[],verification:data.harnessSkill?.verification,lastUpdated:data.harnessSkill?.lastUpdated,lastUpdatedAt:data.harnessSkill?.lastUpdatedAt}; return data; } return null; }
|
|
219
|
+
function validateSkillLibrary(dir,opts={}){ dir=path.resolve(dir); let failures=0; const meta=readSkillLibraryMeta(dir); if(!meta||!meta.name){ failures++; fail('skill-library.json 또는 skill.json에 name이 필요합니다.'); } const sd=path.join(dir,'skills'); if(!exists(sd)||!skillLibraryFiles(sd).some(f=>f.endsWith('.md'))){ failures++; fail('skills/*.md 파일이 필요합니다.'); } const findings=scanSensitivePath(dir); if(findings.length){ failures+=findings.length; fail('민감정보 의심 패턴 감지.'); findings.slice(0,10).forEach(f=>warn(rel(dir,f.file)+' · '+f.type)); } if(opts.strictAi){ const v=(meta||{}).verification||{}; if(!(v.status==='passed'&&/ai/i.test(String(v.method||''))&&v.verifiedAt)){ failures++; fail('AI 검증 메타데이터가 필요합니다.'); } } if(!opts.silent){ if(failures) fail('검증 실패: '+failures); else ok('스킬 라이브러리 검증 완료: '+meta.name); } return {ok:failures===0,meta,findings}; }
|
|
220
|
+
function learnSkillLibrary(root,flags){ root=path.resolve(root||process.cwd()); const from=path.resolve(flags.from||path.join(root,'.harness/skills')); const name=slug(flags.name||path.basename(from)); const outRoot=path.resolve(flags.out||path.join(root,'.harness/library',name)); if(!exists(from)){ fail('학습할 스킬 경로가 없습니다: '+from); process.exitCode=1; return; } const files=skillLibraryFiles(from).filter(f=>isTextFile(f)&&!f.includes(path.sep+'archive'+path.sep)); fs.mkdirSync(path.join(outRoot,'skills'),{recursive:true}); const envs=new Set(), copied=[]; for(const f of files){ const body=read(f); inferEnvNames(body).forEach(e=>envs.add(e)); const dest='skills/'+path.basename(f).replace(/[^a-zA-Z0-9._-]/g,'-'); write(path.join(outRoot,dest),body); copied.push(dest); } const meta={name,version:String(flags.version||'0.1.0'),title:flags.title||name,description:flags.description||'Learned Leerness skill library.',category:flags.category||'custom',requiresEnv:Array.from(envs).sort(),files:copied,lastUpdated:today(),lastUpdatedAt:now(),verification:{status:'needs-review',method:'none',verifiedBy:null,verifiedAt:null,checks:[]}}; write(path.join(outRoot,'skill-library.json'),JSON.stringify(meta,null,2)+'\n'); write(path.join(outRoot,'README.md'),'# '+meta.title+'\n\n'+meta.description+'\n'); ok('스킬 라이브러리 학습 완료: '+outRoot); info('다음: leerness library verify '+outRoot+' --ai --reviewer leerness-ai'); }
|
|
221
|
+
function verifySkillLibrary(dir,flags){ dir=path.resolve(dir||process.cwd()); const res=validateSkillLibrary(dir,{silent:false}); if(!res.ok){ process.exitCode=1; return; } const meta=res.meta; meta.verification={status:'passed',method:'ai-assisted-review',verifiedBy:String(flags.reviewer||flags.by||'leerness-ai'),verifiedAt:now(),checks:['structure','secret-scan','env-reference-only','metadata']}; meta.lastUpdated=today(); meta.lastUpdatedAt=now(); write(path.join(dir,'skill-library.json'),JSON.stringify(meta,null,2)+'\n'); write(path.join(dir,'ai-verification.json'),JSON.stringify(meta.verification,null,2)+'\n'); ok('AI 검증 완료: '+meta.name); }
|
|
222
|
+
function buildSkillLibrary(dir,flags){ dir=path.resolve(dir||process.cwd()); const res=validateSkillLibrary(dir,{silent:false,strictAi:Boolean(flags['strict-ai'])}); if(!res.ok){ process.exitCode=1; return; } const meta=res.meta; const out=path.resolve(flags.out||path.join(dir,'dist')); const libRoot=path.join(out,slug(meta.name)); if(exists(libRoot)) fs.rmSync(libRoot,{recursive:true,force:true}); fs.mkdirSync(libRoot,{recursive:true}); for(const item of ['README.md','skill-library.json','skill.json','ai-verification.json','env.example','skills','examples','migrations']){ const src=path.join(dir,item); if(exists(src)) copyRecursive(src,path.join(libRoot,item)); } const pkg={name:flags.package||('leerness-skill-'+slug(meta.name)),version:meta.version||'0.1.0',description:meta.description||meta.title||meta.name,type:'commonjs',files:['skill-library.json','ai-verification.json','README.md','env.example','skills/','examples/','migrations/'],keywords:['leerness','harness-skill','ai-skill-library'],license:'MIT',publishConfig:{access:'public'},harnessSkill:meta}; write(path.join(libRoot,'package.json'),JSON.stringify(pkg,null,2)+'\n'); ok('스킬 라이브러리 빌드 완료: '+libRoot); }
|
|
223
|
+
function resolvePublishToken(target,flags){ if(flags['token-env']&&process.env[flags['token-env']]) return process.env[flags['token-env']]; const names=target==='npm'?['LEERNESS_NPM_TOKEN','NPM_TOKEN','NODE_AUTH_TOKEN']:['LEERNESS_GIT_TOKEN','LEERNESS_GITHUB_TOKEN','GITHUB_TOKEN','GH_TOKEN']; for(const n of names) if(process.env[n]) return process.env[n]; return null; }
|
|
224
|
+
function publishSkillLibrary(dir,flags){ dir=path.resolve(dir||process.cwd()); const target=String(flags.target||'npm'); const execute=Boolean(flags.execute); const res=validateSkillLibrary(dir,{silent:false,strictAi:true}); if(!res.ok){ process.exitCode=1; return; } if(!execute){ info('[dry-run] '+target+' publish target: '+dir); info('실제 업로드는 --execute가 필요합니다.'); return; } const token=resolvePublishToken(target,flags); if(!token && flags['no-prompt']){ fail('업로드 토큰이 없습니다. 환경변수 또는 --token-env를 설정하세요.'); process.exitCode=1; return; } if(target==='npm'){ const env={...process.env}; if(token) env.NODE_AUTH_TOKEN=token; const r=childProcess.spawnSync('npm',['publish','--access','public'],{cwd:dir,stdio:'inherit',env,shell:process.platform==='win32'}); process.exitCode=r.status||0; return; } if(target==='git'){ const repo=flags.repo||DEFAULT_GIT_REPOSITORY; info('Git target: '+repo); 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(!exists(path.join(dir,'.git'))) run('git',['init']); try{ run('git',['remote','add','origin',repo]); }catch{} run('git',['add','.']); run('git',['commit','-m',flags.message||('Publish skill library '+res.meta.name)]); run('git',['branch','-M',flags.branch||'main']); run('git',['push','-u','origin',flags.branch||'main']); return; } fail('지원하지 않는 target: '+target); }
|
|
225
|
+
function libraryGuide(root){ root=path.resolve(root||process.cwd()); const p=path.join(root,'.harness/AX_SKILL_LIBRARY_GUIDE.md'); if(exists(p)) log(read(p)); else log(coreFiles['.harness/AX_SKILL_LIBRARY_GUIDE.md']); }
|
|
226
|
+
function libraryCommand(args,flags){ const sub=args[1]||'help'; if(sub==='guide') return libraryGuide(args[2]||flags.path||process.cwd()); if(sub==='validate') return validateSkillLibrary(args[2]||process.cwd(),{silent:false,strictAi:Boolean(flags['strict-ai'])}); if(sub==='verify') return verifySkillLibrary(args[2]||process.cwd(),flags); if(sub==='build') return buildSkillLibrary(args[2]||process.cwd(),flags); if(sub==='publish'||sub==='upload') return publishSkillLibrary(args[2]||process.cwd(),flags); if(sub==='status'){ const meta=readSkillLibraryMeta(args[2]||process.cwd()); if(!meta) return fail('메타데이터 없음'); renderSkillMeta(meta); return; } fail('알 수 없는 library 명령: '+sub); }
|
|
227
|
+
|
|
228
|
+
const routeData={feature:{read:['project-brief.md','current-state.md','architecture.md','context-map.md','feature-contracts.md'],update:['current-state.md','task-log.md','session-handoff.md','feature-contracts.md']},ui:{read:['design-system.md','feature-contracts.md','context-map.md'],update:['design-system.md','feature-contracts.md','current-state.md','task-log.md']},debugging:{read:['current-state.md','task-log.md','feature-contracts.md','testing-strategy.md'],update:['task-log.md','current-state.md','session-handoff.md']},refactor:{read:['architecture.md','decisions.md','guardrails.md'],update:['architecture.md','decisions.md','task-log.md']},release:{read:['release-checklist.md','testing-strategy.md','current-state.md','decisions.md','secret-policy.md'],update:['release-checklist.md','task-log.md','current-state.md','session-handoff.md']},migration:{read:['AX_MIGRATION_GUIDE.md','context-routing.md','writeback-policy.md'],update:['Only missing files by default','Use --force only when requested']},'new-install':{read:['AX_NEW_PROJECT_GUIDE.md','actual project files'],update:['project-brief.md','architecture.md','context-map.md','design-system.md','feature-contracts.md','release-checklist.md']},'skill-library':{read:['AX_SKILL_LIBRARY_GUIDE.md','skill-index.md','secret-policy.md'],update:['skill-index.md','skills-lock.json','task-log.md']},documentation:{read:['writeback-policy.md','context-routing.md'],update:['specific memory file','task-log.md','session-handoff.md']}};
|
|
229
|
+
function routeCommand(task){ banner(); if(!task||task==='list'){ log('사용 가능한 task type: '+Object.keys(routeData).join(', ')); return; } const r=routeData[task]; if(!r){ fail('알 수 없는 task type: '+task); process.exitCode=1; return; } log('Task route: '+task); log('\nRead before work:'); r.read.forEach(x=>log(' - .harness/'+x)); log('\nUpdate after work:'); r.update.forEach(x=>log(' - .harness/'+x)); }
|
|
230
|
+
function help(){ log(['Leerness v'+VERSION,'','Usage:',' leerness init [path] [--yes] [--skills recommended|all|office,commerce-api] [--force]',' leerness migrate [path] [--dry-run] [--force]',' leerness status [path]',' leerness verify [path]',' leerness route <feature|ui|debugging|refactor|release|migration|new-install|skill-library|documentation>','','Skills:',' leerness skill list',' leerness skill info <name>',' leerness skill add <name> [--path <project>]',' leerness skill remove <name> [--path <project>]',' leerness skill learn <name> --from <validated-skill-path>','','Skill library:',' leerness library guide [path]',' leerness library validate <path> [--strict-ai]',' leerness library verify <path> --ai --reviewer leerness-ai',' leerness library build <path> [--package leerness-skill-name]',' leerness library publish <built-library> --target npm|git [--execute]',''].join('\n')); }
|
|
231
|
+
async function main(){ const parsed=parseArgs(process.argv.slice(2)); const args=parsed.positionals, 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==='route') return routeCommand(args[1]||'list'); if(cmd==='skill') return skillCommand(args,flags); if(cmd==='library') return libraryCommand(args,flags); help(); process.exitCode=1; }
|
|
486
232
|
main().catch(err=>{ fail(err.stack||err.message); process.exit(1); });
|