@angar2/taskery 0.8.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +423 -188
- package/bin/add.js +27 -99
- package/bin/backlog.js +240 -113
- package/bin/check.js +166 -0
- package/bin/commands.js +151 -0
- package/bin/finish.js +353 -0
- package/bin/init.js +99 -190
- package/bin/install.js +143 -0
- package/bin/lib.js +515 -1320
- package/bin/mcp.js +31 -263
- package/bin/orche.js +240 -0
- package/bin/plan.js +58 -120
- package/bin/prepare.js +326 -0
- package/bin/prune.js +46 -176
- package/bin/status.js +57 -151
- package/bin/taskery.js +76 -120
- package/bin/update.js +66 -266
- 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 +84 -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/migrate.js +0 -344
- 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/add.js
CHANGED
|
@@ -1,113 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
// `npx @angar2/taskery add <platform>` — 기존 설치 리포에 다른 플랫폼 자산 추가.
|
|
4
|
-
//
|
|
5
|
-
// 동작:
|
|
6
|
-
// 1. <platform> 인자 검증 (claude | codex)
|
|
7
|
-
// 2. .taskery-manifest.json 필수 — 없으면 'init' 안내
|
|
8
|
-
// 3. 이미 manifest.platforms에 있으면 변경 없이 종료
|
|
9
|
-
// 4. 해당 플랫폼 전용 자산만 카피 (공통 .project/는 init이 이미 설치)
|
|
10
|
-
// 5. manifest.platforms push + files 갱신
|
|
11
|
-
|
|
2
|
+
// add — 설치된 리포에 플랫폼(claude·codex)의 스킬·설정을 더한다
|
|
12
3
|
const fs = require('fs');
|
|
13
4
|
const path = require('path');
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const {
|
|
17
|
-
MANIFEST_NAME,
|
|
18
|
-
findTemplateDir,
|
|
19
|
-
walkTemplate,
|
|
20
|
-
copyFile,
|
|
21
|
-
writeManifest,
|
|
22
|
-
readManifest,
|
|
23
|
-
getPackageVersion,
|
|
24
|
-
resolveInstallPlan,
|
|
25
|
-
PLATFORMS,
|
|
26
|
-
} = require('./lib');
|
|
5
|
+
const L = require('./lib');
|
|
6
|
+
const I = require('./install');
|
|
27
7
|
|
|
28
|
-
|
|
29
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
30
|
-
return new Promise((resolve) => {
|
|
31
|
-
rl.question(`${msg} (y/N) `, (ans) => {
|
|
32
|
-
rl.close();
|
|
33
|
-
resolve(ans.trim().toLowerCase() === 'y');
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function main() {
|
|
39
|
-
const cwd = process.cwd();
|
|
8
|
+
function main() {
|
|
40
9
|
const platform = (process.argv[2] || '').trim();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
`taskery add: 플랫폼 인자 필요 (${PLATFORMS.join(' | ')}). 예: npx @angar2/taskery add codex`,
|
|
46
|
-
);
|
|
47
|
-
process.exit(1);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// 2. manifest 필수
|
|
51
|
-
const manifestPath = path.join(cwd, MANIFEST_NAME);
|
|
52
|
-
const manifest = readManifest(manifestPath);
|
|
53
|
-
if (!manifest) {
|
|
54
|
-
console.error(
|
|
55
|
-
`taskery add: ${MANIFEST_NAME} 없음 — 'npx @angar2/taskery init' 먼저 실행 필요.`,
|
|
56
|
-
);
|
|
57
|
-
process.exit(1);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// 3. 이미 설치된 플랫폼?
|
|
10
|
+
if (!I.PLATFORMS.includes(platform)) L.fail(`add: 플랫폼을 넣는다 (${I.PLATFORMS.join(' | ')}). 예: npx @angar2/taskery add codex`);
|
|
11
|
+
const main = L.findMain(process.cwd());
|
|
12
|
+
const manifest = L.readManifest(main);
|
|
13
|
+
if (!manifest) L.fail("add: taskery가 설치돼 있지 않다. 'npx @angar2/taskery init'을 먼저 부른다.");
|
|
61
14
|
const platforms = Array.isArray(manifest.platforms) ? manifest.platforms : ['claude'];
|
|
62
15
|
if (platforms.includes(platform)) {
|
|
63
|
-
console.log(`
|
|
64
|
-
|
|
16
|
+
console.log(`'${platform}'은 이미 설치돼 있다 — 바꾼 것 없음.`);
|
|
17
|
+
return;
|
|
65
18
|
}
|
|
66
|
-
|
|
67
|
-
// 4. 해당 플랫폼 전용 + 공통(shared/) 자산만 카피 (agnostic은 init이 이미 설치)
|
|
68
|
-
const templateDir = findTemplateDir();
|
|
69
|
-
const templateFiles = walkTemplate(templateDir);
|
|
70
|
-
const plan = resolveInstallPlan(templateFiles, [platform], { includeAgnostic: false });
|
|
71
|
-
|
|
72
|
-
console.log(`\ntaskery v${getPackageVersion()} add ${platform}`);
|
|
73
|
-
console.log(`카피 대상: ${plan.length}개 파일\n`);
|
|
74
|
-
|
|
75
19
|
const files = { ...(manifest.files || {}) };
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const dst = path.join(cwd, installRel);
|
|
82
|
-
if (fs.existsSync(dst)) {
|
|
83
|
-
const ok = await confirm(` '${installRel}' 이미 존재 — 덮어쓸까?`);
|
|
84
|
-
if (!ok) {
|
|
85
|
-
console.log(` skip: ${installRel}`);
|
|
86
|
-
skipped++;
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
20
|
+
const notes = [];
|
|
21
|
+
for (const it of I.installPlan([platform], { agnostic: false })) {
|
|
22
|
+
if (fs.existsSync(path.join(main, it.dst))) {
|
|
23
|
+
notes.push(`건너뜀: ${it.dst} (기존 파일 유지)`);
|
|
24
|
+
continue;
|
|
89
25
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
console.log(` copy: ${installRel}`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// 5. manifest 갱신
|
|
97
|
-
manifest.platforms = [...platforms, platform];
|
|
98
|
-
manifest.files = files;
|
|
99
|
-
manifest.updated_at = new Date().toISOString();
|
|
100
|
-
writeManifest(manifest, manifestPath);
|
|
101
|
-
|
|
102
|
-
console.log(`\n✅ taskery add ${platform} 완료 (카피 ${copied} / 스킵 ${skipped})`);
|
|
103
|
-
console.log(` platforms: ${manifest.platforms.join(', ')}`);
|
|
104
|
-
if (platform === 'codex') {
|
|
105
|
-
console.log(` 다음: Codex 최초 1회 '/hooks'로 hook trust 승인 (.codex/config.toml)`);
|
|
106
|
-
console.log(` AGENTS.md 정독 (코덱스 진입점)`);
|
|
26
|
+
const text = I.readTemplate(it.src);
|
|
27
|
+
I.writeFile(main, it.dst, text);
|
|
28
|
+
files[it.dst] = I.hashText(text);
|
|
107
29
|
}
|
|
30
|
+
notes.push(...I.writeConfigs(main, [platform]));
|
|
31
|
+
L.writeManifest(main, { ...manifest, platforms: [...platforms, platform], files, updated_at: new Date().toISOString() });
|
|
32
|
+
console.log(`taskery add ${platform} 완료`);
|
|
33
|
+
for (const n of notes) console.log(`- ${n}`);
|
|
108
34
|
}
|
|
109
35
|
|
|
110
|
-
|
|
111
|
-
|
|
36
|
+
try {
|
|
37
|
+
main();
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.error(e instanceof L.TaskeryError ? e.message : `taskery add 실패: ${e.stack || e.message}`);
|
|
112
40
|
process.exit(1);
|
|
113
|
-
}
|
|
41
|
+
}
|
package/bin/backlog.js
CHANGED
|
@@ -1,131 +1,258 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
* bin/backlog.js
|
|
5
|
-
* `npx @angar2/taskery backlog-add|backlog-get|backlog-mark ...` — 백로그 조작.
|
|
6
|
-
*
|
|
7
|
-
* dispatcher(taskery.js)가 서브op(add|get|mark)를 첫 인자로 주입한다.
|
|
8
|
-
* add --type <t> --title <제목> --slug <slug> --summary <개요> --target <대상영역>
|
|
9
|
-
* → withMetaLock + BL-NNN 채번 + append. stdout: { blNum, blId }
|
|
10
|
-
* get <BL-NNN> → 항목 메타 JSON ({ blId, status, type, title, slug, summary, target, taskNums }). 없으면 exit 1
|
|
11
|
-
* mark <BL-NNN> <TASK-NNN> → withMetaLock + [ ]→[x] + TASK 마크. stdout: { blId, taskNum }
|
|
12
|
-
*
|
|
13
|
-
* 채번·서식·락은 전부 코드(lib.js)가 보장 — LLM 손 파싱/Edit 제거.
|
|
14
|
-
* 실패(인자 미달 / 활성 plan 부재 / 항목 없음 등) 시 stderr + exit 1.
|
|
15
|
-
*/
|
|
16
|
-
|
|
1
|
+
// backlog-add · backlog-get · backlog-mark — 프로젝트 백로그 하나(.taskery/BACKLOG.md, 부록 B)를 명령이 다룬다(§5-3, §6-3)
|
|
2
|
+
// BACKLOG.md 쓰기는 모두 번호 잠금 안에서 한다 — prepare-task --from·close-task도 같은 잠금 안에서 아래 텍스트 함수를 부른다
|
|
3
|
+
const fs = require('fs');
|
|
17
4
|
const path = require('path');
|
|
5
|
+
const L = require('./lib');
|
|
18
6
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (!a.startsWith('--')) continue;
|
|
35
|
-
const eq = a.indexOf('=');
|
|
36
|
-
let key;
|
|
37
|
-
let val;
|
|
38
|
-
if (eq !== -1) {
|
|
39
|
-
key = a.slice(2, eq);
|
|
40
|
-
val = a.slice(eq + 1);
|
|
41
|
-
} else {
|
|
42
|
-
key = a.slice(2);
|
|
43
|
-
val = argv[++i];
|
|
44
|
-
}
|
|
45
|
-
if (KEYS.includes(key)) out[key] = val;
|
|
7
|
+
const OPEN = '## 열린 항목';
|
|
8
|
+
const DONE = '## 끝난 항목';
|
|
9
|
+
const NONE = '–';
|
|
10
|
+
|
|
11
|
+
function backlogFile(main) {
|
|
12
|
+
return path.join(main, '.taskery', 'BACKLOG.md');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// BACKLOG.md가 없으면 설치 직후 모습(부록 B)으로 만든다
|
|
16
|
+
function readBacklog(main) {
|
|
17
|
+
const file = backlogFile(main);
|
|
18
|
+
if (!fs.existsSync(file)) {
|
|
19
|
+
const tpl = fs.readFileSync(path.resolve(__dirname, '..', 'template', '.taskery', 'BACKLOG.md'), 'utf8');
|
|
20
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
21
|
+
fs.writeFileSync(file, tpl);
|
|
46
22
|
}
|
|
47
|
-
return
|
|
23
|
+
return fs.readFileSync(file, 'utf8');
|
|
48
24
|
}
|
|
49
25
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
const m = String(token).match(/(\d+)/);
|
|
53
|
-
return m ? parseInt(m[1], 10) : NaN;
|
|
26
|
+
function writeBacklog(main, text) {
|
|
27
|
+
fs.writeFileSync(backlogFile(main), text);
|
|
54
28
|
}
|
|
55
29
|
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
30
|
+
function parseBlNum(token) {
|
|
31
|
+
const m = String(token || '').trim().match(/^(?:BL-?)?(\d+)$/i);
|
|
32
|
+
if (!m) L.fail(`백로그 번호를 읽지 못했다: '${token}'. 'BL-3'이나 '3'처럼 넘긴다.`);
|
|
33
|
+
return parseInt(m[1], 10);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 항목 블록 = `### BL-N` 줄부터 다음 `### `·`## ` 줄 앞까지(뒤 빈 줄 포함)
|
|
37
|
+
function parse(lines) {
|
|
38
|
+
const items = [];
|
|
39
|
+
let sectionName = null;
|
|
40
|
+
let cur = null;
|
|
41
|
+
lines.forEach((l, i) => {
|
|
42
|
+
if (/^## /.test(l)) {
|
|
43
|
+
if (cur) cur.end = i;
|
|
44
|
+
cur = null;
|
|
45
|
+
sectionName = l.trim() === OPEN ? 'open' : l.trim() === DONE ? 'done' : null;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (/^### /.test(l)) {
|
|
49
|
+
if (cur) cur.end = i;
|
|
50
|
+
cur = null;
|
|
51
|
+
const m = l.match(/^### BL-(\d+)\b\s*(?:\[([^\]]*)\])?\s*(.*)$/);
|
|
52
|
+
if (m) {
|
|
53
|
+
cur = { num: parseInt(m[1], 10), type: (m[2] || '').trim(), title: m[3].trim(), start: i, end: lines.length, section: sectionName };
|
|
54
|
+
items.push(cur);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return items;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const FIELD = (name) => new RegExp(`^- ${name}\\s*:\\s*(.*)$`);
|
|
62
|
+
|
|
63
|
+
function getField(lines, item, name) {
|
|
64
|
+
for (let i = item.start + 1; i < item.end; i++) {
|
|
65
|
+
const m = lines[i].match(FIELD(name));
|
|
66
|
+
if (m) return m[1].trim();
|
|
80
67
|
}
|
|
81
|
-
|
|
82
|
-
const blNum = await appendBacklogItem(mainWt, meta);
|
|
83
|
-
const blId = `BL-${String(blNum).padStart(3, '0')}`;
|
|
84
|
-
process.stdout.write(JSON.stringify({ blNum, blId }) + '\n');
|
|
68
|
+
return null;
|
|
85
69
|
}
|
|
86
70
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
71
|
+
// 칸 값을 바꾼다. 칸이 없으면 항목의 마지막 칸 줄 뒤에 넣는다
|
|
72
|
+
function setField(lines, item, name, value) {
|
|
73
|
+
let last = item.start;
|
|
74
|
+
for (let i = item.start + 1; i < item.end; i++) {
|
|
75
|
+
if (FIELD(name).test(lines[i])) {
|
|
76
|
+
lines[i] = `- ${name}: ${value}`;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (lines[i].trim()) last = i;
|
|
92
80
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
81
|
+
lines.splice(last + 1, 0, `- ${name}: ${value}`);
|
|
82
|
+
item.end++;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 섹션 머리 줄 번호. 없으면 파일 끝에 만든다(끝난 항목은 열린 항목 뒤)
|
|
86
|
+
function ensureSection(lines, title) {
|
|
87
|
+
let i = lines.findIndex((l) => l.trim() === title);
|
|
88
|
+
if (i !== -1) return i;
|
|
89
|
+
while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();
|
|
90
|
+
lines.push('', title, '');
|
|
91
|
+
return lines.length - 2;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 섹션 맨 위(머리 줄과 빈 줄 하나 뒤)에 항목 블록을 넣는다
|
|
95
|
+
function insertTop(lines, title, block) {
|
|
96
|
+
const h = ensureSection(lines, title);
|
|
97
|
+
let at = h + 1;
|
|
98
|
+
if (lines[at] !== undefined && lines[at].trim() === '') at++;
|
|
99
|
+
else lines.splice(at++, 0, '');
|
|
100
|
+
const body = block.slice();
|
|
101
|
+
while (body.length && body[body.length - 1].trim() === '') body.pop();
|
|
102
|
+
lines.splice(at, 0, ...body, '');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function moveTo(lines, item, title) {
|
|
106
|
+
const block = lines.splice(item.start, item.end - item.start);
|
|
107
|
+
insertTop(lines, title, block);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function findItem(lines, num) {
|
|
111
|
+
const item = parse(lines).find((it) => it.num === num);
|
|
112
|
+
if (!item) L.fail(`BACKLOG.md에 BL-${num}이 없다. 'backlog-get'으로 열린 항목 번호를 확인한다.`);
|
|
113
|
+
return item;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function linkTokens(value) {
|
|
117
|
+
if (!value || value === NONE || value === '-') return [];
|
|
118
|
+
return value.split(/\s*,\s*/).filter(Boolean);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 태스크를 항목에 잇는다 — 연결 태스크 칸에 더하고 상태를 진행으로. 끝난 항목이면 열린 항목으로 되돌린다(§5-3)
|
|
122
|
+
function linkText(text, num, label) {
|
|
123
|
+
const lines = text.split('\n');
|
|
124
|
+
const item = findItem(lines, num);
|
|
125
|
+
const tokens = linkTokens(getField(lines, item, '연결 태스크'));
|
|
126
|
+
if (!tokens.some((t) => t.split(/\s/)[0] === label)) tokens.push(label);
|
|
127
|
+
setField(lines, item, '연결 태스크', tokens.join(', '));
|
|
128
|
+
setField(lines, item, '상태', '진행');
|
|
129
|
+
const reopened = item.section === 'done';
|
|
130
|
+
if (reopened) moveTo(lines, item, OPEN);
|
|
131
|
+
return { text: lines.join('\n'), reopened };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 닫힌 태스크의 연결 표시 — (완료)·(포기). 연결 태스크가 모두 닫히면 끝난 항목으로 옮기거나 대기로 되돌린다(§5-3)
|
|
135
|
+
function closeText(text, label, finished) {
|
|
136
|
+
const lines = text.split('\n');
|
|
137
|
+
const notes = [];
|
|
138
|
+
for (const { num } of parse(lines)) {
|
|
139
|
+
const item = parse(lines).find((it) => it.num === num);
|
|
140
|
+
const tokens = linkTokens(getField(lines, item, '연결 태스크'));
|
|
141
|
+
let hit = false;
|
|
142
|
+
const next = tokens.map((t) => {
|
|
143
|
+
if (t === label) {
|
|
144
|
+
hit = true;
|
|
145
|
+
return `${label} (${finished ? '완료' : '포기'})`;
|
|
146
|
+
}
|
|
147
|
+
return t;
|
|
148
|
+
});
|
|
149
|
+
if (!hit) continue;
|
|
150
|
+
setField(lines, item, '연결 태스크', next.join(', '));
|
|
151
|
+
if (next.every((t) => /\((완료|포기)\)$/.test(t))) {
|
|
152
|
+
if (next.some((t) => t.endsWith('(완료)'))) {
|
|
153
|
+
setField(lines, item, '상태', '완료');
|
|
154
|
+
if (item.section !== 'done') moveTo(lines, item, DONE);
|
|
155
|
+
notes.push(`BL-${num} — 연결 태스크가 모두 닫혀 끝난 항목으로 옮겼다`);
|
|
156
|
+
} else {
|
|
157
|
+
setField(lines, item, '상태', '대기');
|
|
158
|
+
notes.push(`BL-${num} — 연결 태스크가 모두 포기로 닫혀 대기로 되돌렸다`);
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
notes.push(`BL-${num} — ${label} ${finished ? '(완료)' : '(포기)'} 표시`);
|
|
162
|
+
}
|
|
98
163
|
}
|
|
99
|
-
|
|
164
|
+
return { text: lines.join('\n'), notes };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function itemText(lines, item) {
|
|
168
|
+
const block = lines.slice(item.start, item.end);
|
|
169
|
+
while (block.length && block[block.length - 1].trim() === '') block.pop();
|
|
170
|
+
return block.join('\n');
|
|
100
171
|
}
|
|
101
172
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
173
|
+
// --from 목록 → 번호 배열. 없는 번호는 거부한다(태스크를 만들기 전에 확인)
|
|
174
|
+
function parseFromList(main, from) {
|
|
175
|
+
const nums = String(from)
|
|
176
|
+
.split(/[\s,]+/)
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
.map(parseBlNum);
|
|
179
|
+
const have = new Set(parse(readBacklog(main).split('\n')).map((it) => it.num));
|
|
180
|
+
const missing = nums.filter((n) => !have.has(n));
|
|
181
|
+
if (missing.length) L.fail(`prepare-task: --from의 ${missing.map((n) => `BL-${n}`).join(', ')}이 BACKLOG.md에 없다. 'backlog-get'으로 번호를 확인한다.`);
|
|
182
|
+
return [...new Set(nums)];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// prepare-task·backlog-mark가 부른다(번호 잠금 안)
|
|
186
|
+
function linkTasks(main, nums, label) {
|
|
187
|
+
let text = readBacklog(main);
|
|
188
|
+
const notes = [];
|
|
189
|
+
for (const n of nums) {
|
|
190
|
+
const r = linkText(text, n, label);
|
|
191
|
+
text = r.text;
|
|
192
|
+
if (r.reopened) notes.push(`BL-${n}은 끝난 항목이었다 — 열린 항목으로 되돌리고 진행으로 바꿨다`);
|
|
108
193
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
194
|
+
writeBacklog(main, text);
|
|
195
|
+
return notes;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// close-task가 부른다(번호 잠금 안)
|
|
199
|
+
function markClosed(main, label, finished) {
|
|
200
|
+
if (!fs.existsSync(backlogFile(main))) return [];
|
|
201
|
+
const { text, notes } = closeText(readBacklog(main), label, finished);
|
|
202
|
+
if (notes.length) writeBacklog(main, text);
|
|
203
|
+
return notes;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function backlogAdd(ctx, a) {
|
|
207
|
+
const main = ctx.main;
|
|
208
|
+
L.requireInstalled(main);
|
|
209
|
+
const title = String(a.title || '').trim();
|
|
210
|
+
if (!title) L.fail('backlog-add: <제목>을 넣는다 — 무엇이 문제인지, 또는 무엇을 원하는지 한 줄.');
|
|
211
|
+
if (a.type && !L.TYPES.includes(a.type)) L.fail(`backlog-add: --type '${a.type}'을 모른다. ${L.TYPES.join(' · ')} 중 하나를 넣는다.`);
|
|
212
|
+
return L.withLock(main, 'number', async () => {
|
|
213
|
+
const lines = readBacklog(main).split('\n');
|
|
214
|
+
const num = parse(lines).reduce((m, it) => Math.max(m, it.num), 0) + 1;
|
|
215
|
+
const block = [
|
|
216
|
+
`### BL-${num} [${a.type || '<종류>'}] ${title}`,
|
|
217
|
+
'- 상태: 대기',
|
|
218
|
+
`- 등록: ${L.todayLocal()}`,
|
|
219
|
+
'- 현상: <현상>',
|
|
220
|
+
`- 연결 태스크: ${NONE}`,
|
|
221
|
+
];
|
|
222
|
+
insertTop(lines, OPEN, block);
|
|
223
|
+
writeBacklog(main, lines.join('\n'));
|
|
224
|
+
const left = a.type ? '현상' : '종류·현상';
|
|
225
|
+
return `BL-${num}을 ${backlogFile(main)}의 열린 항목 맨 위에 넣었다.\n다음: 필수 칸(${left})의 <…>를 채운다. 선택 칸은 근거가 있을 때만 줄을 더해 적는다.`;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function backlogGet(ctx, a) {
|
|
230
|
+
const main = ctx.main;
|
|
231
|
+
L.requireInstalled(main);
|
|
232
|
+
const lines = readBacklog(main).split('\n');
|
|
233
|
+
if (a.id) {
|
|
234
|
+
const item = findItem(lines, parseBlNum(a.id));
|
|
235
|
+
return itemText(lines, item);
|
|
128
236
|
}
|
|
237
|
+
const open = parse(lines).filter((it) => it.section === 'open');
|
|
238
|
+
if (!open.length) return '열린 항목 없음.';
|
|
239
|
+
return [
|
|
240
|
+
`열린 항목 ${open.length}개 (${backlogFile(main)})`,
|
|
241
|
+
...open.map((it) => `- BL-${it.num} [${it.type || '?'}] ${it.title} — ${getField(lines, it, '상태') || '?'}`),
|
|
242
|
+
].join('\n');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function backlogMark(ctx, a) {
|
|
246
|
+
const main = ctx.main;
|
|
247
|
+
L.requireInstalled(main);
|
|
248
|
+
if (!a.id) L.fail('backlog-mark: <BL-번호>와 <TASK>를 넣는다. 예: backlog-mark BL-3 TASK-012');
|
|
249
|
+
const num = parseBlNum(a.id);
|
|
250
|
+
const st = L.readState(main, L.parseTaskNum(a.task));
|
|
251
|
+
const label = L.taskLabel(st.num);
|
|
252
|
+
return L.withLock(main, 'number', async () => {
|
|
253
|
+
const notes = linkTasks(main, [num], label);
|
|
254
|
+
return [`BL-${num}에 ${label}을 연결했다 — 상태 진행.`, ...notes].join('\n');
|
|
255
|
+
});
|
|
129
256
|
}
|
|
130
257
|
|
|
131
|
-
|
|
258
|
+
module.exports = { backlogAdd, backlogGet, backlogMark, parseFromList, linkTasks, markClosed, parseBlNum };
|