@angar2/taskery 0.8.1 → 1.0.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/README.md +437 -188
- package/bin/add.js +27 -99
- package/bin/backlog.js +240 -113
- package/bin/check.js +182 -0
- package/bin/commands.js +151 -0
- package/bin/finish.js +365 -0
- package/bin/init.js +99 -190
- package/bin/install.js +143 -0
- package/bin/lib.js +591 -1336
- package/bin/mcp.js +31 -263
- package/bin/orche.js +240 -0
- package/bin/plan.js +58 -120
- package/bin/prepare.js +329 -0
- package/bin/prune.js +46 -176
- package/bin/status.js +57 -151
- package/bin/taskery.js +76 -120
- package/bin/update.js +66 -269
- package/package.json +5 -5
- package/template/.taskery/BACKLOG.md +38 -0
- package/template/.taskery/rules/CHANGELOG_RULE.md +31 -0
- package/template/.taskery/rules/DEV_RULE.local.md +8 -0
- package/template/.taskery/rules/GIT_RULE.md +82 -0
- package/template/.taskery/rules/MOCKUP_RULE.md +19 -0
- package/template/.taskery/rules/TASKERY_RULE.md +112 -0
- package/template/.taskery/rules/TASK_DOC_RULE.md +104 -0
- package/template/.taskery/rules/TEST_RULE.local.md +16 -0
- package/template/AGENTS.md +15 -53
- package/template/shared/skills/add-backlog/SKILL.md +8 -38
- package/template/shared/skills/log-friction/SKILL.md +10 -88
- package/template/shared/skills/plan-init/SKILL.md +10 -98
- package/template/shared/skills/project-init/SKILL.md +13 -342
- package/template/shared/skills/task-close/SKILL.md +16 -319
- package/template/shared/skills/task-dev/SKILL.md +19 -233
- package/template/shared/skills/task-init/SKILL.md +11 -68
- package/template/shared/skills/task-orche/SKILL.md +35 -0
- package/template/shared/skills/task-plan/SKILL.md +19 -498
- package/template/shared/skills/task-test/SKILL.md +23 -342
- package/bin/close.js +0 -86
- package/bin/fork.js +0 -112
- package/bin/legacy-quotes.js +0 -60
- package/bin/migrate.js +0 -481
- package/bin/set-status.js +0 -98
- package/template/.claude/hooks/closed-immutable.sh +0 -77
- package/template/.claude/settings.json +0 -18
- package/template/.claude/skills/run-team/SKILL.md +0 -145
- package/template/.codex/agents/task-tester.toml +0 -41
- package/template/.codex/agents/test-plan-author.toml +0 -34
- package/template/.codex/config.toml +0 -17
- package/template/.codex/hooks/closed-immutable.sh +0 -66
- 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/CHANGELOG_RULE.md +0 -97
- package/template/.project/rules/GIT_RULE.md +0 -325
- package/template/.project/rules/MOCKUP_RULE.md +0 -86
- package/template/.project/rules/TASKERY_RULE.md +0 -237
- package/template/.project/rules/TASK_DOC_RULE.md +0 -668
- 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/shared/hooks/git-guard.sh +0 -108
package/bin/prune.js
CHANGED
|
@@ -1,189 +1,59 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* bin/prune.js
|
|
4
|
-
* `npx @angar2/taskery prune` — stale 워크트리 / 브랜치 대화형 정리.
|
|
5
|
-
*
|
|
6
|
-
* 흐름:
|
|
7
|
-
* 1. git worktree prune (자동, 케이스 A 폴더 없는 등록 정리)
|
|
8
|
-
* 2. 의심 항목 목록 표시 (케이스 B/C/D + orphan)
|
|
9
|
-
* 3. 사용자 항목별 보존 / 삭제 선택
|
|
10
|
-
* 4. 선택된 항목 삭제:
|
|
11
|
-
* - 브랜치: git branch -D <name> ← 사용자 명시 선택이므로 -D 허용 (rebase 잔여 등 -d 거부 가능)
|
|
12
|
-
* - 워크트리: git worktree remove --force <path>
|
|
13
|
-
* 5. 결과 보고
|
|
14
|
-
*
|
|
15
|
-
* 본 명령은 *변경 발생 가능* — 사용자 confirm 항목별로 받음.
|
|
16
|
-
*/
|
|
17
|
-
|
|
1
|
+
// prune — 닫힌 태스크에 남은 워크트리·브랜치를 정리한다. 기본은 항목마다 묻고, --yes면 묻지 않는다(§5-3)
|
|
18
2
|
const fs = require('fs');
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
getActiveTasks,
|
|
28
|
-
getWorktreesRoot,
|
|
29
|
-
DEFAULT_STALE_DAYS,
|
|
30
|
-
} = require('./lib');
|
|
31
|
-
const { execFileSync } = require('child_process');
|
|
32
|
-
|
|
33
|
-
function ask(rl, q) {
|
|
34
|
-
return new Promise((resolve) => rl.question(q, (a) => resolve(a.trim().toLowerCase())));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function daysAgo(date) {
|
|
38
|
-
return Math.floor((Date.now() - date.getTime()) / (1000 * 60 * 60 * 24));
|
|
3
|
+
const L = require('./lib');
|
|
4
|
+
const { cleanup } = require('./finish');
|
|
5
|
+
|
|
6
|
+
// 닫혔는데 워크트리나 브랜치가 남은 태스크
|
|
7
|
+
function leftovers(main) {
|
|
8
|
+
return L.listStates(main).filter(
|
|
9
|
+
(st) => st.closed && !st.noBranch && ((st.worktree && fs.existsSync(st.worktree)) || L.branchExists(main, st.branch)),
|
|
10
|
+
);
|
|
39
11
|
}
|
|
40
12
|
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
process.exit(1);
|
|
13
|
+
function describe(main, st) {
|
|
14
|
+
const parts = [];
|
|
15
|
+
if (st.worktree && fs.existsSync(st.worktree)) {
|
|
16
|
+
const dirty = L.changedFiles(st.worktree);
|
|
17
|
+
parts.push(`워크트리 ${st.worktree}${dirty.length ? ` — 커밋 안 된 변경 ${dirty.length}개` : ''}`);
|
|
47
18
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
console.log(`\ntaskery prune\n`);
|
|
52
|
-
console.log(`메인 워크트리: ${mainWt}`);
|
|
53
|
-
console.log(`stale_days: ${staleDays}\n`);
|
|
54
|
-
|
|
55
|
-
// 1. git worktree prune (자동 — 케이스 A)
|
|
56
|
-
console.log(`Step 1 — git worktree prune (폴더 없는 워크트리 등록 정리)`);
|
|
57
|
-
try {
|
|
58
|
-
execFileSync('git', ['-C', mainWt, 'worktree', 'prune', '-v'], { stdio: 'inherit' });
|
|
59
|
-
} catch (e) {
|
|
60
|
-
console.error(` git worktree prune 실패: ${e.message}`);
|
|
19
|
+
if (L.branchExists(main, st.branch)) {
|
|
20
|
+
parts.push(`브랜치 ${st.branch}${L.isAncestor(main, st.branch, st.parent) ? '' : ' — 부모에 병합되지 않음'}`);
|
|
61
21
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// 2. 의심 항목 수집
|
|
65
|
-
const active = getActiveTasks(mainWt);
|
|
66
|
-
const worktreesRoot = getWorktreesRoot(projectId);
|
|
67
|
-
|
|
68
|
-
// 케이스 B/C/D
|
|
69
|
-
const suspects = [];
|
|
70
|
-
for (const t of active) {
|
|
71
|
-
const wtPath = path.join(
|
|
72
|
-
worktreesRoot,
|
|
73
|
-
`TASK-${String(t.taskNum).padStart(3, '0')}_${t.src}_${t.slug}`,
|
|
74
|
-
);
|
|
75
|
-
const wtExists = fs.existsSync(wtPath);
|
|
76
|
-
const lastCommit = gitCapture(mainWt, ['log', '-1', '--format=%ci', t.branch], {
|
|
77
|
-
allowFail: true,
|
|
78
|
-
});
|
|
79
|
-
const lastDate = lastCommit ? new Date(lastCommit) : null;
|
|
80
|
-
const days = lastDate ? daysAgo(lastDate) : null;
|
|
81
|
-
|
|
82
|
-
let kase = null;
|
|
83
|
-
if (!wtExists) kase = 'A (폴더 없음 — git worktree prune 처리됐을 가능성)';
|
|
84
|
-
else if (days !== null && days >= staleDays) kase = `C (워크트리 폴더 존재 + ${days}일 비활성)`;
|
|
85
|
-
|
|
86
|
-
if (wtExists) {
|
|
87
|
-
const rebaseApply = path.join(wtPath, '.git', 'rebase-apply');
|
|
88
|
-
const rebaseMerge = path.join(wtPath, '.git', 'rebase-merge');
|
|
89
|
-
if (fs.existsSync(rebaseApply) || fs.existsSync(rebaseMerge)) {
|
|
90
|
-
kase = `D (rebase 잔여 상태)`;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
22
|
+
return `${L.taskLabel(st.num)} ${st.title} (${st.closed.finished ? '끝난 태스크' : '포기'}): ${parts.join(' · ')}`;
|
|
23
|
+
}
|
|
93
24
|
|
|
94
|
-
|
|
95
|
-
|
|
25
|
+
async function prune(ctx, a) {
|
|
26
|
+
const main = ctx.main;
|
|
27
|
+
L.requireInstalled(main);
|
|
28
|
+
L.git(main, ['worktree', 'prune']);
|
|
29
|
+
const items = leftovers(main);
|
|
30
|
+
if (!items.length) return '정리할 것 없음 — 닫힌 태스크에 남은 워크트리·브랜치가 없다.';
|
|
31
|
+
const out = [];
|
|
32
|
+
let asked = false;
|
|
33
|
+
for (const st of items) {
|
|
34
|
+
const line = describe(main, st);
|
|
35
|
+
if (st.worktree && fs.existsSync(st.worktree) && L.changedFiles(st.worktree).length) {
|
|
36
|
+
out.push(`건너뜀: ${line}`);
|
|
37
|
+
continue;
|
|
96
38
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
);
|
|
108
|
-
for (const f of folders) {
|
|
109
|
-
if (!activeFolders.has(f)) {
|
|
110
|
-
orphans.push(path.join(worktreesRoot, f));
|
|
39
|
+
if (!a.yes) {
|
|
40
|
+
if (!ctx.ask) {
|
|
41
|
+
out.push(`남음: ${line}`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
asked = true;
|
|
45
|
+
const ans = await ctx.ask(`${line}\n 정리할까? (y/N) `, 'n');
|
|
46
|
+
if (String(ans).toLowerCase() !== 'y') {
|
|
47
|
+
out.push(`보존: ${line}`);
|
|
48
|
+
continue;
|
|
111
49
|
}
|
|
112
50
|
}
|
|
51
|
+
out.push(`${L.taskLabel(st.num)}:`, ...cleanup(main, st, { force: true }).map((n) => ` - ${n}`));
|
|
113
52
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
console.log(`✅ 정리할 의심 항목 없음.`);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
console.log(`Step 2 — 의심 항목 (${suspects.length + orphans.length}건)\n`);
|
|
121
|
-
|
|
122
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
123
|
-
const toDelete = { branches: [], worktrees: [] };
|
|
124
|
-
|
|
125
|
-
// suspects 처리
|
|
126
|
-
for (const s of suspects) {
|
|
127
|
-
console.log(`[${s.kase}] ${s.branch}`);
|
|
128
|
-
console.log(` 워크트리: ${s.wtPath}${s.wtExists ? '' : ' (폴더 X)'}`);
|
|
129
|
-
if (s.days !== null) console.log(` 마지막 커밋: ${s.days}일 전`);
|
|
130
|
-
const ans = await ask(rl, ` 삭제? (y=브랜치+워크트리 / w=워크트리만 / k=보존, 기본 k) `);
|
|
131
|
-
if (ans === 'y') {
|
|
132
|
-
toDelete.branches.push(s.branch);
|
|
133
|
-
if (s.wtExists) toDelete.worktrees.push(s.wtPath);
|
|
134
|
-
} else if (ans === 'w' && s.wtExists) {
|
|
135
|
-
toDelete.worktrees.push(s.wtPath);
|
|
136
|
-
} else {
|
|
137
|
-
console.log(` 보존`);
|
|
138
|
-
}
|
|
139
|
-
console.log();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// orphan 처리
|
|
143
|
-
for (const o of orphans) {
|
|
144
|
-
console.log(`[orphan] 워크트리 폴더 (SSoT에 없는 브랜치)`);
|
|
145
|
-
console.log(` ${o}`);
|
|
146
|
-
const ans = await ask(rl, ` 워크트리 폴더 삭제? (y/N) `);
|
|
147
|
-
if (ans === 'y') toDelete.worktrees.push(o);
|
|
148
|
-
else console.log(` 보존`);
|
|
149
|
-
console.log();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
rl.close();
|
|
153
|
-
|
|
154
|
-
// 3. 실제 삭제
|
|
155
|
-
if (toDelete.branches.length === 0 && toDelete.worktrees.length === 0) {
|
|
156
|
-
console.log(`삭제 대상 없음.`);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
console.log(`\nStep 3 — 삭제 실행\n`);
|
|
161
|
-
|
|
162
|
-
for (const wt of toDelete.worktrees) {
|
|
163
|
-
try {
|
|
164
|
-
execFileSync('git', ['-C', mainWt, 'worktree', 'remove', '--force', wt], {
|
|
165
|
-
stdio: 'inherit',
|
|
166
|
-
});
|
|
167
|
-
console.log(` worktree removed: ${wt}`);
|
|
168
|
-
} catch (e) {
|
|
169
|
-
console.error(` worktree remove 실패 (${wt}): ${e.message}`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
for (const b of toDelete.branches) {
|
|
174
|
-
try {
|
|
175
|
-
// -D 강제 삭제 — 사용자 명시 선택 (prune 대화형) 이므로 허용
|
|
176
|
-
execFileSync('git', ['-C', mainWt, 'branch', '-D', b], { stdio: 'inherit' });
|
|
177
|
-
console.log(` branch deleted: ${b}`);
|
|
178
|
-
} catch (e) {
|
|
179
|
-
console.error(` branch -D 실패 (${b}): ${e.message}`);
|
|
180
|
-
}
|
|
53
|
+
if (!a.yes && !asked && !ctx.ask) {
|
|
54
|
+
out.push('묻지 않고 정리하려면 사용자가 명시했을 때 --yes로 다시 부른다(커밋 안 된 변경이 있는 곳은 그래도 건너뛴다).');
|
|
181
55
|
}
|
|
182
|
-
|
|
183
|
-
console.log(`\n✅ taskery prune 완료 — 워크트리 ${toDelete.worktrees.length}건 / 브랜치 ${toDelete.branches.length}건 삭제.`);
|
|
56
|
+
return out.join('\n');
|
|
184
57
|
}
|
|
185
58
|
|
|
186
|
-
|
|
187
|
-
console.error(`taskery prune 실패: ${e.message}`);
|
|
188
|
-
process.exit(1);
|
|
189
|
-
});
|
|
59
|
+
module.exports = { prune };
|
package/bin/status.js
CHANGED
|
@@ -1,163 +1,69 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* bin/status.js
|
|
4
|
-
* `npx @angar2/taskery status` — 멀티세션 현황 요약.
|
|
5
|
-
*
|
|
6
|
-
* 출력:
|
|
7
|
-
* - 메인 워크트리 + 현재 브랜치
|
|
8
|
-
* - projectId / stale_days / lock_timeout_ms
|
|
9
|
-
* - 진행중 태스크 (SSoT) + 각 워크트리 폴더 존재 여부 + 마지막 커밋 시각
|
|
10
|
-
* - stale 의심 항목 (케이스 A/B/C/D)
|
|
11
|
-
* - 머지 락 상태 (잡혀 있으면 누가 잡고 있는지)
|
|
12
|
-
*
|
|
13
|
-
* 본 명령은 *변경 X — 읽기 전용*. 정리는 `taskery prune`.
|
|
14
|
-
*/
|
|
15
|
-
|
|
1
|
+
// status — 열린 태스크·단계 표·단계별 시간·문서 경로·워크트리 경로, 플랜별 시작할 수 있는 태스크·읽지 못한 항목(§5-3)
|
|
16
2
|
const fs = require('fs');
|
|
17
3
|
const path = require('path');
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
const {
|
|
21
|
-
MANIFEST_NAME,
|
|
22
|
-
readManifest,
|
|
23
|
-
gitCapture,
|
|
24
|
-
getMainWorktreePath,
|
|
25
|
-
getActiveTasks,
|
|
26
|
-
getActiveVersion,
|
|
27
|
-
getWorktreesRoot,
|
|
28
|
-
getMergeLockPath,
|
|
29
|
-
DEFAULT_STALE_DAYS,
|
|
30
|
-
DEFAULT_LOCK_TIMEOUT_MS,
|
|
31
|
-
} = require('./lib');
|
|
32
|
-
|
|
33
|
-
function daysAgo(date) {
|
|
34
|
-
const now = Date.now();
|
|
35
|
-
const ms = now - date.getTime();
|
|
36
|
-
return Math.floor(ms / (1000 * 60 * 60 * 24));
|
|
37
|
-
}
|
|
4
|
+
const L = require('./lib');
|
|
5
|
+
const { parsePlanItems } = require('./plan');
|
|
38
6
|
|
|
39
|
-
|
|
40
|
-
// 1. 메인 워크트리 + manifest
|
|
41
|
-
let mainWt;
|
|
42
|
-
try {
|
|
43
|
-
mainWt = getMainWorktreePath();
|
|
44
|
-
} catch (e) {
|
|
45
|
-
console.error(`taskery status 실패: ${e.message}`);
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
7
|
+
const HEAD = ['시작·기획', '개발', '테스트', '닫기'];
|
|
48
8
|
|
|
49
|
-
|
|
50
|
-
if (!
|
|
51
|
-
|
|
52
|
-
|
|
9
|
+
function openTasks(main, open) {
|
|
10
|
+
if (!open.length) return [`열린 태스크 없음. (본진: ${main})`];
|
|
11
|
+
const out = [`열린 태스크 ${open.length}개 (본진: ${main})`];
|
|
12
|
+
for (const st of open) {
|
|
13
|
+
const cells = L.stageCells(st);
|
|
14
|
+
out.push('');
|
|
15
|
+
out.push(`${L.taskLabel(st.num)} ${st.title}`);
|
|
16
|
+
out.push(`- 플랜 ${st.plan}${st.item ? ` 항목 ${st.item}` : ''} · ${st.type} · ${st.size} · 스위치 ${st.switch.join(',')} · 범위 ${st.range}`);
|
|
17
|
+
out.push(`- 단계: ${HEAD.map((h, i) => `${h} ${cells[i]}`).join(' | ')}`);
|
|
18
|
+
out.push(`- 문서: ${L.docShown(main, st)}`);
|
|
19
|
+
if (st.noBranch) out.push(`- 워크트리: 없음 — 분기 생략(--no-branch), 본진 ${st.parent}에서 일한다`);
|
|
20
|
+
else if (st.noWorktree) out.push(`- 워크트리: 없음 — 분기 생략(--no-worktree), 본진에서 브랜치 ${st.branch}로 일한다`);
|
|
21
|
+
else out.push(`- 워크트리: ${st.worktree} (브랜치 ${st.branch})`);
|
|
53
22
|
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
54
25
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
let activePlan = null;
|
|
72
|
-
try {
|
|
73
|
-
activePlan = getActiveVersion(mainWt);
|
|
74
|
-
} catch (e) {
|
|
75
|
-
activePlan = null;
|
|
76
|
-
}
|
|
77
|
-
console.log(
|
|
78
|
-
`활성 plan: ${activePlan ?? '미설정 — /plan-init으로 생성하거나 plan-switch <NNN_slug>로 지정'}`,
|
|
79
|
-
);
|
|
80
|
-
console.log(`stale_days: ${staleDays} / lock_timeout_ms: ${lockTimeoutMs}\n`);
|
|
81
|
-
|
|
82
|
-
// 2. 머지 락 상태
|
|
83
|
-
const lockPath = getMergeLockPath(projectId);
|
|
84
|
-
const lockDir = lockPath + '.lock'; // proper-lockfile은 <file>.lock 디렉토리 생성
|
|
85
|
-
if (fs.existsSync(lockDir)) {
|
|
86
|
-
let lockInfo = '';
|
|
87
|
-
try {
|
|
88
|
-
const stat = fs.statSync(lockDir);
|
|
89
|
-
const ageMs = Date.now() - stat.mtimeMs;
|
|
90
|
-
lockInfo = ` (잡힘 ${Math.round(ageMs / 1000)}초 전)`;
|
|
91
|
-
} catch (_) {}
|
|
92
|
-
console.log(`머지 락: 활성${lockInfo}`);
|
|
93
|
-
} else {
|
|
94
|
-
console.log(`머지 락: 비활성`);
|
|
95
|
-
}
|
|
96
|
-
console.log();
|
|
97
|
-
|
|
98
|
-
// 3. 진행중 태스크 (SSoT)
|
|
99
|
-
const active = getActiveTasks(mainWt);
|
|
100
|
-
console.log(`진행중 태스크 (SSoT, ${active.length}건):`);
|
|
101
|
-
|
|
102
|
-
if (active.length === 0) {
|
|
103
|
-
console.log(` (없음)`);
|
|
104
|
-
} else {
|
|
105
|
-
const worktreesRoot = getWorktreesRoot(projectId);
|
|
106
|
-
for (const t of active) {
|
|
107
|
-
const wtPath = path.join(worktreesRoot, `TASK-${String(t.taskNum).padStart(3, '0')}_${t.src}_${t.slug}`);
|
|
108
|
-
const wtExists = fs.existsSync(wtPath);
|
|
109
|
-
const lastCommit = gitCapture(mainWt, [
|
|
110
|
-
'log', '-1', '--format=%ci', t.branch,
|
|
111
|
-
], { allowFail: true });
|
|
112
|
-
const lastDate = lastCommit ? new Date(lastCommit) : null;
|
|
113
|
-
const daysOld = lastDate ? daysAgo(lastDate) : null;
|
|
114
|
-
|
|
115
|
-
console.log(` ${t.branch}`);
|
|
116
|
-
console.log(` 워크트리: ${wtPath}${wtExists ? '' : ' ⚠ 폴더 없음 (케이스 A — prune 필요)'}`);
|
|
117
|
-
if (lastDate) {
|
|
118
|
-
console.log(` 마지막 커밋: ${lastCommit} (${daysOld}일 전)${daysOld >= staleDays ? ' ⚠ stale 의심 (케이스 B/C)' : ''}`);
|
|
119
|
-
} else {
|
|
120
|
-
console.log(` 마지막 커밋: (조회 실패)`);
|
|
121
|
-
}
|
|
122
|
-
if (wtExists) {
|
|
123
|
-
const rebaseApply = path.join(wtPath, '.git', 'rebase-apply');
|
|
124
|
-
const rebaseMerge = path.join(wtPath, '.git', 'rebase-merge');
|
|
125
|
-
if (fs.existsSync(rebaseApply) || fs.existsSync(rebaseMerge)) {
|
|
126
|
-
console.log(` ⚠ rebase 잔여 상태 (케이스 D) — taskery prune 또는 워크트리에서 git rebase --abort`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
26
|
+
// 플랜 하나의 시작할 수 있는 항목 — 끝나지 않았고, 열린 태스크가 없고, 선행 항목이 모두 끝난 것(§5-3)
|
|
27
|
+
function planLines(main, plan, byNum) {
|
|
28
|
+
const { items, unreadable, noList } = parsePlanItems(fs.readFileSync(path.join(main, '.taskery', 'plans', plan, 'PLAN.md'), 'utf8'));
|
|
29
|
+
if (noList) return [`- ${plan}: \`## 태스크 목록\`이 없다`];
|
|
30
|
+
const index = new Map(items.map((it) => [it.num, it]));
|
|
31
|
+
// 항목이 끝났다 = 가장 최근 연결된 태스크가 끝난 태스크(마무리 기록이 있고 닫힘)
|
|
32
|
+
const done = (it) => {
|
|
33
|
+
const st = it.tasks.length ? byNum.get(it.tasks[it.tasks.length - 1]) : null;
|
|
34
|
+
return !!(st && st.closed && st.closed.finished);
|
|
35
|
+
};
|
|
36
|
+
const ready = [];
|
|
37
|
+
for (const it of items) {
|
|
38
|
+
const missing = it.pre.filter((n) => !index.has(n));
|
|
39
|
+
if (missing.length) {
|
|
40
|
+
unreadable.push(`${it.line} (선행 항목 ${missing.join(', ')}이 목록에 없다)`);
|
|
41
|
+
continue;
|
|
129
42
|
}
|
|
43
|
+
const busy = it.tasks.some((n) => byNum.has(n) && !byNum.get(n).closed);
|
|
44
|
+
if (!done(it) && !busy && it.pre.every((n) => done(index.get(n)))) ready.push(`${it.num}. ${it.desc}`);
|
|
130
45
|
}
|
|
131
|
-
|
|
46
|
+
const out = [`- ${plan}: ${ready.length ? ready.join(' · ') : '없음'}`];
|
|
47
|
+
for (const u of unreadable) out.push(` - 읽지 못한 항목(시작할 수 있는 태스크에서 뺐다): ${u}`);
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
132
50
|
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
console.log(` → 'taskery prune' 으로 정리`);
|
|
149
|
-
console.log();
|
|
150
|
-
}
|
|
51
|
+
async function status(ctx) {
|
|
52
|
+
const main = ctx.main;
|
|
53
|
+
L.requireInstalled(main);
|
|
54
|
+
const states = L.listStates(main);
|
|
55
|
+
const out = openTasks(
|
|
56
|
+
main,
|
|
57
|
+
states.filter((s) => !s.closed),
|
|
58
|
+
);
|
|
59
|
+
const dir = path.join(main, '.taskery', 'plans');
|
|
60
|
+
const plans = fs.existsSync(dir) ? fs.readdirSync(dir).filter((n) => fs.existsSync(path.join(dir, n, 'PLAN.md'))).sort() : [];
|
|
61
|
+
if (plans.length) {
|
|
62
|
+
const byNum = new Map(states.map((s) => [s.num, s]));
|
|
63
|
+
out.push('', '플랜별 시작할 수 있는 태스크 (PLAN.md 목록 항목)');
|
|
64
|
+
for (const plan of plans) out.push(...planLines(main, plan, byNum));
|
|
151
65
|
}
|
|
152
|
-
|
|
153
|
-
console.log(`기타 명령:`);
|
|
154
|
-
console.log(` npx @angar2/taskery prune stale 워크트리 / 브랜치 대화형 정리`);
|
|
155
|
-
console.log();
|
|
66
|
+
return out.join('\n');
|
|
156
67
|
}
|
|
157
68
|
|
|
158
|
-
|
|
159
|
-
main();
|
|
160
|
-
} catch (e) {
|
|
161
|
-
console.error(`taskery status 실패: ${e.message}`);
|
|
162
|
-
process.exit(1);
|
|
163
|
-
}
|
|
69
|
+
module.exports = { status };
|
package/bin/taskery.js
CHANGED
|
@@ -1,131 +1,87 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
* bin/taskery.js
|
|
4
|
-
* `npx @angar2/taskery <subcommand>` — 진입점 dispatcher.
|
|
5
|
-
*
|
|
6
|
-
* 서브커맨드:
|
|
7
|
-
* init — 현재 디렉토리에 taskery 자산 카피 + manifest 생성 (플랫폼 선택)
|
|
8
|
-
* add — 기존 설치 리포에 플랫폼 자산 추가 (claude | codex)
|
|
9
|
-
* update — 최신 버전 fetch + manifest 비교 + 머지 갱신
|
|
10
|
-
* status — 멀티세션 현황 요약 (진행중 태스크 / 워크트리 / 머지 락, 0.1.2+)
|
|
11
|
-
* prune — stale 워크트리 / 브랜치 대화형 정리 (0.1.2+)
|
|
12
|
-
* fork — task 분기 (채번+워크트리+브랜치 생성 init 락 원자 실행, 0.3.2+) + 골격 task.md 자동 생성(--size/--title)
|
|
13
|
-
* backlog-add/get/mark — 활성 plan BACKLOG.md 조작 (채번/조회/확인 마킹, 코드화)
|
|
14
|
-
* set-status — task 헤더 상태 전이 (7×7 유효전이 검증, 코드화)
|
|
15
|
-
* plan-init — plan 생성 (채번+폴더+ROADMAP/PLAN/BACKLOG 골격+manifest.activePlan 갱신, 코드화)
|
|
16
|
-
* plan-switch — 활성 plan 전환 (manifest.activePlan 갱신, 0.7.0+)
|
|
17
|
-
* close — close 결정적 준비 (Phase커밋+status=closed+문서커밋+추적마커; 비가역 머지/정리는 스킬, 코드화)
|
|
18
|
-
* mcp — stdio MCP 서버 기동 (lib core를 도구 8종으로 노출; Claude/Codex 등록)
|
|
19
|
-
* help — 사용법 출력
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
const { spawnSync } = require('child_process');
|
|
2
|
+
// taskery CLI 입구 — 설치 명령(init·update·add)·mcp는 각 스크립트로, 나머지는 명령 정의표에서 만든다
|
|
23
3
|
const path = require('path');
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
const { COMMANDS, execute } = require('./commands');
|
|
6
|
+
const L = require('./lib');
|
|
24
7
|
|
|
25
|
-
const
|
|
8
|
+
const INSTALL = { init: 'init.js', update: 'update.js', add: 'add.js', mcp: 'mcp.js' };
|
|
26
9
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
process.exit(r.status ?? 1);
|
|
10
|
+
function usage(cmd) {
|
|
11
|
+
const pos = cmd.args.filter((a) => a.positional).map((a) => `<${a.name}>`);
|
|
12
|
+
const opts = cmd.args
|
|
13
|
+
.filter((a) => !a.positional)
|
|
14
|
+
.map((a) => (a.type === 'bool' ? `[--${a.name}]` : a.type === 'list' ? `[--${a.name} <값> …]` : `[--${a.name} <값>]`));
|
|
15
|
+
return [cmd.name, ...pos, ...opts].join(' ');
|
|
34
16
|
}
|
|
35
17
|
|
|
36
18
|
function help() {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
npx @angar2/taskery backlog-mark <BL-NNN> <TASK-NNN> 백로그 확인 마킹
|
|
49
|
-
npx @angar2/taskery set-status <TASK-NNN> <state> task 상태 전이 (유효전이 검증)
|
|
50
|
-
npx @angar2/taskery plan-init <slug> [--force] plan 생성 (채번+폴더+골격+활성plan 갱신)
|
|
51
|
-
npx @angar2/taskery plan-switch <NNN_slug> 활성 plan 전환
|
|
52
|
-
npx @angar2/taskery close <TASK-NNN> close 결정적 준비 (Phase커밋+status=closed+추적마커)
|
|
53
|
-
npx @angar2/taskery mcp MCP stdio 서버 기동 (Claude .mcp.json / Codex codex mcp add 등록)
|
|
54
|
-
npx @angar2/taskery help 도움말
|
|
55
|
-
|
|
56
|
-
새 프로젝트 시작:
|
|
57
|
-
npx -p @angar2/taskery create-taskery <project-name>
|
|
58
|
-
|
|
59
|
-
상세: https://github.com/angar2/taskery
|
|
60
|
-
`);
|
|
19
|
+
const lines = [`taskery v${L.getPackageVersion()}`, '', '설치:'];
|
|
20
|
+
lines.push(' init 현재 리포(또는 빈 폴더)에 taskery를 설치한다');
|
|
21
|
+
lines.push(' update 설치된 파일을 이 버전으로 갱신한다');
|
|
22
|
+
lines.push(' add <claude|codex> 플랫폼을 추가한다');
|
|
23
|
+
lines.push(' mcp MCP 서버를 띄운다(.mcp.json·.codex/config.toml이 부른다)');
|
|
24
|
+
lines.push('', '명령:');
|
|
25
|
+
for (const c of COMMANDS) {
|
|
26
|
+
lines.push(` ${usage(c)}`);
|
|
27
|
+
lines.push(` ${c.summary}`);
|
|
28
|
+
}
|
|
29
|
+
return lines.join('\n');
|
|
61
30
|
}
|
|
62
31
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
케이스 분기: A (워크트리 + 브랜치 모두 stale) / B (브랜치만 stale) / C (워크트리 폴더 잔존) / D (정합 영역).
|
|
86
|
-
각 케이스마다 y/n/k 선택 가능 (y = 삭제 / n = 건너뜀 / k = 보존 명시).
|
|
87
|
-
|
|
88
|
-
상세: https://github.com/angar2/taskery
|
|
89
|
-
`);
|
|
90
|
-
break;
|
|
32
|
+
function parseArgs(cmd, argv) {
|
|
33
|
+
const out = {};
|
|
34
|
+
const positional = cmd.args.filter((a) => a.positional);
|
|
35
|
+
let pi = 0;
|
|
36
|
+
for (let i = 0; i < argv.length; i++) {
|
|
37
|
+
const tok = argv[i];
|
|
38
|
+
if (tok.startsWith('--')) {
|
|
39
|
+
const name = tok.slice(2);
|
|
40
|
+
const def = cmd.args.find((a) => !a.positional && a.name === name);
|
|
41
|
+
if (!def) L.fail(`${cmd.name}: 모르는 옵션 --${name}.\n사용법: ${usage(cmd)}`);
|
|
42
|
+
if (def.type === 'bool') out[name] = true;
|
|
43
|
+
else if (def.type === 'list') {
|
|
44
|
+
const vals = [];
|
|
45
|
+
while (i + 1 < argv.length && !argv[i + 1].startsWith('--')) vals.push(argv[++i]);
|
|
46
|
+
out[name] = vals;
|
|
47
|
+
} else {
|
|
48
|
+
if (i + 1 >= argv.length) L.fail(`${cmd.name}: --${name}에 값을 넣는다.`);
|
|
49
|
+
out[name] = argv[++i];
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
if (pi >= positional.length) L.fail(`${cmd.name}: 인자가 너무 많다 ('${tok}').\n사용법: ${usage(cmd)}`);
|
|
53
|
+
out[positional[pi++].name] = tok;
|
|
91
54
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
case 'fork':
|
|
95
|
-
runScript('fork.js');
|
|
96
|
-
break;
|
|
97
|
-
case 'backlog-add':
|
|
98
|
-
runScript('backlog.js', ['add']);
|
|
99
|
-
break;
|
|
100
|
-
case 'backlog-get':
|
|
101
|
-
runScript('backlog.js', ['get']);
|
|
102
|
-
break;
|
|
103
|
-
case 'backlog-mark':
|
|
104
|
-
runScript('backlog.js', ['mark']);
|
|
105
|
-
break;
|
|
106
|
-
case 'set-status':
|
|
107
|
-
runScript('set-status.js');
|
|
108
|
-
break;
|
|
109
|
-
case 'plan-init':
|
|
110
|
-
runScript('plan.js', ['init']);
|
|
111
|
-
break;
|
|
112
|
-
case 'plan-switch':
|
|
113
|
-
runScript('plan.js', ['switch']);
|
|
114
|
-
break;
|
|
115
|
-
case 'close':
|
|
116
|
-
runScript('close.js');
|
|
117
|
-
break;
|
|
118
|
-
case 'mcp':
|
|
119
|
-
runScript('mcp.js');
|
|
120
|
-
break;
|
|
121
|
-
case 'help':
|
|
122
|
-
case '--help':
|
|
123
|
-
case '-h':
|
|
124
|
-
case undefined:
|
|
125
|
-
help();
|
|
126
|
-
break;
|
|
127
|
-
default:
|
|
128
|
-
console.error(`taskery: 알 수 없는 서브커맨드 '${sub}'.`);
|
|
129
|
-
help();
|
|
130
|
-
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
131
57
|
}
|
|
58
|
+
|
|
59
|
+
async function main() {
|
|
60
|
+
const sub = process.argv[2];
|
|
61
|
+
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
|
|
62
|
+
console.log(help());
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
if (INSTALL[sub]) {
|
|
66
|
+
const r = spawnSync(process.execPath, [path.join(__dirname, INSTALL[sub]), ...process.argv.slice(3)], { stdio: 'inherit' });
|
|
67
|
+
return r.status ?? 1;
|
|
68
|
+
}
|
|
69
|
+
const cmd = COMMANDS.find((c) => c.name === sub);
|
|
70
|
+
if (!cmd) {
|
|
71
|
+
console.error(`taskery: 모르는 명령 '${sub}'.\n\n${help()}`);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
const asker = cmd.interactive ? require('./install').makeAsker() : null;
|
|
75
|
+
try {
|
|
76
|
+
const out = await execute(cmd, parseArgs(cmd, process.argv.slice(3)), { ask: asker && asker.ask });
|
|
77
|
+
if (out) console.log(out);
|
|
78
|
+
return 0;
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.error(e instanceof L.TaskeryError ? e.message : `taskery ${sub} 실패: ${e.stack || e.message}`);
|
|
81
|
+
return 1;
|
|
82
|
+
} finally {
|
|
83
|
+
if (asker) asker.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
main().then((code) => process.exit(code));
|