aiwf 0.3.5 → 0.3.7
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/.aiwf/00_PROJECT_MANIFEST.md +29 -16
- package/.aiwf/02_REQUIREMENTS/M03_Simone_Integration_Enhancement/M03_milestone_meta.md +55 -0
- package/.aiwf/02_REQUIREMENTS/M03_Simone_Integration_Enhancement/SPECS_AIWF_Simplified_Plan.md +221 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/S03_M02_sprint_meta.md +0 -1
- package/.aiwf/test-results-s02.json +5 -5
- package/.claude/commands/aiwf/aiwf_add_task_to_sprint.md +283 -0
- package/.claude/commands/aiwf/aiwf_install_git_hooks.md +63 -0
- package/.claude/commands/aiwf/aiwf_link_feature_branch.md +43 -0
- package/.claude/commands/aiwf/aiwf_link_feature_commit.md +46 -0
- package/claude-code/aiwf/en/.claude/commands/aiwf/aiwf_create_testcase.md +561 -0
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_create_testcase.md +561 -0
- package/package.json +1 -1
- package/src/cli/index.js +17 -2
- package/src/commands/create-project.js +10 -7
- package/src/commands/sprint-task.js +262 -0
- package/src/lib/file-downloader.js +194 -33
- package/src/lib/installer.js +52 -15
- package/tests/integration/test-persona-system/.aiwf/metrics/metrics.json +833 -0
- package/tests/unit/sprint-task.test.js +172 -0
- package/.aiwf/sessions/session_1752877859489_1t7n2fhdb.json +0 -7
- package/.aiwf/sessions/session_1752877859515_cwdv211ry.json +0 -7
- package/.aiwf/sessions/session_1752877860664_huziadmbo.json +0 -7
- package/.aiwf/sessions/session_1752877860666_kyr5u1np6.json +0 -7
- package/.aiwf/sessions/session_1752877861848_9yinepw3t.json +0 -7
- package/.aiwf/sessions/session_1752877861874_y32sputuc.json +0 -7
- package/.aiwf/sessions/session_1752877862685_ivxbl5rhz.json +0 -7
- package/.aiwf/sessions/session_1752877862820_wnq4glvxi.json +0 -7
- package/.aiwf/sessions/session_1752877864632_3ebnpaelb.json +0 -7
- package/.aiwf/sessions/session_1752877864879_b9zgjcb7d.json +0 -7
- package/.aiwf/sessions/session_1752877865533_x14m1vodi.json +0 -7
- package/.aiwf/sessions/session_1752877865562_gkoh19n7d.json +0 -7
- package/.aiwf/sessions/session_1752877866333_wp8qpdcoh.json +0 -7
- package/.aiwf/sessions/session_1752877866509_dawntfpab.json +0 -7
- package/.aiwf/sessions/session_1752877866890_plc5vfucl.json +0 -7
- package/.aiwf/sessions/session_1752877866895_ga8hmq3oi.json +0 -7
- package/.aiwf/sessions/session_1752877867761_ayjp4v5f5.json +0 -7
- package/.aiwf/sessions/session_1752877867892_lfz6jy252.json +0 -7
- package/.aiwf/sessions/session_1752877868139_7bqd9ubli.json +0 -7
- package/.aiwf/sessions/session_1752877868162_zq3ofjbsh.json +0 -7
- package/.aiwf/sessions/session_1752877869054_7rlgm9jt6.json +0 -7
- package/.aiwf/sessions/session_1752877869477_ax3nhbxzr.json +0 -7
- package/.aiwf/sessions/session_1752877870095_6hgp4vl1e.json +0 -7
- package/.aiwf/sessions/session_1752877870381_qfcrdg4li.json +0 -7
- package/.aiwf/sessions/session_1752877871895_pgd5vllhz.json +0 -7
- package/.aiwf/sessions/session_1752877872199_jbhv2g2da.json +0 -7
- package/.aiwf/sessions/session_1752877872680_x04wpffu6.json +0 -7
- package/.aiwf/sessions/session_1752877872895_3y4vbeauo.json +0 -7
- package/.aiwf/sessions/session_1752877873178_3maolzc6b.json +0 -7
- package/.aiwf/sessions/session_1752877873180_ytm83j07f.json +0 -7
- package/.aiwf/sessions/session_1752877874217_dp56c9t42.json +0 -7
- package/.aiwf/sessions/session_1752877874415_epwp1g1og.json +0 -7
- package/.aiwf/sessions/session_1752877874454_heicyuryb.json +0 -7
- package/.aiwf/sessions/session_1752877874869_1c2illeu5.json +0 -7
- package/.aiwf/sessions/session_1752877874879_k1vo4vxyx.json +0 -7
- package/.aiwf/sessions/session_1752877874913_bwgyiz9bp.json +0 -7
- package/.aiwf/sessions/session_1752877876218_44807kztz.json +0 -7
- package/.aiwf/sessions/session_1752877876515_qkhjv5kpw.json +0 -7
- package/.aiwf/sessions/session_1752877876543_p605aud1m.json +0 -7
- package/.aiwf/sessions/session_1752877876819_l65w7ifck.json +0 -7
- package/.aiwf/sessions/session_1752877876867_xrhzss4we.json +0 -7
- package/.aiwf/sessions/session_1752877876872_u7aso54qb.json +0 -7
- package/.aiwf/sessions/session_1752877877705_telnfg4o6.json +0 -7
- package/.aiwf/sessions/session_1752877878285_6s45im2ib.json +0 -7
- package/.aiwf/sessions/session_1752877878506_v0dvguqhz.json +0 -7
- package/.aiwf/sessions/session_1752877878581_3ri3yc66a.json +0 -7
- package/.aiwf/sessions/session_1752877878756_a82x9s2pd.json +0 -7
- package/.aiwf/sessions/session_1752877878886_20pjsfour.json +0 -7
- package/.aiwf/sessions/session_1752877880266_fm1ydjw0g.json +0 -7
- package/.aiwf/sessions/session_1752877881149_xbat4z03d.json +0 -7
- package/.aiwf/sessions/session_1752877881551_qy5sic9qs.json +0 -7
- package/.aiwf/sessions/session_1752877881554_24ugd96wu.json +0 -7
- package/.aiwf/sessions/session_1752877881797_e9mm25uh0.json +0 -7
- package/.aiwf/sessions/session_1752877881822_bxvjj8c9g.json +0 -7
- package/.aiwf/sessions/session_1752877881890_jv7l6jve3.json +0 -7
- package/.aiwf/sessions/session_1752877881991_f0qugdtcq.json +0 -7
- package/.aiwf/sessions/session_1752877883387_0bvw9rvnx.json +0 -7
- package/.aiwf/sessions/session_1752877883820_q6g2md2up.json +0 -7
- package/.aiwf/sessions/session_1752877884393_sqr7trvjl.json +0 -7
- package/.aiwf/sessions/session_1752877884500_whtypmzlf.json +0 -7
- package/.aiwf/sessions/session_1752877884645_y9mvvxli1.json +0 -7
- package/.aiwf/sessions/session_1752877884733_hk46eenel.json +0 -7
- package/.aiwf/sessions/session_1752877884738_c62j6214r.json +0 -7
- package/.aiwf/sessions/session_1752877884771_temhxehoz.json +0 -7
- package/.aiwf/sessions/session_1752877885643_6v5fcktc7.json +0 -7
- package/.aiwf/sessions/session_1752877886426_ee8di2fqh.json +0 -7
- package/.aiwf/sessions/session_1752877886549_3xj6tdoew.json +0 -7
- package/.aiwf/sessions/session_1752877886604_aj80yyfco.json +0 -7
- package/.aiwf/sessions/session_1752877886702_2b8841vyg.json +0 -7
- package/.aiwf/sessions/session_1752877886712_ntqc8b8bz.json +0 -7
- package/.aiwf/sessions/session_1752877886968_946m8tbw6.json +0 -7
- package/.aiwf/sessions/session_1752877887008_wg6zfg8dn.json +0 -7
- package/.aiwf/sessions/session_1752877888479_ciiu2qiqs.json +0 -7
- package/.aiwf/sessions/session_1752877889330_ravg8l66k.json +0 -7
- package/.aiwf/sessions/session_1752877889542_7zepkhjkc.json +0 -7
- package/.aiwf/sessions/session_1752877889638_ev4htqiu0.json +0 -7
- package/.aiwf/sessions/session_1752877889956_szphy2qma.json +0 -7
- package/.aiwf/sessions/session_1752877890220_o6aaa57tt.json +0 -7
- package/.aiwf/sessions/session_1752877890527_v2loy5vu1.json +0 -7
- package/.aiwf/sessions/session_1752877890572_cwq8fiuma.json +0 -7
- package/.aiwf/sessions/session_1752877892200_sgcwq1alt.json +0 -7
- package/.aiwf/sessions/session_1752877892268_v8kexpbc5.json +0 -7
- package/.aiwf/sessions/session_1752877892384_1gbbgrp4y.json +0 -7
- package/.aiwf/sessions/session_1752877893568_9p2n5p31a.json +0 -7
- package/.aiwf/sessions/session_1752877893727_un34a5tvp.json +0 -7
- package/.aiwf/sessions/session_1752877893778_01j79sw9i.json +0 -7
- package/.aiwf/sessions/session_1752877894053_jrhba6wko.json +0 -7
- package/.aiwf/sessions/session_1752877894082_x3bghawox.json +0 -7
- package/.aiwf/sessions/session_1752877894500_iytiav2ku.json +0 -7
- package/.aiwf/sessions/session_1752877894667_ucn0gc2ge.json +0 -7
- package/test-token-system.js +0 -88
- package/tests/temp_invalid_json/invalid.json +0 -1
- /package/{.aiwf/templates → templates}/README.md +0 -0
- /package/{.aiwf/templates → templates}/api-server/config.json +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/.aiwf/config.json +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/.aiwf/feature-ledger.json +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/.aiwf/personas/backend-engineer.json +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/.aiwf/scripts/cli.js +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/.env.example +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/.eslintrc.json +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/README.md +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/jest.config.js +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/nodemon.json +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/package.json +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/src/app.ts +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/src/config/swagger.ts +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/src/controllers/aiwfController.ts +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/src/controllers/authController.ts +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/src/controllers/statusController.ts +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/src/index.ts +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/src/middleware/aiwfMiddleware.ts +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/src/middleware/authMiddleware.ts +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/src/middleware/errorHandler.ts +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/src/middleware/notFoundHandler.ts +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/src/middleware/requestLogger.ts +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/src/routes/aiwf.ts +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/src/routes/index.ts +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/src/routes/v1/index.ts +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/src/utils/logger.ts +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/tests/app.test.ts +0 -0
- /package/{.aiwf/templates → templates}/api-server/template/tsconfig.json +0 -0
- /package/{.aiwf/templates → templates}/npm-library/config.json +0 -0
- /package/{.aiwf/templates → templates}/npm-library/template/LICENSE +0 -0
- /package/{.aiwf/templates → templates}/npm-library/template/README.md +0 -0
- /package/{.aiwf/templates → templates}/npm-library/template/package.json +0 -0
- /package/{.aiwf/templates → templates}/web-app/config.json +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/.aiwf/config.json +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/.aiwf/feature-ledger.json +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/.aiwf/personas/fullstack-developer.json +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/.aiwf/scripts/cli.js +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/.eslintrc.cjs +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/README.md +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/index.html +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/package.json +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/postcss.config.js +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/public/vite.svg +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/src/App.tsx +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/src/components/Layout.tsx +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/src/components/aiwf/ContextStatus.tsx +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/src/components/aiwf/FeatureLedger.tsx +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/src/components/aiwf/TokenUsage.tsx +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/src/index.css +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/src/main.tsx +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/src/pages/AiwfDashboard.tsx +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/src/pages/HomePage.tsx +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/src/pages/NotFound.tsx +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/src/stores/aiwfStore.ts +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/src/types/global.d.ts +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/src/vite-env.d.ts +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/tailwind.config.js +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/tsconfig.json +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/tsconfig.node.json +0 -0
- /package/{.aiwf/templates → templates}/web-app/template/vite.config.ts +0 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 스프린트 태스크 관리 명령어
|
|
5
|
+
* 스프린트에 개별 태스크를 추가하고 관리하는 기능
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import ora from 'ora';
|
|
12
|
+
|
|
13
|
+
// 프로젝트 루트에서 .aiwf 디렉토리 찾기
|
|
14
|
+
async function findProjectRoot(startDir = process.cwd()) {
|
|
15
|
+
let currentDir = startDir;
|
|
16
|
+
|
|
17
|
+
while (currentDir !== path.parse(currentDir).root) {
|
|
18
|
+
try {
|
|
19
|
+
const aiwfPath = path.join(currentDir, '.aiwf');
|
|
20
|
+
await fs.access(aiwfPath);
|
|
21
|
+
return currentDir;
|
|
22
|
+
} catch {
|
|
23
|
+
currentDir = path.dirname(currentDir);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
throw new Error('.aiwf 디렉토리를 찾을 수 없습니다. AIWF 프로젝트 내에서 실행해주세요.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 스프린트 존재 확인
|
|
31
|
+
async function verifySprintExists(projectRoot, sprintId) {
|
|
32
|
+
const sprintsDir = path.join(projectRoot, '.aiwf', '03_SPRINTS');
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const entries = await fs.readdir(sprintsDir);
|
|
36
|
+
const sprintDir = entries.find(entry => entry.startsWith(sprintId + '_'));
|
|
37
|
+
|
|
38
|
+
if (!sprintDir) {
|
|
39
|
+
throw new Error(`스프린트 ${sprintId}를 찾을 수 없습니다.`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return path.join(sprintsDir, sprintDir);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new Error(`스프린트 디렉토리 읽기 실패: ${error.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 다음 태스크 번호 결정
|
|
49
|
+
async function getNextTaskNumber(sprintPath, sprintId) {
|
|
50
|
+
try {
|
|
51
|
+
const files = await fs.readdir(sprintPath);
|
|
52
|
+
const taskFiles = files.filter(f => f.match(/^T\d+_/));
|
|
53
|
+
|
|
54
|
+
if (taskFiles.length === 0) {
|
|
55
|
+
return 'T01';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const numbers = taskFiles.map(f => {
|
|
59
|
+
const match = f.match(/^T(\d+)_/);
|
|
60
|
+
return match ? parseInt(match[1]) : 0;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const maxNumber = Math.max(...numbers);
|
|
64
|
+
return `T${String(maxNumber + 1).padStart(2, '0')}`;
|
|
65
|
+
} catch {
|
|
66
|
+
return 'T01';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 태스크 파일 생성
|
|
71
|
+
async function createTaskFile(sprintPath, taskNumber, sprintId, taskTitle) {
|
|
72
|
+
const timestamp = new Date().toISOString().slice(0, 16).replace('T', ' ');
|
|
73
|
+
const fileName = `${taskNumber}_${sprintId}_${taskTitle.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_가-힣]/g, '')}.md`;
|
|
74
|
+
const filePath = path.join(sprintPath, fileName);
|
|
75
|
+
|
|
76
|
+
const content = `# ${taskNumber}: ${taskTitle}
|
|
77
|
+
|
|
78
|
+
**스프린트**: ${sprintId}
|
|
79
|
+
**생성일**: ${timestamp}
|
|
80
|
+
**상태**: pending
|
|
81
|
+
**우선순위**: medium
|
|
82
|
+
**예상 시간**: TBD
|
|
83
|
+
|
|
84
|
+
## 개요
|
|
85
|
+
|
|
86
|
+
[태스크에 대한 간단한 설명을 작성하세요]
|
|
87
|
+
|
|
88
|
+
## 요구사항
|
|
89
|
+
|
|
90
|
+
- [ ] [구체적인 요구사항 1]
|
|
91
|
+
- [ ] [구체적인 요구사항 2]
|
|
92
|
+
- [ ] [구체적인 요구사항 3]
|
|
93
|
+
|
|
94
|
+
## 승인 기준
|
|
95
|
+
|
|
96
|
+
- [ ] [완료 조건 1]
|
|
97
|
+
- [ ] [완료 조건 2]
|
|
98
|
+
- [ ] [완료 조건 3]
|
|
99
|
+
|
|
100
|
+
## 기술 가이드라인
|
|
101
|
+
|
|
102
|
+
- [관련 아키텍처 결정 참조]
|
|
103
|
+
- [기존 코드베이스 패턴 참조]
|
|
104
|
+
- [사용할 기술 스택]
|
|
105
|
+
|
|
106
|
+
## 구현 노트
|
|
107
|
+
|
|
108
|
+
- [구현 시 고려사항]
|
|
109
|
+
- [잠재적 리스크]
|
|
110
|
+
- [테스트 전략]
|
|
111
|
+
|
|
112
|
+
## 참조
|
|
113
|
+
|
|
114
|
+
- [관련 문서나 코드 링크]
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
await fs.writeFile(filePath, content);
|
|
118
|
+
return { filePath, fileName, taskNumber };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 스프린트 메타 파일 업데이트
|
|
122
|
+
async function updateSprintMeta(sprintPath, taskNumber, taskTitle) {
|
|
123
|
+
const files = await fs.readdir(sprintPath);
|
|
124
|
+
const metaFile = files.find(f => f.includes('sprint_meta.md'));
|
|
125
|
+
|
|
126
|
+
if (!metaFile) {
|
|
127
|
+
console.warn(chalk.yellow('⚠️ 스프린트 메타 파일을 찾을 수 없습니다.'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const metaPath = path.join(sprintPath, metaFile);
|
|
132
|
+
let content = await fs.readFile(metaPath, 'utf-8');
|
|
133
|
+
|
|
134
|
+
// 태스크 섹션 찾기 및 업데이트
|
|
135
|
+
const taskSectionRegex = /## 태스크 목록[\s\S]*?(?=##|$)/;
|
|
136
|
+
const taskMatch = content.match(taskSectionRegex);
|
|
137
|
+
|
|
138
|
+
if (taskMatch) {
|
|
139
|
+
const newTaskLine = `- [ ] ${taskNumber}: ${taskTitle} - 상태: pending\n`;
|
|
140
|
+
const updatedSection = taskMatch[0] + newTaskLine;
|
|
141
|
+
content = content.replace(taskSectionRegex, updatedSection);
|
|
142
|
+
} else {
|
|
143
|
+
// 태스크 섹션이 없으면 추가
|
|
144
|
+
content += `\n## 태스크 목록\n\n- [ ] ${taskNumber}: ${taskTitle} - 상태: pending\n`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await fs.writeFile(metaPath, content);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 매니페스트 업데이트
|
|
151
|
+
async function updateManifest(projectRoot, sprintId, taskNumber, taskTitle) {
|
|
152
|
+
const manifestPath = path.join(projectRoot, '.aiwf', '00_PROJECT_MANIFEST.md');
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
let content = await fs.readFile(manifestPath, 'utf-8');
|
|
156
|
+
|
|
157
|
+
// 최종 업데이트 날짜 변경
|
|
158
|
+
const dateRegex = /\*\*마지막 업데이트\*\*: \d{4}-\d{2}-\d{2}/;
|
|
159
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
160
|
+
content = content.replace(dateRegex, `**마지막 업데이트**: ${today}`);
|
|
161
|
+
|
|
162
|
+
// 활성 태스크 섹션에 추가
|
|
163
|
+
const activeTasksRegex = /## 활성 태스크[\s\S]*?### 계획됨/;
|
|
164
|
+
const match = content.match(activeTasksRegex);
|
|
165
|
+
|
|
166
|
+
if (match) {
|
|
167
|
+
const newTaskLine = `\n- **${taskNumber}_${sprintId}**: ${taskTitle} (pending → ${today} 생성)\n`;
|
|
168
|
+
const updatedSection = match[0].replace('### 계획됨', newTaskLine + '### 계획됨');
|
|
169
|
+
content = content.replace(activeTasksRegex, updatedSection);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 태스크 통계 업데이트
|
|
173
|
+
const statsRegex = /- \*\*총 태스크\*\*: (\d+)/;
|
|
174
|
+
const statsMatch = content.match(statsRegex);
|
|
175
|
+
if (statsMatch) {
|
|
176
|
+
const total = parseInt(statsMatch[1]) + 1;
|
|
177
|
+
content = content.replace(statsRegex, `- **총 태스크**: ${total}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await fs.writeFile(manifestPath, content);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.warn(chalk.yellow(`⚠️ 매니페스트 업데이트 실패: ${error.message}`));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 메인 함수
|
|
187
|
+
export async function addTaskToSprint(sprintId, taskTitle) {
|
|
188
|
+
const spinner = ora('스프린트에 태스크 추가 중...').start();
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
// 1. 프로젝트 루트 찾기
|
|
192
|
+
const projectRoot = await findProjectRoot();
|
|
193
|
+
|
|
194
|
+
// 2. 스프린트 존재 확인
|
|
195
|
+
spinner.text = '스프린트 확인 중...';
|
|
196
|
+
const sprintPath = await verifySprintExists(projectRoot, sprintId);
|
|
197
|
+
|
|
198
|
+
// 3. 다음 태스크 번호 결정
|
|
199
|
+
spinner.text = '태스크 번호 결정 중...';
|
|
200
|
+
const taskNumber = await getNextTaskNumber(sprintPath, sprintId);
|
|
201
|
+
|
|
202
|
+
// 4. 태스크 파일 생성
|
|
203
|
+
spinner.text = '태스크 파일 생성 중...';
|
|
204
|
+
const { filePath, fileName, taskNumber: finalTaskNumber } = await createTaskFile(
|
|
205
|
+
sprintPath,
|
|
206
|
+
taskNumber,
|
|
207
|
+
sprintId,
|
|
208
|
+
taskTitle
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// 5. 스프린트 메타 업데이트
|
|
212
|
+
spinner.text = '스프린트 메타 업데이트 중...';
|
|
213
|
+
await updateSprintMeta(sprintPath, finalTaskNumber, taskTitle);
|
|
214
|
+
|
|
215
|
+
// 6. 매니페스트 업데이트
|
|
216
|
+
spinner.text = '프로젝트 매니페스트 업데이트 중...';
|
|
217
|
+
await updateManifest(projectRoot, sprintId, finalTaskNumber, taskTitle);
|
|
218
|
+
|
|
219
|
+
spinner.succeed('태스크 추가 완료!');
|
|
220
|
+
|
|
221
|
+
// 성공 메시지
|
|
222
|
+
console.log('\n' + chalk.green('✅ 태스크 추가 완료!'));
|
|
223
|
+
console.log('\n📄 생성된 파일: ' + chalk.cyan(fileName));
|
|
224
|
+
console.log('🏃 스프린트: ' + chalk.yellow(sprintId));
|
|
225
|
+
console.log('📋 태스크: ' + chalk.yellow(`${finalTaskNumber} - ${taskTitle}`));
|
|
226
|
+
console.log('⏱️ 상태: ' + chalk.blue('pending'));
|
|
227
|
+
|
|
228
|
+
console.log('\n' + chalk.bold('📝 다음 단계:'));
|
|
229
|
+
console.log('1. 태스크 파일을 열어 세부사항 작성');
|
|
230
|
+
console.log(`2. ${chalk.cyan(`/aiwf:do_task ${finalTaskNumber}`)}로 작업 시작`);
|
|
231
|
+
console.log(`3. 완료 후 ${chalk.cyan(`/aiwf:update_task_status ${finalTaskNumber} completed`)} 실행`);
|
|
232
|
+
|
|
233
|
+
console.log('\n💡 팁: 태스크 목록 확인은 ' + chalk.cyan(`/aiwf:list_sprint_tasks ${sprintId}`));
|
|
234
|
+
|
|
235
|
+
return { success: true, taskNumber: finalTaskNumber, filePath };
|
|
236
|
+
|
|
237
|
+
} catch (error) {
|
|
238
|
+
spinner.fail(`태스크 추가 실패: ${error.message}`);
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// CLI 실행
|
|
244
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
245
|
+
const args = process.argv.slice(2);
|
|
246
|
+
|
|
247
|
+
if (args.length < 2) {
|
|
248
|
+
console.error(chalk.red('사용법: aiwf-sprint-task <스프린트ID> <태스크 제목>'));
|
|
249
|
+
console.error(chalk.gray('예시: aiwf-sprint-task S03 "API 성능 최적화"'));
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const [sprintId, ...titleParts] = args;
|
|
254
|
+
const taskTitle = titleParts.join(' ').replace(/^["']|["']$/g, '');
|
|
255
|
+
|
|
256
|
+
addTaskToSprint(sprintId, taskTitle).catch(error => {
|
|
257
|
+
console.error(chalk.red(`오류: ${error.message}`));
|
|
258
|
+
process.exit(1);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export default addTaskToSprint;
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import https from 'https';
|
|
2
2
|
import { createWriteStream } from 'fs';
|
|
3
|
+
import fs from 'fs/promises';
|
|
3
4
|
import { pipeline } from 'stream/promises';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
|
|
6
7
|
const GITHUB_RAW_URL = 'https://raw.githubusercontent.com/moonklabs/aiwf/main';
|
|
7
8
|
const GITHUB_CONTENT_PREFIX = 'claude-code/aiwf';
|
|
9
|
+
const GITHUB_API_URL = 'https://api.github.com/repos/moonklabs/aiwf/contents';
|
|
10
|
+
const GITHUB_OWNER = 'moonklabs';
|
|
11
|
+
const GITHUB_REPO = 'aiwf';
|
|
12
|
+
const GITHUB_BRANCH = 'main';
|
|
8
13
|
|
|
9
14
|
// Pre-defined list of command files
|
|
10
15
|
const COMMAND_FILES = [
|
|
@@ -81,16 +86,22 @@ const PROMPT_FILES = [
|
|
|
81
86
|
/**
|
|
82
87
|
* Fetch content from URL
|
|
83
88
|
* @param {string} url - The URL to fetch from
|
|
89
|
+
* @param {Object} headers - Optional headers
|
|
84
90
|
* @returns {Promise<string>} The response content
|
|
85
91
|
*/
|
|
86
|
-
async function fetchContent(url) {
|
|
92
|
+
async function fetchContent(url, headers = {}) {
|
|
87
93
|
return new Promise((resolve, reject) => {
|
|
88
|
-
|
|
94
|
+
const defaultHeaders = { 'User-Agent': 'aiwf' };
|
|
95
|
+
const requestHeaders = { ...defaultHeaders, ...headers };
|
|
96
|
+
|
|
97
|
+
https.get(url, { headers: requestHeaders }, (res) => {
|
|
89
98
|
let data = '';
|
|
90
99
|
res.on('data', chunk => data += chunk);
|
|
91
100
|
res.on('end', () => {
|
|
92
101
|
if (res.statusCode === 200) {
|
|
93
102
|
resolve(data);
|
|
103
|
+
} else if (res.statusCode === 403 && res.headers['x-ratelimit-remaining'] === '0') {
|
|
104
|
+
reject(new Error('GitHub API rate limit exceeded. Please try again later.'));
|
|
94
105
|
} else {
|
|
95
106
|
reject(new Error(`Failed to fetch ${url}: ${res.statusCode}`));
|
|
96
107
|
}
|
|
@@ -103,31 +114,164 @@ async function fetchContent(url) {
|
|
|
103
114
|
}
|
|
104
115
|
|
|
105
116
|
/**
|
|
106
|
-
*
|
|
117
|
+
* Sleep for specified milliseconds
|
|
118
|
+
* @param {number} ms - Milliseconds to sleep
|
|
119
|
+
* @returns {Promise<void>}
|
|
120
|
+
*/
|
|
121
|
+
function sleep(ms) {
|
|
122
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Fetch directory contents from GitHub API with retry logic
|
|
127
|
+
* @param {string} path - Repository path
|
|
128
|
+
* @param {number} retries - Number of retries for rate limit
|
|
129
|
+
* @returns {Promise<Array>} Array of file/directory objects
|
|
130
|
+
*/
|
|
131
|
+
async function fetchGitHubDirectory(path, retries = 3) {
|
|
132
|
+
const apiUrl = `${GITHUB_API_URL}/${path}?ref=${GITHUB_BRANCH}`;
|
|
133
|
+
|
|
134
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
135
|
+
try {
|
|
136
|
+
const response = await fetchContent(apiUrl, {
|
|
137
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
138
|
+
});
|
|
139
|
+
return JSON.parse(response);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error.message.includes('rate limit exceeded') && attempt < retries) {
|
|
142
|
+
console.warn(`GitHub API rate limit hit. Retrying in ${(attempt + 1) * 10} seconds...`);
|
|
143
|
+
await sleep((attempt + 1) * 10000); // 10s, 20s, 30s
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// GitHub API가 실패하면 빈 배열 반환
|
|
148
|
+
console.warn(`Failed to fetch directory listing for ${path}: ${error.message}`);
|
|
149
|
+
|
|
150
|
+
// 404 에러의 경우 정상적으로 빈 배열 반환 (디렉토리가 없는 경우)
|
|
151
|
+
if (error.message.includes('404')) {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Rate limit이나 다른 에러의 경우 재시도 후 실패
|
|
156
|
+
if (attempt === retries) {
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Recursively download directory contents
|
|
167
|
+
* @param {string} githubPath - GitHub repository path
|
|
168
|
+
* @param {string} localPath - Local destination path
|
|
169
|
+
* @param {Object} spinner - Ora spinner instance
|
|
170
|
+
* @param {boolean} debugLog - Debug logging flag
|
|
171
|
+
* @returns {Promise<void>}
|
|
172
|
+
*/
|
|
173
|
+
async function downloadDirectoryRecursive(githubPath, localPath, spinner, debugLog = false) {
|
|
174
|
+
try {
|
|
175
|
+
// GitHub API로 디렉토리 내용 가져오기
|
|
176
|
+
const contents = await fetchGitHubDirectory(githubPath);
|
|
177
|
+
|
|
178
|
+
if (contents.length === 0) {
|
|
179
|
+
if (debugLog) console.log(`No contents found in ${githubPath}`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 로컬 디렉토리 생성
|
|
184
|
+
await fs.mkdir(localPath, { recursive: true });
|
|
185
|
+
|
|
186
|
+
for (const item of contents) {
|
|
187
|
+
if (item.type === 'file') {
|
|
188
|
+
// 파일 다운로드
|
|
189
|
+
const destPath = path.join(localPath, item.name);
|
|
190
|
+
const downloadUrl = item.download_url;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
await downloadFile(downloadUrl, destPath);
|
|
194
|
+
if (spinner) {
|
|
195
|
+
spinner.text = `Downloaded: ${item.name}`;
|
|
196
|
+
}
|
|
197
|
+
if (debugLog) console.log(`Downloaded: ${githubPath}/${item.name}`);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (debugLog) console.error(`Failed to download ${item.name}: ${error.message}`);
|
|
200
|
+
}
|
|
201
|
+
} else if (item.type === 'dir') {
|
|
202
|
+
// 하위 디렉토리 재귀적으로 처리
|
|
203
|
+
const subGithubPath = `${githubPath}/${item.name}`;
|
|
204
|
+
const subLocalPath = path.join(localPath, item.name);
|
|
205
|
+
|
|
206
|
+
if (spinner) {
|
|
207
|
+
spinner.text = `Processing directory: ${item.name}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await downloadDirectoryRecursive(subGithubPath, subLocalPath, spinner, debugLog);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (debugLog) console.error(`Error processing directory ${githubPath}: ${error.message}`);
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Download a file from URL to destination path with retry logic
|
|
107
221
|
* @param {string} url - The URL to download from
|
|
108
222
|
* @param {string} destPath - The destination file path
|
|
223
|
+
* @param {number} retries - Number of retries
|
|
109
224
|
* @returns {Promise<void>}
|
|
110
225
|
*/
|
|
111
|
-
async function downloadFile(url, destPath) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
226
|
+
async function downloadFile(url, destPath, retries = 3) {
|
|
227
|
+
// Ensure destination directory exists
|
|
228
|
+
const destDir = path.dirname(destPath);
|
|
229
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
230
|
+
|
|
231
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
232
|
+
try {
|
|
233
|
+
await new Promise((resolve, reject) => {
|
|
234
|
+
https.get(url, { headers: { 'User-Agent': 'aiwf' } }, (response) => {
|
|
235
|
+
if (response.statusCode === 403 && response.headers['x-ratelimit-remaining'] === '0') {
|
|
236
|
+
response.destroy();
|
|
237
|
+
reject(new Error('GitHub API rate limit exceeded'));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (response.statusCode !== 200) {
|
|
242
|
+
response.destroy();
|
|
243
|
+
reject(new Error(`Failed to download ${url}: ${response.statusCode}`));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const file = createWriteStream(destPath);
|
|
248
|
+
pipeline(response, file)
|
|
249
|
+
.then(() => {
|
|
250
|
+
resolve();
|
|
251
|
+
})
|
|
252
|
+
.catch((error) => {
|
|
253
|
+
reject(error);
|
|
254
|
+
});
|
|
255
|
+
}).on('error', (err) => {
|
|
256
|
+
reject(err);
|
|
126
257
|
});
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// 성공하면 반환
|
|
261
|
+
return;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (error.message.includes('rate limit exceeded') && attempt < retries) {
|
|
264
|
+
console.warn(`Rate limit hit while downloading ${path.basename(destPath)}. Retrying in ${(attempt + 1) * 10} seconds...`);
|
|
265
|
+
await sleep((attempt + 1) * 10000);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 마지막 시도에서도 실패하면 에러 throw
|
|
270
|
+
if (attempt === retries) {
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
131
275
|
}
|
|
132
276
|
|
|
133
277
|
/**
|
|
@@ -159,17 +303,33 @@ async function downloadFileList(githubPath, localPath, fileList, spinner, messag
|
|
|
159
303
|
}
|
|
160
304
|
|
|
161
305
|
/**
|
|
162
|
-
* Download directory contents using predefined file lists
|
|
306
|
+
* Download directory contents using predefined file lists or dynamic fetching
|
|
163
307
|
* @param {string} githubPath - GitHub repository path
|
|
164
308
|
* @param {string} localPath - Local destination path
|
|
165
309
|
* @param {Object} spinner - Ora spinner instance
|
|
166
310
|
* @param {Object} messages - Localized messages
|
|
167
311
|
* @param {string} [selectedLanguage='en'] - Selected language
|
|
312
|
+
* @param {boolean} [useDynamicFetch=false] - Use GitHub API for dynamic fetching
|
|
168
313
|
* @returns {Promise<void>}
|
|
169
314
|
*/
|
|
170
|
-
async function downloadDirectory(githubPath, localPath, spinner, messages, selectedLanguage = 'en') {
|
|
315
|
+
async function downloadDirectory(githubPath, localPath, spinner, messages, selectedLanguage = 'en', useDynamicFetch = false) {
|
|
171
316
|
try {
|
|
172
|
-
//
|
|
317
|
+
// .aiwf 디렉토리나 알 수 없는 디렉토리의 경우 동적 다운로드 사용
|
|
318
|
+
if (useDynamicFetch || githubPath.includes('.aiwf') ||
|
|
319
|
+
(!githubPath.includes('.claude/commands/aiwf') &&
|
|
320
|
+
!githubPath.includes('rules/global') &&
|
|
321
|
+
!githubPath.includes('rules/manual'))) {
|
|
322
|
+
|
|
323
|
+
if (spinner) {
|
|
324
|
+
spinner.text = `Fetching directory structure: ${githubPath}`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// GitHub API를 사용한 동적 디렉토리 다운로드
|
|
328
|
+
await downloadDirectoryRecursive(githubPath, localPath, spinner, messages && messages.debugLog);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 기존 로직: 사전 정의된 파일 리스트 사용
|
|
173
333
|
let fileList = [];
|
|
174
334
|
|
|
175
335
|
if (githubPath.includes('.claude/commands/aiwf')) {
|
|
@@ -178,16 +338,11 @@ async function downloadDirectory(githubPath, localPath, spinner, messages, selec
|
|
|
178
338
|
fileList = GLOBAL_RULES_FILES;
|
|
179
339
|
} else if (githubPath.includes('rules/manual')) {
|
|
180
340
|
fileList = MANUAL_RULES_FILES;
|
|
181
|
-
} else if (githubPath.includes('98_PROMPTS')) {
|
|
182
|
-
fileList = PROMPT_FILES;
|
|
183
|
-
} else if (githubPath.includes('99_TEMPLATES')) {
|
|
184
|
-
fileList = TEMPLATE_FILES;
|
|
185
|
-
} else {
|
|
186
|
-
// For unknown directories, try to download common files
|
|
187
|
-
fileList = ['README.md', 'index.md'];
|
|
188
341
|
}
|
|
189
342
|
|
|
190
|
-
|
|
343
|
+
if (fileList.length > 0) {
|
|
344
|
+
await downloadFileList(githubPath, localPath, fileList, spinner, messages);
|
|
345
|
+
}
|
|
191
346
|
} catch (error) {
|
|
192
347
|
throw new Error(`Failed to download directory ${githubPath}: ${error.message}`);
|
|
193
348
|
}
|
|
@@ -198,8 +353,14 @@ export {
|
|
|
198
353
|
downloadFile,
|
|
199
354
|
downloadFileList,
|
|
200
355
|
downloadDirectory,
|
|
356
|
+
downloadDirectoryRecursive,
|
|
357
|
+
fetchGitHubDirectory,
|
|
201
358
|
GITHUB_RAW_URL,
|
|
202
359
|
GITHUB_CONTENT_PREFIX,
|
|
360
|
+
GITHUB_API_URL,
|
|
361
|
+
GITHUB_OWNER,
|
|
362
|
+
GITHUB_REPO,
|
|
363
|
+
GITHUB_BRANCH,
|
|
203
364
|
COMMAND_FILES,
|
|
204
365
|
GLOBAL_RULES_FILES,
|
|
205
366
|
MANUAL_RULES_FILES,
|
package/src/lib/installer.js
CHANGED
|
@@ -182,9 +182,14 @@ async function handleExistingInstallation(msg, options) {
|
|
|
182
182
|
async function createDirectoryStructure() {
|
|
183
183
|
const aiwfDirs = getAllAiwfDirectories();
|
|
184
184
|
|
|
185
|
+
// Create AIWF directories
|
|
185
186
|
for (const dir of aiwfDirs) {
|
|
186
187
|
await fs.mkdir(dir, { recursive: true });
|
|
187
188
|
}
|
|
189
|
+
|
|
190
|
+
// Create tool-specific directories
|
|
191
|
+
await fs.mkdir(TOOL_DIRS.CURSOR_RULES, { recursive: true });
|
|
192
|
+
await fs.mkdir(TOOL_DIRS.WINDSURF_RULES, { recursive: true });
|
|
188
193
|
}
|
|
189
194
|
|
|
190
195
|
/**
|
|
@@ -214,20 +219,46 @@ async function downloadManifest(languagePath, spinner, msg, debugLog) {
|
|
|
214
219
|
async function downloadTemplates(languagePath, spinner, msg, debugLog) {
|
|
215
220
|
try {
|
|
216
221
|
logWithSpinner(spinner, msg.downloadingTemplates, debugLog);
|
|
222
|
+
|
|
223
|
+
// .aiwf 전체 디렉토리를 동적으로 다운로드
|
|
224
|
+
logWithSpinner(spinner, `Downloading .aiwf directory structure...`, debugLog);
|
|
217
225
|
await downloadDirectory(
|
|
218
|
-
`${GITHUB_CONTENT_PREFIX}/${languagePath}/.aiwf
|
|
219
|
-
AIWF_DIRS.
|
|
220
|
-
spinner,
|
|
221
|
-
msg
|
|
222
|
-
);
|
|
223
|
-
await downloadDirectory(
|
|
224
|
-
`${GITHUB_CONTENT_PREFIX}/${languagePath}/.aiwf/99_TEMPLATES`,
|
|
225
|
-
AIWF_DIRS.TEMPLATES,
|
|
226
|
+
`${GITHUB_CONTENT_PREFIX}/${languagePath}/.aiwf`,
|
|
227
|
+
AIWF_DIRS.ROOT,
|
|
226
228
|
spinner,
|
|
227
|
-
msg
|
|
229
|
+
msg,
|
|
230
|
+
languagePath,
|
|
231
|
+
true // useDynamicFetch = true
|
|
228
232
|
);
|
|
233
|
+
|
|
234
|
+
logWithSpinner(spinner, `Successfully downloaded .aiwf directory`, debugLog);
|
|
229
235
|
} catch (error) {
|
|
230
|
-
logWithSpinner(spinner,
|
|
236
|
+
logWithSpinner(spinner, `Error downloading .aiwf: ${error.message}`, debugLog);
|
|
237
|
+
|
|
238
|
+
// Fallback: 개별 디렉토리 다운로드 시도
|
|
239
|
+
try {
|
|
240
|
+
logWithSpinner(spinner, `Trying individual directory downloads...`, debugLog);
|
|
241
|
+
|
|
242
|
+
await downloadDirectory(
|
|
243
|
+
`${GITHUB_CONTENT_PREFIX}/${languagePath}/.aiwf/98_PROMPTS`,
|
|
244
|
+
AIWF_DIRS.PROMPTS,
|
|
245
|
+
spinner,
|
|
246
|
+
msg,
|
|
247
|
+
languagePath,
|
|
248
|
+
true
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
await downloadDirectory(
|
|
252
|
+
`${GITHUB_CONTENT_PREFIX}/${languagePath}/.aiwf/99_TEMPLATES`,
|
|
253
|
+
AIWF_DIRS.TEMPLATES,
|
|
254
|
+
spinner,
|
|
255
|
+
msg,
|
|
256
|
+
languagePath,
|
|
257
|
+
true
|
|
258
|
+
);
|
|
259
|
+
} catch (fallbackError) {
|
|
260
|
+
logWithSpinner(spinner, msg.templatesNotFound, debugLog);
|
|
261
|
+
}
|
|
231
262
|
}
|
|
232
263
|
}
|
|
233
264
|
|
|
@@ -277,7 +308,8 @@ async function updateCommands(languagePath, spinner, msg, debugLog) {
|
|
|
277
308
|
TOOL_DIRS.CLAUDE_COMMANDS,
|
|
278
309
|
spinner,
|
|
279
310
|
msg,
|
|
280
|
-
languagePath
|
|
311
|
+
languagePath,
|
|
312
|
+
false // commands는 기존 방식 유지 (파일 리스트 사용)
|
|
281
313
|
);
|
|
282
314
|
} catch (error) {
|
|
283
315
|
logWithSpinner(spinner, msg.commandsNotFound, debugLog);
|
|
@@ -301,7 +333,7 @@ async function downloadAndProcessRules(spinner, msg, debugLog) {
|
|
|
301
333
|
// Try to download rules/global from GitHub
|
|
302
334
|
try {
|
|
303
335
|
await downloadDirectory(
|
|
304
|
-
|
|
336
|
+
`rules/global`,
|
|
305
337
|
tmpRulesGlobal,
|
|
306
338
|
spinner,
|
|
307
339
|
debugLog
|
|
@@ -310,20 +342,25 @@ async function downloadAndProcessRules(spinner, msg, debugLog) {
|
|
|
310
342
|
} catch (error) {
|
|
311
343
|
logWithSpinner(spinner, `Failed to download rules/global from GitHub, trying local files`, debugLog);
|
|
312
344
|
// Fallback to local files
|
|
313
|
-
const localRulesGlobal = 'rules/global';
|
|
345
|
+
const localRulesGlobal = path.join(process.cwd(), 'rules/global');
|
|
314
346
|
try {
|
|
347
|
+
logWithSpinner(spinner, `Trying to access: ${localRulesGlobal}`, debugLog);
|
|
315
348
|
await fs.access(localRulesGlobal);
|
|
349
|
+
logWithSpinner(spinner, `Successfully accessed local rules/global`, debugLog);
|
|
316
350
|
// Copy local files to temp directory
|
|
317
351
|
await fs.mkdir(tmpRulesGlobal, { recursive: true });
|
|
318
352
|
const files = await fs.readdir(localRulesGlobal);
|
|
353
|
+
logWithSpinner(spinner, `Found ${files.length} files in rules/global: ${files.join(', ')}`, debugLog);
|
|
319
354
|
for (const file of files) {
|
|
320
355
|
const srcPath = path.join(localRulesGlobal, file);
|
|
321
356
|
const destPath = path.join(tmpRulesGlobal, file);
|
|
322
357
|
await fs.copyFile(srcPath, destPath);
|
|
358
|
+
logWithSpinner(spinner, `Copied: ${file}`, debugLog);
|
|
323
359
|
}
|
|
324
360
|
useLocalGlobal = true;
|
|
325
361
|
logWithSpinner(spinner, `Using local rules/global directory`, debugLog);
|
|
326
362
|
} catch (localError) {
|
|
363
|
+
logWithSpinner(spinner, `Local rules/global error: ${localError.message}`, debugLog);
|
|
327
364
|
logWithSpinner(spinner, msg.rulesGlobalNotFound, debugLog);
|
|
328
365
|
}
|
|
329
366
|
}
|
|
@@ -331,7 +368,7 @@ async function downloadAndProcessRules(spinner, msg, debugLog) {
|
|
|
331
368
|
// Try to download rules/manual from GitHub
|
|
332
369
|
try {
|
|
333
370
|
await downloadDirectory(
|
|
334
|
-
|
|
371
|
+
`rules/manual`,
|
|
335
372
|
tmpRulesManual,
|
|
336
373
|
spinner,
|
|
337
374
|
debugLog
|
|
@@ -340,7 +377,7 @@ async function downloadAndProcessRules(spinner, msg, debugLog) {
|
|
|
340
377
|
} catch (error) {
|
|
341
378
|
logWithSpinner(spinner, `Failed to download rules/manual from GitHub, trying local files`, debugLog);
|
|
342
379
|
// Fallback to local files
|
|
343
|
-
const localRulesManual = 'rules/manual';
|
|
380
|
+
const localRulesManual = path.join(process.cwd(), 'rules/manual');
|
|
344
381
|
try {
|
|
345
382
|
await fs.access(localRulesManual);
|
|
346
383
|
// Copy local files to temp directory
|