aiwf
Version:
AI Workflow Framework for Claude Code with multi-language support (Korean/English)
262 lines (202 loc) • 8.54 kB
JavaScript
/**
* 스프린트 태스크 관리 명령어
* 스프린트에 개별 태스크를 추가하고 관리하는 기능
*/
import fs from 'fs/promises';
import path from 'path';
import chalk from 'chalk';
import ora from 'ora';
// 프로젝트 루트에서 .aiwf 디렉토리 찾기
async function findProjectRoot(startDir = process.cwd()) {
let currentDir = startDir;
while (currentDir !== path.parse(currentDir).root) {
try {
const aiwfPath = path.join(currentDir, '.aiwf');
await fs.access(aiwfPath);
return currentDir;
} catch {
currentDir = path.dirname(currentDir);
}
}
throw new Error('.aiwf 디렉토리를 찾을 수 없습니다. AIWF 프로젝트 내에서 실행해주세요.');
}
// 스프린트 존재 확인
async function verifySprintExists(projectRoot, sprintId) {
const sprintsDir = path.join(projectRoot, '.aiwf', '03_SPRINTS');
try {
const entries = await fs.readdir(sprintsDir);
const sprintDir = entries.find(entry => entry.startsWith(sprintId + '_'));
if (!sprintDir) {
throw new Error(`스프린트 ${sprintId}를 찾을 수 없습니다.`);
}
return path.join(sprintsDir, sprintDir);
} catch (error) {
throw new Error(`스프린트 디렉토리 읽기 실패: ${error.message}`);
}
}
// 다음 태스크 번호 결정
async function getNextTaskNumber(sprintPath, sprintId) {
try {
const files = await fs.readdir(sprintPath);
const taskFiles = files.filter(f => f.match(/^T\d+_/));
if (taskFiles.length === 0) {
return 'T01';
}
const numbers = taskFiles.map(f => {
const match = f.match(/^T(\d+)_/);
return match ? parseInt(match[1]) : 0;
});
const maxNumber = Math.max(...numbers);
return `T${String(maxNumber + 1).padStart(2, '0')}`;
} catch {
return 'T01';
}
}
// 태스크 파일 생성
async function createTaskFile(sprintPath, taskNumber, sprintId, taskTitle) {
const timestamp = new Date().toISOString().slice(0, 16).replace('T', ' ');
const fileName = `${taskNumber}_${sprintId}_${taskTitle.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_가-힣]/g, '')}.md`;
const filePath = path.join(sprintPath, fileName);
const content = `# ${taskNumber}: ${taskTitle}
**스프린트**: ${sprintId}
**생성일**: ${timestamp}
**상태**: pending
**우선순위**: medium
**예상 시간**: TBD
## 개요
[태스크에 대한 간단한 설명을 작성하세요]
## 요구사항
- [ ] [구체적인 요구사항 1]
- [ ] [구체적인 요구사항 2]
- [ ] [구체적인 요구사항 3]
## 승인 기준
- [ ] [완료 조건 1]
- [ ] [완료 조건 2]
- [ ] [완료 조건 3]
## 기술 가이드라인
- [관련 아키텍처 결정 참조]
- [기존 코드베이스 패턴 참조]
- [사용할 기술 스택]
## 구현 노트
- [구현 시 고려사항]
- [잠재적 리스크]
- [테스트 전략]
## 참조
- [관련 문서나 코드 링크]
`;
await fs.writeFile(filePath, content);
return { filePath, fileName, taskNumber };
}
// 스프린트 메타 파일 업데이트
async function updateSprintMeta(sprintPath, taskNumber, taskTitle) {
const files = await fs.readdir(sprintPath);
const metaFile = files.find(f => f.includes('sprint_meta.md'));
if (!metaFile) {
console.warn(chalk.yellow('⚠️ 스프린트 메타 파일을 찾을 수 없습니다.'));
return;
}
const metaPath = path.join(sprintPath, metaFile);
let content = await fs.readFile(metaPath, 'utf-8');
// 태스크 섹션 찾기 및 업데이트
const taskSectionRegex = /## 태스크 목록[\s\S]*?(?=##|$)/;
const taskMatch = content.match(taskSectionRegex);
if (taskMatch) {
const newTaskLine = `- [ ] ${taskNumber}: ${taskTitle} - 상태: pending\n`;
const updatedSection = taskMatch[0] + newTaskLine;
content = content.replace(taskSectionRegex, updatedSection);
} else {
// 태스크 섹션이 없으면 추가
content += `\n## 태스크 목록\n\n- [ ] ${taskNumber}: ${taskTitle} - 상태: pending\n`;
}
await fs.writeFile(metaPath, content);
}
// 매니페스트 업데이트
async function updateManifest(projectRoot, sprintId, taskNumber, taskTitle) {
const manifestPath = path.join(projectRoot, '.aiwf', '00_PROJECT_MANIFEST.md');
try {
let content = await fs.readFile(manifestPath, 'utf-8');
// 최종 업데이트 날짜 변경
const dateRegex = /\*\*마지막 업데이트\*\*: \d{4}-\d{2}-\d{2}/;
const today = new Date().toISOString().slice(0, 10);
content = content.replace(dateRegex, `**마지막 업데이트**: ${today}`);
// 활성 태스크 섹션에 추가
const activeTasksRegex = /## 활성 태스크[\s\S]*?### 계획됨/;
const match = content.match(activeTasksRegex);
if (match) {
const newTaskLine = `\n- **${taskNumber}_${sprintId}**: ${taskTitle} (pending → ${today} 생성)\n`;
const updatedSection = match[0].replace('### 계획됨', newTaskLine + '### 계획됨');
content = content.replace(activeTasksRegex, updatedSection);
}
// 태스크 통계 업데이트
const statsRegex = /- \*\*총 태스크\*\*: (\d+)/;
const statsMatch = content.match(statsRegex);
if (statsMatch) {
const total = parseInt(statsMatch[1]) + 1;
content = content.replace(statsRegex, `- **총 태스크**: ${total}`);
}
await fs.writeFile(manifestPath, content);
} catch (error) {
console.warn(chalk.yellow(`⚠️ 매니페스트 업데이트 실패: ${error.message}`));
}
}
// 메인 함수
export async function addTaskToSprint(sprintId, taskTitle) {
const spinner = ora('스프린트에 태스크 추가 중...').start();
try {
// 1. 프로젝트 루트 찾기
const projectRoot = await findProjectRoot();
// 2. 스프린트 존재 확인
spinner.text = '스프린트 확인 중...';
const sprintPath = await verifySprintExists(projectRoot, sprintId);
// 3. 다음 태스크 번호 결정
spinner.text = '태스크 번호 결정 중...';
const taskNumber = await getNextTaskNumber(sprintPath, sprintId);
// 4. 태스크 파일 생성
spinner.text = '태스크 파일 생성 중...';
const { filePath, fileName, taskNumber: finalTaskNumber } = await createTaskFile(
sprintPath,
taskNumber,
sprintId,
taskTitle
);
// 5. 스프린트 메타 업데이트
spinner.text = '스프린트 메타 업데이트 중...';
await updateSprintMeta(sprintPath, finalTaskNumber, taskTitle);
// 6. 매니페스트 업데이트
spinner.text = '프로젝트 매니페스트 업데이트 중...';
await updateManifest(projectRoot, sprintId, finalTaskNumber, taskTitle);
spinner.succeed('태스크 추가 완료!');
// 성공 메시지
console.log('\n' + chalk.green('✅ 태스크 추가 완료!'));
console.log('\n📄 생성된 파일: ' + chalk.cyan(fileName));
console.log('🏃 스프린트: ' + chalk.yellow(sprintId));
console.log('📋 태스크: ' + chalk.yellow(`${finalTaskNumber} - ${taskTitle}`));
console.log('⏱️ 상태: ' + chalk.blue('pending'));
console.log('\n' + chalk.bold('📝 다음 단계:'));
console.log('1. 태스크 파일을 열어 세부사항 작성');
console.log(`2. ${chalk.cyan(`/aiwf:do_task ${finalTaskNumber}`)}로 작업 시작`);
console.log(`3. 완료 후 ${chalk.cyan(`/aiwf:update_task_status ${finalTaskNumber} completed`)} 실행`);
console.log('\n💡 팁: 태스크 목록 확인은 ' + chalk.cyan(`/aiwf:list_sprint_tasks ${sprintId}`));
return { success: true, taskNumber: finalTaskNumber, filePath };
} catch (error) {
spinner.fail(`태스크 추가 실패: ${error.message}`);
throw error;
}
}
// CLI 실행
if (import.meta.url === `file://${process.argv[1]}`) {
const args = process.argv.slice(2);
if (args.length < 2) {
console.error(chalk.red('사용법: aiwf-sprint-task <스프린트ID> <태스크 제목>'));
console.error(chalk.gray('예시: aiwf-sprint-task S03 "API 성능 최적화"'));
process.exit(1);
}
const [sprintId, ...titleParts] = args;
const taskTitle = titleParts.join(' ').replace(/^["']|["']$/g, '');
addTaskToSprint(sprintId, taskTitle).catch(error => {
console.error(chalk.red(`오류: ${error.message}`));
process.exit(1);
});
}
export default addTaskToSprint;