@angar2/taskery 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/bin/create.js +52 -0
- package/bin/init.js +118 -0
- package/bin/lib.js +107 -0
- package/bin/taskery.js +57 -0
- package/bin/update.js +171 -0
- package/package.json +43 -0
- package/template/.claude/hooks/closed-immutable.sh +74 -0
- package/template/.claude/hooks/git-guard.sh +74 -0
- package/template/.claude/hooks/pre-commit-verify.sh +97 -0
- package/template/.claude/settings.json +19 -0
- package/template/.claude/skills/plan-init.md +140 -0
- package/template/.claude/skills/project-init.md +193 -0
- package/template/.claude/skills/refine.md +177 -0
- package/template/.claude/skills/task-close.md +178 -0
- package/template/.claude/skills/task-dev.md +155 -0
- package/template/.claude/skills/task-init.md +142 -0
- package/template/.claude/skills/task-plan.md +210 -0
- package/template/.claude/skills/task-test.md +189 -0
- package/template/.project/FRICTION_LOG.md +23 -0
- package/template/.project/changelog/.gitkeep +0 -0
- package/template/.project/flows/.gitkeep +0 -0
- package/template/.project/plans/.gitkeep +0 -0
- package/template/.project/rules/GIT_RULE.md +169 -0
- package/template/.project/rules/TASK_DOC_RULE.md +614 -0
- package/template/.project/shared/received/completed/.gitkeep +0 -0
- package/template/.project/shared/sent/completed/.gitkeep +0 -0
- package/template/.project/tasks/.gitkeep +0 -0
- package/template/CLAUDE.md +106 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 angar2
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# taskery
|
|
2
|
+
|
|
3
|
+
> Claude Code 메인 세션을 위한 *가벼운 task 시스템*.
|
|
4
|
+
> **1 메인 세션 + 스킬 8종 + catastrophic hook 3종 + npx 배포**.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 한 줄 정신
|
|
9
|
+
|
|
10
|
+
practice 영역(SW 개발)을 process로 강제하지 않는다.
|
|
11
|
+
|
|
12
|
+
- **Process는 자동화 OK, Practice는 자유롭게 + 사용자 판단 신뢰** — 결정적 영역(린트/타입/빌드)만 강제, 휴리스틱은 강제 X
|
|
13
|
+
- **Catastrophic만 hook 차단, 형식 위반은 instruction + 대화** — 합리적 변형 차단 사고 회피
|
|
14
|
+
- **Top-down 선제적 작성 금지, bottoms-up** — 진짜 데이터 모이면 그때 추가 (PLAYBOOK 카탈로그 + FRICTION_LOG 패턴 ≥ 3회 트리거)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 빠른 시작
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# 새 프로젝트
|
|
22
|
+
npx -p @angar2/taskery create-taskery <project-name>
|
|
23
|
+
|
|
24
|
+
# 기존 프로젝트에 설치
|
|
25
|
+
cd <your-project>
|
|
26
|
+
npx @angar2/taskery init
|
|
27
|
+
|
|
28
|
+
# 최신 버전 동기화
|
|
29
|
+
npx @angar2/taskery update
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
> 글로벌 install (`npm install -g @angar2/taskery`) 후엔 `taskery init` / `create-taskery <name>` 단축 명령 사용 가능.
|
|
33
|
+
|
|
34
|
+
요구 사항: Node.js >=18, Claude Code (CLI / VS Code / JetBrains).
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 스킬 8종 — project > plan > task 위계 + 회고
|
|
39
|
+
|
|
40
|
+
| 스킬 | 레벨 | 역할 |
|
|
41
|
+
|------|------|------|
|
|
42
|
+
| `/project-init` | project | PROJECT/AGENT-GUIDE/LINKED-REPOS/.env 골격 생성 (1회성) |
|
|
43
|
+
| `/plan-init` | plan | `.project/plans/<vX.X>/` 안 기획 문서 작성 |
|
|
44
|
+
| `/task-init` | task | task.md 빈 골격 + status=draft |
|
|
45
|
+
| `/task-plan` | task | Requirements/Scope/Dev Plan/Test Plan 작성 |
|
|
46
|
+
| `/task-dev` | task | Phase 순서 구현 + self-check |
|
|
47
|
+
| `/task-test` | task | 격리 세션 검증 (confirmation bias 회피) |
|
|
48
|
+
| `/task-close` | task | 검증 명령 게이트 + 커밋 + dev 병합 |
|
|
49
|
+
| `/refine` | meta | FRICTION_LOG 정독 + 패턴 감지 + bottoms-up 보강 제안 |
|
|
50
|
+
|
|
51
|
+
명령어 직접 호출(`/task-init` 등) 또는 메인 세션이 frontmatter description 매칭으로 자동 발동.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Catastrophic Hook 3종
|
|
56
|
+
|
|
57
|
+
| Hook | 영역 | 잡는 것 |
|
|
58
|
+
|------|------|--------|
|
|
59
|
+
| `git-guard.sh` | PreToolUse(Bash) | main/dev 직접 커밋 / `--force` / `--no-verify` / `branch -D` / `reset --hard` |
|
|
60
|
+
| `pre-commit-verify.sh` | PreToolUse(Bash) | `git commit` 시 검증 명령(린트/타입/빌드/테스트) 모두 PASS 게이트 |
|
|
61
|
+
| `closed-immutable.sh` | PreToolUse(Write\|Edit) | `closed` 상태 task.md 본 파일 재수정 차단 |
|
|
62
|
+
|
|
63
|
+
**잘 지키면 hook 작동 0회 (무해). catastrophic만 차단.**
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 디렉토리 구조 (사용자 프로젝트 — `npx @angar2/taskery init` 후)
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
my-app/
|
|
71
|
+
├─ CLAUDE.md # 메인 세션 진입점
|
|
72
|
+
├─ .taskery-manifest.json # 자산 manifest (npx 자동 갱신)
|
|
73
|
+
├─ .claude/
|
|
74
|
+
│ ├─ settings.json # hook 등록
|
|
75
|
+
│ ├─ skills/ # 8 스킬
|
|
76
|
+
│ └─ hooks/ # 3 catastrophic 안전망
|
|
77
|
+
└─ .project/
|
|
78
|
+
├─ PROJECT.md # /project-init 생성
|
|
79
|
+
├─ AGENT-GUIDE.md # /project-init 생성
|
|
80
|
+
├─ rules/
|
|
81
|
+
│ ├─ TASK_DOC_RULE.md # task 양식
|
|
82
|
+
│ └─ GIT_RULE.md # git 정책
|
|
83
|
+
├─ plans/ # /plan-init 생성
|
|
84
|
+
├─ tasks/ # /task-init 생성
|
|
85
|
+
├─ flows/ # /task-dev 갱신
|
|
86
|
+
├─ changelog/ # /task-close 갱신
|
|
87
|
+
├─ shared/ # 멀티리포 메시지
|
|
88
|
+
└─ FRICTION_LOG.md # 짜증 누적
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 상세 문서
|
|
94
|
+
|
|
95
|
+
> 본 리포 spec 문서는 npm 패키지에 포함되지 않으므로 GitHub에서 정독.
|
|
96
|
+
|
|
97
|
+
- [plan/OVERVIEW.md](https://github.com/angar2/taskery/blob/main/plan/OVERVIEW.md) — 진입 + 큰 그림 + 단일 진실 소스
|
|
98
|
+
- [plan/SKILLS.md](https://github.com/angar2/taskery/blob/main/plan/SKILLS.md) — 스킬 8종 명세 + 흐름
|
|
99
|
+
- [plan/TASK-DOC.md](https://github.com/angar2/taskery/blob/main/plan/TASK-DOC.md) — 태스크 양식 + 7 상태
|
|
100
|
+
- [plan/HOOKS.md](https://github.com/angar2/taskery/blob/main/plan/HOOKS.md) — 3 catastrophic hook 정책
|
|
101
|
+
- [plan/DISTRIBUTION.md](https://github.com/angar2/taskery/blob/main/plan/DISTRIBUTION.md) — npx 배포 + manifest
|
|
102
|
+
- [plan/DECISIONS.md](https://github.com/angar2/taskery/blob/main/plan/DECISIONS.md) — 핵심 의사결정
|
|
103
|
+
- [plan/PLAYBOOK.md](https://github.com/angar2/taskery/blob/main/plan/PLAYBOOK.md) — 미래 옵션 카탈로그
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 라이선스
|
|
108
|
+
|
|
109
|
+
[MIT](LICENSE)
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 수정 이력
|
|
114
|
+
|
|
115
|
+
| 날짜 | 변경 사항 |
|
|
116
|
+
|------|----------|
|
|
117
|
+
| 2026-05-09 | 신규 작성 — npm 페이지 진입 문서 |
|
package/bin/create.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bin/create.js
|
|
4
|
+
* `npx -p @angar2/taskery create-taskery <project-name>` — 새 디렉토리 생성 + 진입 + init 흐름.
|
|
5
|
+
*
|
|
6
|
+
* 동작:
|
|
7
|
+
* 1. <project-name> 인자 검증 (없으면 에러)
|
|
8
|
+
* 2. 동일 이름 디렉토리 존재 검사 (있으면 에러)
|
|
9
|
+
* 3. mkdir <project-name> + cd
|
|
10
|
+
* 4. init.js 호출 (현재 디렉토리에 카피)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { spawnSync } = require('child_process');
|
|
16
|
+
|
|
17
|
+
function fail(msg) {
|
|
18
|
+
console.error(`create-taskery: ${msg}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const arg = process.argv[2];
|
|
23
|
+
if (!arg) {
|
|
24
|
+
fail(`프로젝트 이름 인자 필요. 예: npx -p @angar2/taskery create-taskery my-app`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 안전한 디렉토리 이름 검증 — 절대경로 / .. / 빈 문자열 등 차단
|
|
28
|
+
if (arg.startsWith('/') || arg.includes('..') || arg.trim() === '') {
|
|
29
|
+
fail(`'${arg}'는 유효한 디렉토리 이름이 아님. 상대 경로 + 단순 이름 사용.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const targetDir = path.resolve(process.cwd(), arg);
|
|
33
|
+
|
|
34
|
+
if (fs.existsSync(targetDir)) {
|
|
35
|
+
fail(`'${arg}' 이미 존재. 다른 이름 사용하거나 'cd ${arg} && npx @angar2/taskery init' 호출.`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(`create-taskery: '${arg}' 생성 + taskery init...`);
|
|
39
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
40
|
+
|
|
41
|
+
const initScript = path.resolve(__dirname, 'init.js');
|
|
42
|
+
const r = spawnSync(process.execPath, [initScript], {
|
|
43
|
+
cwd: targetDir,
|
|
44
|
+
stdio: 'inherit',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (r.status !== 0) {
|
|
48
|
+
console.error(`create-taskery: init 실패 — '${targetDir}' 정리는 사용자 직접 수행.`);
|
|
49
|
+
process.exit(r.status ?? 1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(`\n다음: cd ${arg}`);
|
package/bin/init.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bin/init.js
|
|
4
|
+
* `npx @angar2/taskery init` 본체 — 현재 디렉토리에 template/ 자산 카피 + manifest 생성.
|
|
5
|
+
*
|
|
6
|
+
* 동작:
|
|
7
|
+
* 1. cwd에 .taskery-manifest.json 이미 있으면 경고 + confirm
|
|
8
|
+
* 2. template/ 안 *모든 파일* cwd로 카피 (디렉토리 자동 생성)
|
|
9
|
+
* 3. *.local.md 사용자 오버라이드 충돌 검사 (덮어쓰지 X)
|
|
10
|
+
* 4. .taskery-manifest.json 박음 (version + installed_at + files{path,hash,core,managed} 맵)
|
|
11
|
+
* 5. 결과 요약 출력 + 다음 단계 안내 (`/project-init`)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const readline = require('readline');
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
MANIFEST_NAME,
|
|
20
|
+
findTemplateDir,
|
|
21
|
+
walkTemplate,
|
|
22
|
+
copyFile,
|
|
23
|
+
writeManifest,
|
|
24
|
+
readManifest,
|
|
25
|
+
getPackageVersion,
|
|
26
|
+
isLocalOverride,
|
|
27
|
+
} = require('./lib');
|
|
28
|
+
|
|
29
|
+
async function confirm(msg) {
|
|
30
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
rl.question(`${msg} (y/N) `, (ans) => {
|
|
33
|
+
rl.close();
|
|
34
|
+
resolve(ans.trim().toLowerCase() === 'y');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function main() {
|
|
40
|
+
const cwd = process.cwd();
|
|
41
|
+
const templateDir = findTemplateDir();
|
|
42
|
+
const manifestPath = path.join(cwd, MANIFEST_NAME);
|
|
43
|
+
|
|
44
|
+
// 1. 기존 manifest 검사
|
|
45
|
+
if (fs.existsSync(manifestPath)) {
|
|
46
|
+
const ok = await confirm(
|
|
47
|
+
`${MANIFEST_NAME} 이미 존재 — taskery 이미 설치된 디렉토리. 'update' 사용 권장. 그래도 init 강행할까?`,
|
|
48
|
+
);
|
|
49
|
+
if (!ok) {
|
|
50
|
+
console.log('취소 — 종료.');
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2. template/ 정독 + 해시 맵
|
|
56
|
+
const templateFiles = walkTemplate(templateDir);
|
|
57
|
+
const fileEntries = Object.entries(templateFiles);
|
|
58
|
+
|
|
59
|
+
console.log(`\ntaskery v${getPackageVersion()} init`);
|
|
60
|
+
console.log(`대상: ${cwd}`);
|
|
61
|
+
console.log(`카피 대상: ${fileEntries.length}개 파일\n`);
|
|
62
|
+
|
|
63
|
+
// 3. 카피 + manifest 빌드
|
|
64
|
+
const manifestFiles = {};
|
|
65
|
+
let copied = 0;
|
|
66
|
+
let skipped = 0;
|
|
67
|
+
|
|
68
|
+
for (const [rel, hash] of fileEntries) {
|
|
69
|
+
const src = path.join(templateDir, rel);
|
|
70
|
+
const dst = path.join(cwd, rel);
|
|
71
|
+
|
|
72
|
+
// .local.md suffix는 사용자 영역 — taskery template에는 없어야 정상이지만 방어적으로 스킵
|
|
73
|
+
if (isLocalOverride(rel)) {
|
|
74
|
+
console.log(` skip (.local override 영역): ${rel}`);
|
|
75
|
+
skipped++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 사용자 .local.md 충돌 검사 — 사용자가 직접 만든 *.local.md 보호
|
|
80
|
+
// 본 init은 신규 설치 default, 충돌 거의 없지만 update 흐름과 일관성 유지
|
|
81
|
+
if (fs.existsSync(dst)) {
|
|
82
|
+
const ok = await confirm(` '${rel}' 이미 존재 — 덮어쓸까?`);
|
|
83
|
+
if (!ok) {
|
|
84
|
+
console.log(` skip: ${rel}`);
|
|
85
|
+
skipped++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
copyFile(src, dst);
|
|
91
|
+
manifestFiles[rel] = { hash, core: true, managed: true };
|
|
92
|
+
copied++;
|
|
93
|
+
console.log(` copy: ${rel}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 4. manifest 박음
|
|
97
|
+
const manifest = {
|
|
98
|
+
version: getPackageVersion(),
|
|
99
|
+
installed_at: new Date().toISOString(),
|
|
100
|
+
files: manifestFiles,
|
|
101
|
+
};
|
|
102
|
+
writeManifest(manifest, manifestPath);
|
|
103
|
+
|
|
104
|
+
// 5. 결과 보고
|
|
105
|
+
console.log(`\n✅ taskery init 완료`);
|
|
106
|
+
console.log(` 카피: ${copied}개 / 스킵: ${skipped}개`);
|
|
107
|
+
console.log(` manifest: ${MANIFEST_NAME}\n`);
|
|
108
|
+
console.log(`다음 단계:`);
|
|
109
|
+
console.log(` 1. CLAUDE.md 정독 + 프로젝트 메타 + 검증 명령 채우기`);
|
|
110
|
+
console.log(` 2. Claude Code 진입 → '/project-init' 호출 → '.project/' 진입 문서 골격 생성`);
|
|
111
|
+
console.log(` 3. '/plan-init <vX.X>' 호출 → 9 기획 문서 작성`);
|
|
112
|
+
console.log(` 4. '/task-init' 호출 → 첫 task 시작\n`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
main().catch((e) => {
|
|
116
|
+
console.error(`taskery init 실패: ${e.message}`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
});
|
package/bin/lib.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bin/lib.js
|
|
3
|
+
* 공통 유틸 — taskery init/create/update 스크립트가 공유.
|
|
4
|
+
*
|
|
5
|
+
* 핵심:
|
|
6
|
+
* - findTemplateDir(): 패키지 설치 위치에서 template/ 디렉토리 찾기
|
|
7
|
+
* - walkTemplate(templateDir): template/ 안 *모든 파일* 상대경로 + 해시 맵 리턴
|
|
8
|
+
* - sha256(path): 파일 해시
|
|
9
|
+
* - mkdirp(path): mkdir -p
|
|
10
|
+
* - copyFile(src, dst): 파일 카피 + 디렉토리 자동 생성
|
|
11
|
+
* - writeManifest(manifest, dst): manifest JSON 박음
|
|
12
|
+
* - readManifest(path): 기존 manifest Read (없으면 null)
|
|
13
|
+
* - LOCAL_SUFFIX: '.local.md' suffix 검사 (npx 미터치 룰)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const crypto = require('crypto');
|
|
19
|
+
|
|
20
|
+
const LOCAL_SUFFIX = '.local.md';
|
|
21
|
+
const MANIFEST_NAME = '.taskery-manifest.json';
|
|
22
|
+
|
|
23
|
+
function findTemplateDir() {
|
|
24
|
+
// 패키지 루트는 이 lib.js 파일의 부모(bin/)의 부모
|
|
25
|
+
const pkgRoot = path.resolve(__dirname, '..');
|
|
26
|
+
const tmpl = path.join(pkgRoot, 'template');
|
|
27
|
+
if (!fs.existsSync(tmpl)) {
|
|
28
|
+
throw new Error(`template/ not found at ${tmpl}. taskery 패키지 설치 손상 가능성.`);
|
|
29
|
+
}
|
|
30
|
+
return tmpl;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sha256(filePath) {
|
|
34
|
+
const buf = fs.readFileSync(filePath);
|
|
35
|
+
return 'sha256:' + crypto.createHash('sha256').update(buf).digest('hex');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function walkTemplate(templateDir) {
|
|
39
|
+
// template/ 안 모든 파일 상대경로 + 해시 맵 리턴
|
|
40
|
+
// 결과: { 'CLAUDE.md': 'sha256:...', '.claude/skills/task-init.md': 'sha256:...', ... }
|
|
41
|
+
const result = {};
|
|
42
|
+
function walk(dir, relBase) {
|
|
43
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
const full = path.join(dir, entry.name);
|
|
46
|
+
const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
walk(full, rel);
|
|
49
|
+
} else if (entry.isFile()) {
|
|
50
|
+
result[rel] = sha256(full);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
walk(templateDir, '');
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function mkdirp(dirPath) {
|
|
59
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function copyFile(src, dst) {
|
|
63
|
+
mkdirp(path.dirname(dst));
|
|
64
|
+
fs.copyFileSync(src, dst);
|
|
65
|
+
// 실행 권한 보존 (hooks .sh 파일)
|
|
66
|
+
if (src.endsWith('.sh')) {
|
|
67
|
+
fs.chmodSync(dst, 0o755);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function writeManifest(manifest, dst) {
|
|
72
|
+
fs.writeFileSync(dst, JSON.stringify(manifest, null, 2) + '\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readManifest(filePath) {
|
|
76
|
+
if (!fs.existsSync(filePath)) return null;
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
79
|
+
} catch (e) {
|
|
80
|
+
throw new Error(`${filePath} 파싱 실패: ${e.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isLocalOverride(relPath) {
|
|
85
|
+
// *.local.md suffix 사용자 오버라이드 — npx 미터치 명시
|
|
86
|
+
return relPath.endsWith(LOCAL_SUFFIX);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getPackageVersion() {
|
|
90
|
+
const pkgPath = path.resolve(__dirname, '..', 'package.json');
|
|
91
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
92
|
+
return pkg.version;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
LOCAL_SUFFIX,
|
|
97
|
+
MANIFEST_NAME,
|
|
98
|
+
findTemplateDir,
|
|
99
|
+
sha256,
|
|
100
|
+
walkTemplate,
|
|
101
|
+
mkdirp,
|
|
102
|
+
copyFile,
|
|
103
|
+
writeManifest,
|
|
104
|
+
readManifest,
|
|
105
|
+
isLocalOverride,
|
|
106
|
+
getPackageVersion,
|
|
107
|
+
};
|
package/bin/taskery.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bin/taskery.js
|
|
4
|
+
* `npx @angar2/taskery <subcommand>` — 진입점 dispatcher.
|
|
5
|
+
*
|
|
6
|
+
* 서브커맨드:
|
|
7
|
+
* init — 현재 디렉토리에 taskery 자산 카피 + manifest 생성
|
|
8
|
+
* update — 최신 버전 fetch + manifest 비교 + 머지 갱신
|
|
9
|
+
* help — 사용법 출력
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { spawnSync } = require('child_process');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const sub = process.argv[2];
|
|
16
|
+
|
|
17
|
+
function runScript(scriptName) {
|
|
18
|
+
const scriptPath = path.resolve(__dirname, scriptName);
|
|
19
|
+
const r = spawnSync(process.execPath, [scriptPath, ...process.argv.slice(3)], {
|
|
20
|
+
stdio: 'inherit',
|
|
21
|
+
});
|
|
22
|
+
process.exit(r.status ?? 1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function help() {
|
|
26
|
+
console.log(`taskery v${require('./lib').getPackageVersion()}
|
|
27
|
+
|
|
28
|
+
사용법:
|
|
29
|
+
npx @angar2/taskery init 현재 디렉토리에 taskery 자산 설치
|
|
30
|
+
npx @angar2/taskery update 최신 버전 fetch + 머지 갱신
|
|
31
|
+
npx @angar2/taskery help 도움말
|
|
32
|
+
|
|
33
|
+
새 프로젝트 시작:
|
|
34
|
+
npx -p @angar2/taskery create-taskery <project-name>
|
|
35
|
+
|
|
36
|
+
상세: https://github.com/angar2/taskery
|
|
37
|
+
`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
switch (sub) {
|
|
41
|
+
case 'init':
|
|
42
|
+
runScript('init.js');
|
|
43
|
+
break;
|
|
44
|
+
case 'update':
|
|
45
|
+
runScript('update.js');
|
|
46
|
+
break;
|
|
47
|
+
case 'help':
|
|
48
|
+
case '--help':
|
|
49
|
+
case '-h':
|
|
50
|
+
case undefined:
|
|
51
|
+
help();
|
|
52
|
+
break;
|
|
53
|
+
default:
|
|
54
|
+
console.error(`taskery: 알 수 없는 서브커맨드 '${sub}'.`);
|
|
55
|
+
help();
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
package/bin/update.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bin/update.js
|
|
4
|
+
* `npx @angar2/taskery update` — template/ 최신 자산과 사용자 측 비교 + 머지 갱신.
|
|
5
|
+
*
|
|
6
|
+
* 머지 로직 (§9-1):
|
|
7
|
+
* 1. .taskery-manifest.json (cwd) 읽기 — 없으면 에러 ('init' 먼저)
|
|
8
|
+
* 2. template/ 정독 → 새 해시 맵
|
|
9
|
+
* 3. 각 파일 분기:
|
|
10
|
+
* a. 동일 (new == manifest) → 미터치
|
|
11
|
+
* b. 신규 (manifest에 없음) → 카피 + manifest에 추가
|
|
12
|
+
* c. 변경 (new != manifest):
|
|
13
|
+
* - 사용자 측 == manifest 해시 → 사용자 customize 없음 → 자동 갱신
|
|
14
|
+
* - 사용자 측 != manifest 해시 → 사용자 customize → *.bak 백업 + confirm
|
|
15
|
+
* d. 사라짐 (manifest에 있는데 template에 없음) → 사용자에게 경고만, 사용자 파일 보존
|
|
16
|
+
* 4. *.local.md suffix → manifest에 없음 → 미터치 (방어)
|
|
17
|
+
* 5. manifest 갱신 + 결과 요약
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const readline = require('readline');
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
MANIFEST_NAME,
|
|
26
|
+
findTemplateDir,
|
|
27
|
+
walkTemplate,
|
|
28
|
+
sha256,
|
|
29
|
+
copyFile,
|
|
30
|
+
writeManifest,
|
|
31
|
+
readManifest,
|
|
32
|
+
getPackageVersion,
|
|
33
|
+
isLocalOverride,
|
|
34
|
+
} = require('./lib');
|
|
35
|
+
|
|
36
|
+
async function confirm(msg) {
|
|
37
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
rl.question(`${msg} (y/N) `, (ans) => {
|
|
40
|
+
rl.close();
|
|
41
|
+
resolve(ans.trim().toLowerCase() === 'y');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function main() {
|
|
47
|
+
const cwd = process.cwd();
|
|
48
|
+
const templateDir = findTemplateDir();
|
|
49
|
+
const manifestPath = path.join(cwd, MANIFEST_NAME);
|
|
50
|
+
|
|
51
|
+
// 1. manifest 검사
|
|
52
|
+
const oldManifest = readManifest(manifestPath);
|
|
53
|
+
if (!oldManifest) {
|
|
54
|
+
console.error(
|
|
55
|
+
`taskery update: ${MANIFEST_NAME} 없음 — 'npx @angar2/taskery init' 먼저 실행 필요.`,
|
|
56
|
+
);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 2. template/ 새 해시 맵
|
|
61
|
+
const newTemplateFiles = walkTemplate(templateDir);
|
|
62
|
+
const oldFiles = oldManifest.files || {};
|
|
63
|
+
|
|
64
|
+
console.log(`\ntaskery update`);
|
|
65
|
+
console.log(` 현재 manifest 버전: ${oldManifest.version}`);
|
|
66
|
+
console.log(` 새 패키지 버전: ${getPackageVersion()}\n`);
|
|
67
|
+
|
|
68
|
+
const newFiles = {};
|
|
69
|
+
const summary = { unchanged: 0, new: 0, autoUpdated: 0, customizedReplaced: 0, skipped: 0, removed: 0 };
|
|
70
|
+
|
|
71
|
+
// 3. 새 template 파일 분기
|
|
72
|
+
for (const [rel, newHash] of Object.entries(newTemplateFiles)) {
|
|
73
|
+
if (isLocalOverride(rel)) {
|
|
74
|
+
// template에 .local.md 들어가면 안 되지만 방어적 스킵
|
|
75
|
+
console.log(` skip (.local override 영역): ${rel}`);
|
|
76
|
+
summary.skipped++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const src = path.join(templateDir, rel);
|
|
81
|
+
const dst = path.join(cwd, rel);
|
|
82
|
+
const oldEntry = oldFiles[rel];
|
|
83
|
+
|
|
84
|
+
// a. 신규 — manifest에 없음
|
|
85
|
+
if (!oldEntry) {
|
|
86
|
+
copyFile(src, dst);
|
|
87
|
+
newFiles[rel] = { hash: newHash, core: true, managed: true };
|
|
88
|
+
summary.new++;
|
|
89
|
+
console.log(` new: ${rel}`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const oldManifestHash = oldEntry.hash;
|
|
94
|
+
|
|
95
|
+
// b. 동일 — new == manifest → 미터치 (manifest 항목 그대로)
|
|
96
|
+
if (newHash === oldManifestHash) {
|
|
97
|
+
newFiles[rel] = oldEntry;
|
|
98
|
+
summary.unchanged++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// c. 변경 — new != manifest
|
|
103
|
+
// 사용자 측 해시 비교
|
|
104
|
+
let userHash = null;
|
|
105
|
+
if (fs.existsSync(dst)) {
|
|
106
|
+
userHash = sha256(dst);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (userHash === oldManifestHash) {
|
|
110
|
+
// 사용자 customize 없음 → 자동 갱신
|
|
111
|
+
copyFile(src, dst);
|
|
112
|
+
newFiles[rel] = { hash: newHash, core: true, managed: true };
|
|
113
|
+
summary.autoUpdated++;
|
|
114
|
+
console.log(` auto-update: ${rel}`);
|
|
115
|
+
} else {
|
|
116
|
+
// 사용자 customize 검출 — *.bak 백업 + confirm
|
|
117
|
+
console.log(`\n ⚠ ${rel} — 사용자 customize 검출`);
|
|
118
|
+
console.log(` manifest 해시: ${oldManifestHash}`);
|
|
119
|
+
console.log(` 사용자 해시: ${userHash}`);
|
|
120
|
+
console.log(` 새 코어 해시: ${newHash}`);
|
|
121
|
+
const ok = await confirm(` *.bak 백업 후 새 코어로 덮어쓸까?`);
|
|
122
|
+
if (ok) {
|
|
123
|
+
// .bak 백업
|
|
124
|
+
if (fs.existsSync(dst)) {
|
|
125
|
+
fs.copyFileSync(dst, dst + '.bak');
|
|
126
|
+
}
|
|
127
|
+
copyFile(src, dst);
|
|
128
|
+
newFiles[rel] = { hash: newHash, core: true, managed: true };
|
|
129
|
+
summary.customizedReplaced++;
|
|
130
|
+
console.log(` replaced (.bak 백업): ${rel}`);
|
|
131
|
+
} else {
|
|
132
|
+
// 사용자 customize 보존 — manifest 항목은 *그대로* 유지 (다음 update가 다시 비교)
|
|
133
|
+
newFiles[rel] = oldEntry;
|
|
134
|
+
summary.skipped++;
|
|
135
|
+
console.log(` skipped (사용자 보존): ${rel}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 4. 사라짐 검사 — manifest에 있는데 새 template에 없음
|
|
141
|
+
for (const rel of Object.keys(oldFiles)) {
|
|
142
|
+
if (!(rel in newTemplateFiles)) {
|
|
143
|
+
console.log(` ⚠ removed in new version (사용자 파일 보존): ${rel}`);
|
|
144
|
+
summary.removed++;
|
|
145
|
+
// manifest에서도 제거 (next update 시 다시 검사 X)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 5. manifest 갱신
|
|
150
|
+
const newManifest = {
|
|
151
|
+
version: getPackageVersion(),
|
|
152
|
+
installed_at: oldManifest.installed_at,
|
|
153
|
+
updated_at: new Date().toISOString(),
|
|
154
|
+
files: newFiles,
|
|
155
|
+
};
|
|
156
|
+
writeManifest(newManifest, manifestPath);
|
|
157
|
+
|
|
158
|
+
// 결과 보고
|
|
159
|
+
console.log(`\n✅ taskery update 완료`);
|
|
160
|
+
console.log(` 동일: ${summary.unchanged}, 신규: ${summary.new}, 자동 갱신: ${summary.autoUpdated}`);
|
|
161
|
+
console.log(` *.bak 후 갱신: ${summary.customizedReplaced}, 사용자 보존: ${summary.skipped}`);
|
|
162
|
+
if (summary.removed > 0) {
|
|
163
|
+
console.log(` ⚠ 새 버전에서 사라진 파일: ${summary.removed} (사용자 파일 그대로 보존)`);
|
|
164
|
+
}
|
|
165
|
+
console.log();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
main().catch((e) => {
|
|
169
|
+
console.error(`taskery update 실패: ${e.message}`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@angar2/taskery",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lightweight task system for Claude Code — 1 메인 세션 + 스킬 8종 + catastrophic hook 안전망 3종",
|
|
5
|
+
"bin": {
|
|
6
|
+
"taskery": "bin/taskery.js",
|
|
7
|
+
"create-taskery": "bin/create.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"template/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"smoke:init": "node bin/init.js",
|
|
17
|
+
"smoke:update": "node bin/update.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"claude-code",
|
|
21
|
+
"task-management",
|
|
22
|
+
"ai-coding-workflow",
|
|
23
|
+
"skills",
|
|
24
|
+
"slash-commands",
|
|
25
|
+
"agent-hooks"
|
|
26
|
+
],
|
|
27
|
+
"author": "angar2 <angaridev@gmail.com>",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/angar2/taskery.git"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/angar2/taskery#readme",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/angar2/taskery/issues"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|