aiwf 0.3.14 → 0.3.16
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.ko.md +19 -10
- package/README.md +54 -9
- package/docs/AI-WORKFLOW.ko.md +1 -1
- package/docs/AI-WORKFLOW.md +1 -1
- package/docs/API_REFERENCE_FULL.ko.md +979 -0
- package/docs/API_REFERENCE_FULL.md +979 -0
- package/docs/ARCHITECTURE.ko.md +314 -0
- package/docs/ARCHITECTURE.md +314 -0
- package/docs/GETTING_STARTED.ko.md +219 -0
- package/docs/GETTING_STARTED.md +1 -2
- package/docs/PERFORMANCE_GUIDELINES.ko.md +388 -0
- package/docs/ROADMAP_v0.4.0.md +286 -0
- package/docs/TROUBLESHOOTING.ko.md +366 -0
- package/package.json +50 -2
- package/src/DEPENDENCY_MAP.md +90 -0
- package/src/cli/index.js +1 -1
- package/src/lib/installer.js +204 -15
- package/src/lib/resources/templates/api-server/template/src/controllers/aiwfController.ts +0 -14
- package/src/lib/resources/templates/api-server/template/src/routes/aiwf.ts +0 -11
- package/src/lib/resources/templates/web-app/template/src/pages/AiwfDashboard.tsx +2 -5
- package/src/lib/resources/utils/token-reporter.js +2 -3
- package/src/utils/checkpoint-manager.js +11 -1
- package/src/utils/engineering-guard.js +11 -1
- package/src/utils/messages.js +16 -0
- package/src/utils/paths.js +2 -2
- package/templates/api-server/template/src/controllers/aiwfController.ts +0 -14
- package/templates/api-server/template/src/routes/aiwf.ts +0 -11
- package/templates/web-app/template/src/pages/AiwfDashboard.tsx +2 -5
- package/docs/CLEANUP_RECOMMENDATIONS.md +0 -86
- package/docs/git-feature-integration-guide.md +0 -253
- package/docs/guides/feature-git-integration-guide-ko.md +0 -481
- package/docs/guides/feature-git-integration-guide.md +0 -481
- package/docs/lightweight-evaluation.md +0 -198
- package/docs/performance-benchmark-report.md +0 -159
- package/docs/reports/S02-sprint-report.md +0 -190
- package/docs/usability-test-report.md +0 -170
- package/src/commands/cache-templates.js +0 -209
- package/src/commands/create-offline.js +0 -86
- package/src/commands/state-refactored.js +0 -419
- package/src/lib/resources/commands/feature-ledger.js +0 -565
- package/src/lib/resources/commands/feature_commit_report.js +0 -370
- package/src/lib/resources/commands/scan_git_history.js +0 -234
- package/src/lib/resources/commands/sync_feature_commits.js +0 -154
- package/src/lib/resources/templates/web-app/template/src/components/aiwf/FeatureLedger.tsx +0 -93
- package/src/lib/resources/utils/feature-updater.js +0 -271
- package/src/utils/git-integration.js +0 -173
- package/templates/web-app/template/src/components/aiwf/FeatureLedger.tsx +0 -93
- /package/docs/{AI_PERSONA_SYSTEM_DESIGN.md → designs/AI_PERSONA_SYSTEM_DESIGN.md} +0 -0
- /package/docs/{API_DOCUMENTATION.md → designs/API_DOCUMENTATION.md} +0 -0
- /package/docs/{API_REFERENCE.md → designs/API_REFERENCE.md} +0 -0
- /package/docs/{Enhanced_Installation_Flow_Design.md → designs/Enhanced_Installation_Flow_Design.md} +0 -0
- /package/docs/{moonklabs-metadata-system-prd.md → designs/aiwf-metadata-system-prd.md} +0 -0
- /package/docs/{offline-template-cache.md → designs/offline-template-cache.md} +0 -0
- /package/docs/{persona-aware-compression.md → designs/persona-aware-compression.md} +0 -0
|
@@ -1,370 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const { Command } = require('commander');
|
|
4
|
-
const chalk = require('chalk');
|
|
5
|
-
const ora = require('ora');
|
|
6
|
-
const fs = require('fs').promises;
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const {
|
|
9
|
-
getCommitsByFeatureId,
|
|
10
|
-
isGitRepository,
|
|
11
|
-
runGitCommand
|
|
12
|
-
} = require('../utils/git-utils');
|
|
13
|
-
const {
|
|
14
|
-
findFeatureFile,
|
|
15
|
-
readFeatureFile
|
|
16
|
-
} = require('../utils/feature-updater');
|
|
17
|
-
|
|
18
|
-
const program = new Command();
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* feature_commit_report 명령어
|
|
22
|
-
* Feature와 Git 커밋의 매핑 리포트 생성
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
program
|
|
26
|
-
.name('feature_commit_report')
|
|
27
|
-
.description('Feature와 Git 커밋의 매핑 리포트를 생성합니다')
|
|
28
|
-
.option('-f, --feature <id>', '특정 Feature ID의 리포트만 생성')
|
|
29
|
-
.option('-s, --status <status>', 'Feature 상태별 필터링 (active|completed|archived)')
|
|
30
|
-
.option('-o, --output <path>', '리포트 출력 경로', './feature_commit_report.md')
|
|
31
|
-
.option('-F, --format <format>', '출력 형식 (markdown|json|html)', 'markdown')
|
|
32
|
-
.option('-d, --detailed', '상세 커밋 정보 포함')
|
|
33
|
-
.option('-t, --timeline', '타임라인 형식으로 표시')
|
|
34
|
-
.option('-v, --verbose', '상세 로그 출력')
|
|
35
|
-
.action(async (options) => {
|
|
36
|
-
const spinner = ora();
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
// Git 저장소 확인
|
|
40
|
-
if (!await isGitRepository()) {
|
|
41
|
-
console.error(chalk.red('❌ 현재 디렉토리는 Git 저장소가 아닙니다.'));
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
spinner.start('Feature 정보 수집 중...');
|
|
46
|
-
|
|
47
|
-
// Feature 파일 수집
|
|
48
|
-
const basePath = path.join(process.cwd(), '.aiwf', '06_FEATURE_LEDGERS');
|
|
49
|
-
const statuses = options.status ? [options.status] : ['active', 'completed', 'archived'];
|
|
50
|
-
const features = [];
|
|
51
|
-
|
|
52
|
-
for (const status of statuses) {
|
|
53
|
-
const dirPath = path.join(basePath, status);
|
|
54
|
-
try {
|
|
55
|
-
const files = await fs.readdir(dirPath);
|
|
56
|
-
const featureFiles = files.filter(f => f.endsWith('.md'));
|
|
57
|
-
|
|
58
|
-
for (const file of featureFiles) {
|
|
59
|
-
const featureId = file.split('_')[0];
|
|
60
|
-
|
|
61
|
-
// 특정 Feature ID 필터링
|
|
62
|
-
if (options.feature && featureId !== options.feature) {
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const filePath = path.join(dirPath, file);
|
|
67
|
-
const { frontmatter } = await readFeatureFile(filePath);
|
|
68
|
-
|
|
69
|
-
features.push({
|
|
70
|
-
id: featureId,
|
|
71
|
-
status: status,
|
|
72
|
-
title: frontmatter.title || 'Untitled',
|
|
73
|
-
milestone: frontmatter.milestone || 'N/A',
|
|
74
|
-
assignee: frontmatter.assignee || 'Unassigned',
|
|
75
|
-
created: frontmatter.created_date || 'Unknown',
|
|
76
|
-
updated: frontmatter.last_updated || 'Unknown',
|
|
77
|
-
gitCommits: frontmatter.git_commits || [],
|
|
78
|
-
progress: frontmatter.progress_percentage || 0
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
} catch (error) {
|
|
82
|
-
// 디렉토리가 없을 수 있음
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
spinner.succeed(chalk.green(`${features.length}개 Feature 정보 수집 완료`));
|
|
88
|
-
|
|
89
|
-
if (features.length === 0) {
|
|
90
|
-
console.log(chalk.yellow('리포트를 생성할 Feature가 없습니다.'));
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// 각 Feature의 커밋 정보 수집
|
|
95
|
-
spinner.start('Git 커밋 정보 수집 중...');
|
|
96
|
-
const reportData = [];
|
|
97
|
-
|
|
98
|
-
for (const feature of features) {
|
|
99
|
-
const commits = await getCommitsByFeatureId(feature.id);
|
|
100
|
-
|
|
101
|
-
reportData.push({
|
|
102
|
-
...feature,
|
|
103
|
-
commits: commits,
|
|
104
|
-
commitCount: commits.length,
|
|
105
|
-
firstCommit: commits.length > 0 ? commits[commits.length - 1] : null,
|
|
106
|
-
lastCommit: commits.length > 0 ? commits[0] : null
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
spinner.succeed(chalk.green('커밋 정보 수집 완료'));
|
|
111
|
-
|
|
112
|
-
// 리포트 생성
|
|
113
|
-
spinner.start('리포트 생성 중...');
|
|
114
|
-
|
|
115
|
-
let reportContent;
|
|
116
|
-
switch (options.format) {
|
|
117
|
-
case 'json':
|
|
118
|
-
reportContent = generateJsonReport(reportData, options);
|
|
119
|
-
break;
|
|
120
|
-
case 'html':
|
|
121
|
-
reportContent = generateHtmlReport(reportData, options);
|
|
122
|
-
break;
|
|
123
|
-
case 'markdown':
|
|
124
|
-
default:
|
|
125
|
-
reportContent = generateMarkdownReport(reportData, options);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// 리포트 저장
|
|
129
|
-
await fs.writeFile(options.output, reportContent, 'utf8');
|
|
130
|
-
spinner.succeed(chalk.green(`리포트 생성 완료: ${options.output}`));
|
|
131
|
-
|
|
132
|
-
// 요약 정보 출력
|
|
133
|
-
console.log(chalk.cyan('\n📊 리포트 요약:'));
|
|
134
|
-
console.log(chalk.white(` 총 Feature: ${reportData.length}개`));
|
|
135
|
-
console.log(chalk.white(` 총 커밋: ${reportData.reduce((sum, f) => sum + f.commitCount, 0)}개`));
|
|
136
|
-
|
|
137
|
-
const byStatus = {};
|
|
138
|
-
reportData.forEach(f => {
|
|
139
|
-
byStatus[f.status] = (byStatus[f.status] || 0) + 1;
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
console.log(chalk.cyan('\n 상태별 분포:'));
|
|
143
|
-
Object.entries(byStatus).forEach(([status, count]) => {
|
|
144
|
-
const icon = status === 'active' ? '🟢' : status === 'completed' ? '✅' : '📦';
|
|
145
|
-
console.log(chalk.white(` ${icon} ${status}: ${count}개`));
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
} catch (error) {
|
|
149
|
-
spinner.fail(chalk.red('리포트 생성 중 오류 발생'));
|
|
150
|
-
console.error(chalk.red(error.message));
|
|
151
|
-
if (options.verbose) {
|
|
152
|
-
console.error(error.stack);
|
|
153
|
-
}
|
|
154
|
-
process.exit(1);
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Markdown 형식 리포트 생성
|
|
160
|
-
*/
|
|
161
|
-
function generateMarkdownReport(data, options) {
|
|
162
|
-
let report = '# Feature-Git Commit Mapping Report\n\n';
|
|
163
|
-
report += `Generated: ${new Date().toISOString()}\n\n`;
|
|
164
|
-
|
|
165
|
-
// 요약 섹션
|
|
166
|
-
report += '## Summary\n\n';
|
|
167
|
-
report += `- Total Features: ${data.length}\n`;
|
|
168
|
-
report += `- Total Commits: ${data.reduce((sum, f) => sum + f.commitCount, 0)}\n`;
|
|
169
|
-
report += '\n';
|
|
170
|
-
|
|
171
|
-
// 상태별 통계
|
|
172
|
-
const byStatus = {};
|
|
173
|
-
data.forEach(f => {
|
|
174
|
-
byStatus[f.status] = (byStatus[f.status] || []);
|
|
175
|
-
byStatus[f.status].push(f);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
report += '## Features by Status\n\n';
|
|
179
|
-
|
|
180
|
-
Object.entries(byStatus).forEach(([status, features]) => {
|
|
181
|
-
report += `### ${status.charAt(0).toUpperCase() + status.slice(1)} (${features.length})\n\n`;
|
|
182
|
-
|
|
183
|
-
features.forEach(feature => {
|
|
184
|
-
report += `#### ${feature.id}: ${feature.title}\n\n`;
|
|
185
|
-
report += `- **Milestone**: ${feature.milestone}\n`;
|
|
186
|
-
report += `- **Assignee**: ${feature.assignee}\n`;
|
|
187
|
-
report += `- **Progress**: ${feature.progress}%\n`;
|
|
188
|
-
report += `- **Commits**: ${feature.commitCount}\n`;
|
|
189
|
-
|
|
190
|
-
if (feature.firstCommit) {
|
|
191
|
-
report += `- **First Commit**: ${feature.firstCommit.date.substring(0, 10)}\n`;
|
|
192
|
-
report += `- **Last Commit**: ${feature.lastCommit.date.substring(0, 10)}\n`;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (options.detailed && feature.commits.length > 0) {
|
|
196
|
-
report += '\n**Commit History**:\n\n';
|
|
197
|
-
feature.commits.forEach(commit => {
|
|
198
|
-
const shortHash = commit.hash.substring(0, 7);
|
|
199
|
-
const date = commit.date.substring(0, 10);
|
|
200
|
-
const message = commit.message.split('\n')[0];
|
|
201
|
-
report += `- \`${shortHash}\` (${date}): ${message}\n`;
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
report += '\n';
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// 타임라인 뷰 (옵션)
|
|
210
|
-
if (options.timeline) {
|
|
211
|
-
report += '## Timeline View\n\n';
|
|
212
|
-
|
|
213
|
-
// 모든 커밋을 날짜순으로 정렬
|
|
214
|
-
const allCommits = [];
|
|
215
|
-
data.forEach(feature => {
|
|
216
|
-
feature.commits.forEach(commit => {
|
|
217
|
-
allCommits.push({
|
|
218
|
-
...commit,
|
|
219
|
-
featureId: feature.id,
|
|
220
|
-
featureTitle: feature.title
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
allCommits.sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
226
|
-
|
|
227
|
-
let currentMonth = '';
|
|
228
|
-
allCommits.forEach(commit => {
|
|
229
|
-
const month = commit.date.substring(0, 7);
|
|
230
|
-
if (month !== currentMonth) {
|
|
231
|
-
currentMonth = month;
|
|
232
|
-
report += `\n### ${month}\n\n`;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const shortHash = commit.hash.substring(0, 7);
|
|
236
|
-
const date = commit.date.substring(0, 10);
|
|
237
|
-
const message = commit.message.split('\n')[0];
|
|
238
|
-
report += `- **${date}** - ${commit.featureId}: ${message} (\`${shortHash}\`)\n`;
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return report;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* JSON 형식 리포트 생성
|
|
247
|
-
*/
|
|
248
|
-
function generateJsonReport(data, options) {
|
|
249
|
-
const report = {
|
|
250
|
-
generated: new Date().toISOString(),
|
|
251
|
-
summary: {
|
|
252
|
-
totalFeatures: data.length,
|
|
253
|
-
totalCommits: data.reduce((sum, f) => sum + f.commitCount, 0),
|
|
254
|
-
byStatus: {}
|
|
255
|
-
},
|
|
256
|
-
features: []
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
// 상태별 집계
|
|
260
|
-
data.forEach(f => {
|
|
261
|
-
report.summary.byStatus[f.status] = (report.summary.byStatus[f.status] || 0) + 1;
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
// Feature 데이터
|
|
265
|
-
report.features = data.map(feature => {
|
|
266
|
-
const featureData = {
|
|
267
|
-
id: feature.id,
|
|
268
|
-
title: feature.title,
|
|
269
|
-
status: feature.status,
|
|
270
|
-
milestone: feature.milestone,
|
|
271
|
-
assignee: feature.assignee,
|
|
272
|
-
progress: feature.progress,
|
|
273
|
-
commitCount: feature.commitCount,
|
|
274
|
-
created: feature.created,
|
|
275
|
-
updated: feature.updated
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
if (options.detailed) {
|
|
279
|
-
featureData.commits = feature.commits.map(commit => ({
|
|
280
|
-
hash: commit.hash,
|
|
281
|
-
author: commit.author,
|
|
282
|
-
date: commit.date,
|
|
283
|
-
message: commit.message
|
|
284
|
-
}));
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return featureData;
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
return JSON.stringify(report, null, 2);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* HTML 형식 리포트 생성
|
|
295
|
-
*/
|
|
296
|
-
function generateHtmlReport(data, options) {
|
|
297
|
-
let html = `<!DOCTYPE html>
|
|
298
|
-
<html>
|
|
299
|
-
<head>
|
|
300
|
-
<meta charset="UTF-8">
|
|
301
|
-
<title>Feature-Git Commit Mapping Report</title>
|
|
302
|
-
<style>
|
|
303
|
-
body { font-family: Arial, sans-serif; margin: 20px; }
|
|
304
|
-
h1, h2, h3 { color: #333; }
|
|
305
|
-
.summary { background: #f5f5f5; padding: 15px; border-radius: 5px; }
|
|
306
|
-
.feature { border: 1px solid #ddd; margin: 10px 0; padding: 15px; border-radius: 5px; }
|
|
307
|
-
.active { border-left: 4px solid #28a745; }
|
|
308
|
-
.completed { border-left: 4px solid #007bff; }
|
|
309
|
-
.archived { border-left: 4px solid #6c757d; }
|
|
310
|
-
.progress { background: #e9ecef; height: 20px; border-radius: 3px; }
|
|
311
|
-
.progress-bar { background: #007bff; height: 100%; border-radius: 3px; }
|
|
312
|
-
.commit { font-family: monospace; font-size: 0.9em; margin: 5px 0; }
|
|
313
|
-
code { background: #f5f5f5; padding: 2px 4px; border-radius: 3px; }
|
|
314
|
-
</style>
|
|
315
|
-
</head>
|
|
316
|
-
<body>
|
|
317
|
-
<h1>Feature-Git Commit Mapping Report</h1>
|
|
318
|
-
<p>Generated: ${new Date().toISOString()}</p>
|
|
319
|
-
|
|
320
|
-
<div class="summary">
|
|
321
|
-
<h2>Summary</h2>
|
|
322
|
-
<ul>
|
|
323
|
-
<li>Total Features: ${data.length}</li>
|
|
324
|
-
<li>Total Commits: ${data.reduce((sum, f) => sum + f.commitCount, 0)}</li>
|
|
325
|
-
</ul>
|
|
326
|
-
</div>
|
|
327
|
-
`;
|
|
328
|
-
|
|
329
|
-
// Features by status
|
|
330
|
-
const byStatus = {};
|
|
331
|
-
data.forEach(f => {
|
|
332
|
-
byStatus[f.status] = (byStatus[f.status] || []);
|
|
333
|
-
byStatus[f.status].push(f);
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
Object.entries(byStatus).forEach(([status, features]) => {
|
|
337
|
-
html += `<h2>${status.charAt(0).toUpperCase() + status.slice(1)} Features (${features.length})</h2>`;
|
|
338
|
-
|
|
339
|
-
features.forEach(feature => {
|
|
340
|
-
html += `<div class="feature ${status}">`;
|
|
341
|
-
html += `<h3>${feature.id}: ${feature.title}</h3>`;
|
|
342
|
-
html += `<p><strong>Milestone:</strong> ${feature.milestone} | `;
|
|
343
|
-
html += `<strong>Assignee:</strong> ${feature.assignee} | `;
|
|
344
|
-
html += `<strong>Commits:</strong> ${feature.commitCount}</p>`;
|
|
345
|
-
|
|
346
|
-
html += `<div class="progress">`;
|
|
347
|
-
html += `<div class="progress-bar" style="width: ${feature.progress}%"></div>`;
|
|
348
|
-
html += `</div>`;
|
|
349
|
-
html += `<p>Progress: ${feature.progress}%</p>`;
|
|
350
|
-
|
|
351
|
-
if (options.detailed && feature.commits.length > 0) {
|
|
352
|
-
html += '<h4>Commit History:</h4>';
|
|
353
|
-
feature.commits.forEach(commit => {
|
|
354
|
-
const shortHash = commit.hash.substring(0, 7);
|
|
355
|
-
const date = commit.date.substring(0, 10);
|
|
356
|
-
const message = commit.message.split('\n')[0];
|
|
357
|
-
html += `<div class="commit"><code>${shortHash}</code> (${date}): ${message}</div>`;
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
html += '</div>';
|
|
362
|
-
});
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
html += '</body></html>';
|
|
366
|
-
return html;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// 명령어 실행
|
|
370
|
-
program.parse(process.argv);
|
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const { Command } = require('commander');
|
|
4
|
-
const chalk = require('chalk');
|
|
5
|
-
const ora = require('ora');
|
|
6
|
-
const fs = require('fs').promises;
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const {
|
|
9
|
-
getFeatureRelatedCommits,
|
|
10
|
-
isGitRepository,
|
|
11
|
-
runGitCommand
|
|
12
|
-
} = require('../utils/git-utils');
|
|
13
|
-
const {
|
|
14
|
-
syncFeatureWithGit,
|
|
15
|
-
findFeatureFile
|
|
16
|
-
} = require('../utils/feature-updater');
|
|
17
|
-
|
|
18
|
-
const program = new Command();
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* scan_git_history 명령어
|
|
22
|
-
* 기존 Git 히스토리를 스캔하여 Feature와 연결
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
program
|
|
26
|
-
.name('scan_git_history')
|
|
27
|
-
.description('기존 Git 히스토리를 스캔하여 Feature Ledger와 연결합니다')
|
|
28
|
-
.option('-s, --since <date>', '스캔 시작 날짜 (YYYY-MM-DD)', '2025-01-01')
|
|
29
|
-
.option('-u, --until <date>', '스캔 종료 날짜 (YYYY-MM-DD)', 'HEAD')
|
|
30
|
-
.option('-l, --limit <number>', '스캔할 최대 커밋 수', '1000')
|
|
31
|
-
.option('-d, --dry-run', '실제 업데이트 없이 결과만 표시')
|
|
32
|
-
.option('-f, --feature <id>', '특정 Feature ID만 스캔')
|
|
33
|
-
.option('-c, --create-missing', '누락된 Feature 파일 자동 생성')
|
|
34
|
-
.option('-v, --verbose', '상세 로그 출력')
|
|
35
|
-
.action(async (options) => {
|
|
36
|
-
const spinner = ora();
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
// Git 저장소 확인
|
|
40
|
-
if (!await isGitRepository()) {
|
|
41
|
-
console.error(chalk.red('❌ 현재 디렉토리는 Git 저장소가 아닙니다.'));
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
console.log(chalk.cyan('🔍 Git 히스토리 스캔 시작'));
|
|
46
|
-
console.log(chalk.gray(` 기간: ${options.since} ~ ${options.until}`));
|
|
47
|
-
console.log(chalk.gray(` 최대 커밋 수: ${options.limit}`));
|
|
48
|
-
if (options.dryRun) {
|
|
49
|
-
console.log(chalk.yellow(' 🏃 Dry-run 모드 (실제 업데이트 없음)'));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
spinner.start('Git 히스토리 분석 중...');
|
|
53
|
-
|
|
54
|
-
// Git log 명령어로 커밋 목록 가져오기
|
|
55
|
-
let gitCommand = `log --since="${options.since}"`;
|
|
56
|
-
if (options.until !== 'HEAD') {
|
|
57
|
-
gitCommand += ` --until="${options.until}"`;
|
|
58
|
-
}
|
|
59
|
-
gitCommand += ` --max-count=${options.limit} --pretty=format:"%H"`;
|
|
60
|
-
|
|
61
|
-
if (options.feature) {
|
|
62
|
-
gitCommand += ` --grep="${options.feature}"`;
|
|
63
|
-
} else {
|
|
64
|
-
gitCommand += ` --grep="FL[0-9]\\{3\\}"`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const commitHashes = await runGitCommand(gitCommand);
|
|
68
|
-
|
|
69
|
-
if (!commitHashes) {
|
|
70
|
-
spinner.warn(chalk.yellow('Feature 관련 커밋을 찾을 수 없습니다.'));
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const hashes = commitHashes.split('\n').filter(Boolean);
|
|
75
|
-
spinner.succeed(chalk.green(`${hashes.length}개의 관련 커밋 발견`));
|
|
76
|
-
|
|
77
|
-
// 커밋 분석 및 Feature별 그룹화
|
|
78
|
-
spinner.start('커밋 분석 중...');
|
|
79
|
-
const { getCommitInfo } = require('../utils/git-utils');
|
|
80
|
-
const featureCommitMap = {};
|
|
81
|
-
const missingFeatures = new Set();
|
|
82
|
-
|
|
83
|
-
for (const hash of hashes) {
|
|
84
|
-
const commitInfo = await getCommitInfo(hash);
|
|
85
|
-
|
|
86
|
-
for (const featureId of commitInfo.featureIds) {
|
|
87
|
-
if (options.feature && featureId !== options.feature) {
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (!featureCommitMap[featureId]) {
|
|
92
|
-
featureCommitMap[featureId] = [];
|
|
93
|
-
}
|
|
94
|
-
featureCommitMap[featureId].push(commitInfo);
|
|
95
|
-
|
|
96
|
-
// Feature 파일 존재 확인
|
|
97
|
-
const featureFile = await findFeatureFile(featureId);
|
|
98
|
-
if (!featureFile) {
|
|
99
|
-
missingFeatures.add(featureId);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
spinner.succeed(chalk.green('커밋 분석 완료'));
|
|
105
|
-
|
|
106
|
-
// 분석 결과 출력
|
|
107
|
-
console.log(chalk.cyan('\n📊 스캔 결과:'));
|
|
108
|
-
console.log(chalk.white(` 발견된 Feature: ${Object.keys(featureCommitMap).length}개`));
|
|
109
|
-
console.log(chalk.white(` 총 커밋 수: ${hashes.length}개`));
|
|
110
|
-
|
|
111
|
-
if (missingFeatures.size > 0) {
|
|
112
|
-
console.log(chalk.yellow(` 누락된 Feature: ${missingFeatures.size}개`));
|
|
113
|
-
if (options.verbose) {
|
|
114
|
-
console.log(chalk.gray(` ${[...missingFeatures].join(', ')}`));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Feature별 상세 정보
|
|
119
|
-
console.log(chalk.cyan('\n📋 Feature별 커밋 수:'));
|
|
120
|
-
for (const [featureId, commits] of Object.entries(featureCommitMap)) {
|
|
121
|
-
const isMissing = missingFeatures.has(featureId);
|
|
122
|
-
const statusIcon = isMissing ? '❌' : '✅';
|
|
123
|
-
const statusText = isMissing ? ' (파일 없음)' : '';
|
|
124
|
-
console.log(chalk.white(` ${statusIcon} ${featureId}: ${commits.length}개 커밋${statusText}`));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// 누락된 Feature 파일 생성 (옵션)
|
|
128
|
-
if (options.createMissing && missingFeatures.size > 0 && !options.dryRun) {
|
|
129
|
-
console.log(chalk.cyan('\n📝 누락된 Feature 파일 생성 중...'));
|
|
130
|
-
|
|
131
|
-
for (const featureId of missingFeatures) {
|
|
132
|
-
spinner.start(`${featureId} 파일 생성 중...`);
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
// Feature 파일 생성
|
|
136
|
-
const featureDir = path.join(process.cwd(), '.aiwf', '06_FEATURE_LEDGERS', 'active');
|
|
137
|
-
await fs.mkdir(featureDir, { recursive: true });
|
|
138
|
-
|
|
139
|
-
const fileName = `${featureId}_Auto_Generated.md`;
|
|
140
|
-
const filePath = path.join(featureDir, fileName);
|
|
141
|
-
|
|
142
|
-
const content = `---
|
|
143
|
-
feature_id: ${featureId}
|
|
144
|
-
title: Auto Generated Feature
|
|
145
|
-
status: active
|
|
146
|
-
created_date: ${new Date().toISOString().substring(0, 10)}
|
|
147
|
-
last_updated: ${new Date().toISOString().replace('T', ' ').substring(0, 16)}
|
|
148
|
-
milestone: TBD
|
|
149
|
-
sprint_ids: []
|
|
150
|
-
tasks: []
|
|
151
|
-
assignee: TBD
|
|
152
|
-
tags: [auto-generated]
|
|
153
|
-
---
|
|
154
|
-
|
|
155
|
-
# Feature: Auto Generated Feature
|
|
156
|
-
|
|
157
|
-
## Overview
|
|
158
|
-
이 Feature는 Git 히스토리 스캔 중 자동으로 생성되었습니다.
|
|
159
|
-
커밋 메시지에서 ${featureId} 참조가 발견되었지만 해당 Feature 파일이 없어 생성되었습니다.
|
|
160
|
-
|
|
161
|
-
## Requirements
|
|
162
|
-
- [ ] 실제 요구사항으로 업데이트 필요
|
|
163
|
-
|
|
164
|
-
## Git History
|
|
165
|
-
### Commits
|
|
166
|
-
`;
|
|
167
|
-
|
|
168
|
-
await fs.writeFile(filePath, content, 'utf8');
|
|
169
|
-
spinner.succeed(chalk.green(`✅ ${featureId} 파일 생성 완료`));
|
|
170
|
-
|
|
171
|
-
// missingFeatures에서 제거
|
|
172
|
-
missingFeatures.delete(featureId);
|
|
173
|
-
} catch (error) {
|
|
174
|
-
spinner.fail(chalk.red(`❌ ${featureId} 파일 생성 실패: ${error.message}`));
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Feature 업데이트
|
|
180
|
-
if (!options.dryRun) {
|
|
181
|
-
console.log(chalk.cyan('\n🔄 Feature 파일 업데이트 중...'));
|
|
182
|
-
|
|
183
|
-
let successCount = 0;
|
|
184
|
-
let failCount = 0;
|
|
185
|
-
|
|
186
|
-
for (const [featureId, commits] of Object.entries(featureCommitMap)) {
|
|
187
|
-
if (missingFeatures.has(featureId)) {
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
spinner.start(`${featureId} 업데이트 중 (${commits.length}개 커밋)...`);
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
const { updateFeatureWithCommits } = require('../utils/feature-updater');
|
|
195
|
-
const success = await updateFeatureWithCommits(featureId, commits);
|
|
196
|
-
|
|
197
|
-
if (success) {
|
|
198
|
-
spinner.succeed(chalk.green(`✅ ${featureId} 업데이트 완료`));
|
|
199
|
-
successCount++;
|
|
200
|
-
} else {
|
|
201
|
-
spinner.fail(chalk.red(`❌ ${featureId} 업데이트 실패`));
|
|
202
|
-
failCount++;
|
|
203
|
-
}
|
|
204
|
-
} catch (error) {
|
|
205
|
-
spinner.fail(chalk.red(`❌ ${featureId} 업데이트 중 오류: ${error.message}`));
|
|
206
|
-
failCount++;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
console.log(chalk.cyan('\n✨ 업데이트 완료!'));
|
|
211
|
-
console.log(chalk.green(` 성공: ${successCount}개`));
|
|
212
|
-
if (failCount > 0) {
|
|
213
|
-
console.log(chalk.red(` 실패: ${failCount}개`));
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// 추가 권장사항
|
|
218
|
-
if (missingFeatures.size > 0 && !options.createMissing) {
|
|
219
|
-
console.log(chalk.yellow('\n💡 팁:'));
|
|
220
|
-
console.log(chalk.gray(' --create-missing 옵션을 사용하면 누락된 Feature 파일을 자동으로 생성할 수 있습니다.'));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
} catch (error) {
|
|
224
|
-
spinner.fail(chalk.red('스캔 중 오류 발생'));
|
|
225
|
-
console.error(chalk.red(error.message));
|
|
226
|
-
if (options.verbose) {
|
|
227
|
-
console.error(error.stack);
|
|
228
|
-
}
|
|
229
|
-
process.exit(1);
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
// 명령어 실행
|
|
234
|
-
program.parse(process.argv);
|