aiwf 0.3.15 → 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 +3 -4
- package/README.md +38 -3
- 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 +0 -1
- package/docs/PERFORMANCE_GUIDELINES.ko.md +388 -0
- package/docs/TROUBLESHOOTING.ko.md +366 -0
- package/package.json +50 -2
- package/src/DEPENDENCY_MAP.md +90 -0
- package/src/lib/installer.js +1 -0
- 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/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/guides/feature-git-integration-guide-ko.md +0 -481
- package/docs/guides/feature-git-integration-guide.md +0 -481
- 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
|
@@ -1,154 +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 {
|
|
7
|
-
syncFeatureWithGit,
|
|
8
|
-
syncAllActiveFeatures,
|
|
9
|
-
findFeatureFile
|
|
10
|
-
} = require('../utils/feature-updater');
|
|
11
|
-
const {
|
|
12
|
-
getFeatureRelatedCommits,
|
|
13
|
-
isGitRepository,
|
|
14
|
-
getRecentFeatureCommits
|
|
15
|
-
} = require('../utils/git-utils');
|
|
16
|
-
|
|
17
|
-
const program = new Command();
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* sync_feature_commits 명령어
|
|
21
|
-
* Feature Ledger와 Git 커밋을 동기화
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
program
|
|
25
|
-
.name('sync_feature_commits')
|
|
26
|
-
.description('Feature Ledger와 Git 커밋 히스토리를 동기화합니다')
|
|
27
|
-
.argument('[featureId]', 'Feature ID (예: FL001). 생략 시 모든 활성 Feature 동기화')
|
|
28
|
-
.option('-s, --since <date>', '특정 날짜 이후의 커밋만 동기화 (YYYY-MM-DD)')
|
|
29
|
-
.option('-r, --recent <count>', '최근 N개의 커밋에서 Feature 관련 커밋 찾기', '50')
|
|
30
|
-
.option('-a, --all', '모든 활성 Feature를 동기화')
|
|
31
|
-
.option('-v, --verbose', '상세 로그 출력')
|
|
32
|
-
.action(async (featureId, options) => {
|
|
33
|
-
const spinner = ora();
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
// Git 저장소 확인
|
|
37
|
-
if (!await isGitRepository()) {
|
|
38
|
-
console.error(chalk.red('❌ 현재 디렉토리는 Git 저장소가 아닙니다.'));
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// 특정 Feature ID가 제공된 경우
|
|
43
|
-
if (featureId && !options.all) {
|
|
44
|
-
spinner.start(`${featureId} Feature와 Git 커밋 동기화 중...`);
|
|
45
|
-
|
|
46
|
-
// Feature 파일 존재 확인
|
|
47
|
-
const featureFile = await findFeatureFile(featureId);
|
|
48
|
-
if (!featureFile) {
|
|
49
|
-
spinner.fail(chalk.red(`Feature 파일을 찾을 수 없습니다: ${featureId}`));
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const result = await syncFeatureWithGit(featureId);
|
|
54
|
-
|
|
55
|
-
if (result.success) {
|
|
56
|
-
spinner.succeed(chalk.green(
|
|
57
|
-
`✅ ${featureId} 동기화 완료! (${result.commitCount}개 커밋 연결됨)`
|
|
58
|
-
));
|
|
59
|
-
|
|
60
|
-
if (options.verbose && result.commitCount > 0) {
|
|
61
|
-
console.log(chalk.gray(`\n연결된 커밋 수: ${result.commitCount}`));
|
|
62
|
-
}
|
|
63
|
-
} else {
|
|
64
|
-
spinner.fail(chalk.red(`❌ ${featureId} 동기화 실패`));
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
// 모든 활성 Feature 동기화
|
|
69
|
-
else if (options.all || !featureId) {
|
|
70
|
-
spinner.start('모든 활성 Feature와 Git 커밋 동기화 중...');
|
|
71
|
-
|
|
72
|
-
const results = await syncAllActiveFeatures();
|
|
73
|
-
|
|
74
|
-
spinner.succeed(chalk.green('✅ 동기화 완료!'));
|
|
75
|
-
console.log(chalk.cyan(`\n📊 동기화 결과:`));
|
|
76
|
-
console.log(chalk.white(` 총 Feature: ${results.total}`));
|
|
77
|
-
console.log(chalk.green(` 성공: ${results.synced}`));
|
|
78
|
-
if (results.failed > 0) {
|
|
79
|
-
console.log(chalk.red(` 실패: ${results.failed}`));
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// 최근 커밋에서 Feature 관련 커밋 찾기 (옵션)
|
|
84
|
-
if (options.recent) {
|
|
85
|
-
spinner.start(`최근 ${options.recent}개 커밋에서 Feature 관련 커밋 검색 중...`);
|
|
86
|
-
|
|
87
|
-
const recentCommits = await getRecentFeatureCommits(parseInt(options.recent));
|
|
88
|
-
|
|
89
|
-
if (recentCommits.length > 0) {
|
|
90
|
-
spinner.succeed(chalk.green(`${recentCommits.length}개의 Feature 관련 커밋 발견`));
|
|
91
|
-
|
|
92
|
-
if (options.verbose) {
|
|
93
|
-
console.log(chalk.cyan('\n📝 Feature 관련 커밋:'));
|
|
94
|
-
recentCommits.forEach(commit => {
|
|
95
|
-
const shortHash = commit.hash.substring(0, 7);
|
|
96
|
-
const features = commit.featureIds.join(', ');
|
|
97
|
-
console.log(chalk.gray(` ${shortHash} - ${features}: ${commit.message.split('\n')[0]}`));
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
} else {
|
|
101
|
-
spinner.info(chalk.yellow('최근 커밋에서 Feature 관련 커밋을 찾을 수 없습니다.'));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// 특정 날짜 이후 커밋 검색 (옵션)
|
|
106
|
-
if (options.since) {
|
|
107
|
-
spinner.start(`${options.since} 이후의 Feature 관련 커밋 검색 중...`);
|
|
108
|
-
|
|
109
|
-
const commits = await getFeatureRelatedCommits(options.since);
|
|
110
|
-
|
|
111
|
-
if (commits.length > 0) {
|
|
112
|
-
spinner.succeed(chalk.green(`${commits.length}개의 Feature 관련 커밋 발견`));
|
|
113
|
-
|
|
114
|
-
// Feature별로 그룹화
|
|
115
|
-
const featureGroups = {};
|
|
116
|
-
commits.forEach(commit => {
|
|
117
|
-
commit.featureIds.forEach(featureId => {
|
|
118
|
-
if (!featureGroups[featureId]) {
|
|
119
|
-
featureGroups[featureId] = [];
|
|
120
|
-
}
|
|
121
|
-
featureGroups[featureId].push(commit);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
console.log(chalk.cyan('\n📊 Feature별 커밋 통계:'));
|
|
126
|
-
Object.entries(featureGroups).forEach(([featureId, commits]) => {
|
|
127
|
-
console.log(chalk.white(` ${featureId}: ${commits.length}개 커밋`));
|
|
128
|
-
});
|
|
129
|
-
} else {
|
|
130
|
-
spinner.info(chalk.yellow(`${options.since} 이후 Feature 관련 커밋이 없습니다.`));
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 도움말 메시지
|
|
135
|
-
if (!featureId && !options.all && !options.recent && !options.since) {
|
|
136
|
-
console.log(chalk.cyan('\n💡 사용 예시:'));
|
|
137
|
-
console.log(chalk.gray(' sync_feature_commits FL001 # 특정 Feature 동기화'));
|
|
138
|
-
console.log(chalk.gray(' sync_feature_commits --all # 모든 활성 Feature 동기화'));
|
|
139
|
-
console.log(chalk.gray(' sync_feature_commits --recent 100 # 최근 100개 커밋에서 Feature 찾기'));
|
|
140
|
-
console.log(chalk.gray(' sync_feature_commits --since 2025-01-01 # 특정 날짜 이후 커밋 검색'));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
} catch (error) {
|
|
144
|
-
spinner.fail(chalk.red('동기화 중 오류 발생'));
|
|
145
|
-
console.error(chalk.red(error.message));
|
|
146
|
-
if (options.verbose) {
|
|
147
|
-
console.error(error.stack);
|
|
148
|
-
}
|
|
149
|
-
process.exit(1);
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// 명령어 실행
|
|
154
|
-
program.parse(process.argv);
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
interface Feature {
|
|
2
|
-
id: string
|
|
3
|
-
name: string
|
|
4
|
-
description: string
|
|
5
|
-
status: 'completed' | 'in_progress' | 'planned'
|
|
6
|
-
type: string
|
|
7
|
-
created_at: string
|
|
8
|
-
completed_at?: string
|
|
9
|
-
tags: string[]
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface FeatureLedgerProps {
|
|
13
|
-
features: Feature[]
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export default function FeatureLedger({ features }: FeatureLedgerProps) {
|
|
17
|
-
const getStatusBadge = (status: Feature['status']) => {
|
|
18
|
-
const styles = {
|
|
19
|
-
completed: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
|
20
|
-
in_progress: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
|
21
|
-
planned: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const labels = {
|
|
25
|
-
completed: '완료',
|
|
26
|
-
in_progress: '진행중',
|
|
27
|
-
planned: '계획됨',
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${styles[status]}`}>
|
|
32
|
-
{labels[status]}
|
|
33
|
-
</span>
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<div className="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg">
|
|
39
|
-
<div className="px-4 py-5 sm:px-6">
|
|
40
|
-
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
|
41
|
-
Feature Ledger
|
|
42
|
-
</h3>
|
|
43
|
-
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
44
|
-
프로젝트에서 개발된 모든 기능을 추적합니다.
|
|
45
|
-
</p>
|
|
46
|
-
</div>
|
|
47
|
-
<div className="border-t border-gray-200 dark:border-gray-700">
|
|
48
|
-
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
|
49
|
-
{features.map((feature) => (
|
|
50
|
-
<li key={feature.id} className="px-4 py-4 sm:px-6">
|
|
51
|
-
<div className="flex items-start justify-between">
|
|
52
|
-
<div className="flex-1">
|
|
53
|
-
<div className="flex items-center justify-between">
|
|
54
|
-
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
|
55
|
-
{feature.name}
|
|
56
|
-
</p>
|
|
57
|
-
{getStatusBadge(feature.status)}
|
|
58
|
-
</div>
|
|
59
|
-
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
60
|
-
{feature.description}
|
|
61
|
-
</p>
|
|
62
|
-
<div className="mt-2 flex items-center text-xs text-gray-500 dark:text-gray-400">
|
|
63
|
-
<span>{feature.id}</span>
|
|
64
|
-
<span className="mx-2">•</span>
|
|
65
|
-
<span>{new Date(feature.created_at).toLocaleDateString('ko-KR')}</span>
|
|
66
|
-
{feature.completed_at && (
|
|
67
|
-
<>
|
|
68
|
-
<span className="mx-2">→</span>
|
|
69
|
-
<span>{new Date(feature.completed_at).toLocaleDateString('ko-KR')}</span>
|
|
70
|
-
</>
|
|
71
|
-
)}
|
|
72
|
-
</div>
|
|
73
|
-
{feature.tags.length > 0 && (
|
|
74
|
-
<div className="mt-2 flex flex-wrap gap-1">
|
|
75
|
-
{feature.tags.map((tag) => (
|
|
76
|
-
<span
|
|
77
|
-
key={tag}
|
|
78
|
-
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
|
|
79
|
-
>
|
|
80
|
-
{tag}
|
|
81
|
-
</span>
|
|
82
|
-
))}
|
|
83
|
-
</div>
|
|
84
|
-
)}
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
</li>
|
|
88
|
-
))}
|
|
89
|
-
</ul>
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
)
|
|
93
|
-
}
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
const fs = require('fs').promises;
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const yaml = require('yaml');
|
|
4
|
-
const { getCommitsByFeatureId } = require('./git-utils');
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Feature Ledger 파일 업데이트 메커니즘
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Feature 파일 경로 구성
|
|
12
|
-
* @param {string} featureId - Feature ID (예: 'FL001')
|
|
13
|
-
* @param {string} status - Feature 상태 (active, completed, archived)
|
|
14
|
-
* @returns {string} - Feature 파일 경로
|
|
15
|
-
*/
|
|
16
|
-
function getFeatureFilePath(featureId, status = 'active') {
|
|
17
|
-
const basePath = path.join(process.cwd(), '.aiwf', '06_FEATURE_LEDGERS');
|
|
18
|
-
const fileName = `${featureId}_*.md`;
|
|
19
|
-
return path.join(basePath, status, fileName);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Feature 파일 찾기
|
|
24
|
-
* @param {string} featureId - Feature ID
|
|
25
|
-
* @returns {Promise<{path: string, status: string}|null>} - 파일 경로와 상태
|
|
26
|
-
*/
|
|
27
|
-
async function findFeatureFile(featureId) {
|
|
28
|
-
const basePath = path.join(process.cwd(), '.aiwf', '06_FEATURE_LEDGERS');
|
|
29
|
-
const statuses = ['active', 'completed', 'archived'];
|
|
30
|
-
|
|
31
|
-
for (const status of statuses) {
|
|
32
|
-
const dirPath = path.join(basePath, status);
|
|
33
|
-
try {
|
|
34
|
-
const files = await fs.readdir(dirPath);
|
|
35
|
-
const featureFile = files.find(file => file.startsWith(`${featureId}_`));
|
|
36
|
-
if (featureFile) {
|
|
37
|
-
return {
|
|
38
|
-
path: path.join(dirPath, featureFile),
|
|
39
|
-
status: status
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
} catch (error) {
|
|
43
|
-
// 디렉토리가 없을 수 있음
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Feature 파일 읽기
|
|
53
|
-
* @param {string} filePath - 파일 경로
|
|
54
|
-
* @returns {Promise<{frontmatter: Object, content: string}>}
|
|
55
|
-
*/
|
|
56
|
-
async function readFeatureFile(filePath) {
|
|
57
|
-
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
58
|
-
const lines = fileContent.split('\n');
|
|
59
|
-
|
|
60
|
-
// Frontmatter 파싱
|
|
61
|
-
let frontmatterEnd = -1;
|
|
62
|
-
let inFrontmatter = false;
|
|
63
|
-
for (let i = 0; i < lines.length; i++) {
|
|
64
|
-
if (lines[i] === '---') {
|
|
65
|
-
if (!inFrontmatter) {
|
|
66
|
-
inFrontmatter = true;
|
|
67
|
-
} else {
|
|
68
|
-
frontmatterEnd = i;
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const frontmatterText = lines.slice(1, frontmatterEnd).join('\n');
|
|
75
|
-
const frontmatter = yaml.parse(frontmatterText);
|
|
76
|
-
const content = lines.slice(frontmatterEnd + 1).join('\n');
|
|
77
|
-
|
|
78
|
-
return { frontmatter, content };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Feature 파일 작성
|
|
83
|
-
* @param {string} filePath - 파일 경로
|
|
84
|
-
* @param {Object} frontmatter - Frontmatter 데이터
|
|
85
|
-
* @param {string} content - 파일 내용
|
|
86
|
-
*/
|
|
87
|
-
async function writeFeatureFile(filePath, frontmatter, content) {
|
|
88
|
-
const frontmatterText = yaml.stringify(frontmatter);
|
|
89
|
-
const fileContent = `---\n${frontmatterText}---\n${content}`;
|
|
90
|
-
|
|
91
|
-
// 백업 생성
|
|
92
|
-
try {
|
|
93
|
-
const backupPath = `${filePath}.backup`;
|
|
94
|
-
await fs.copyFile(filePath, backupPath);
|
|
95
|
-
} catch (error) {
|
|
96
|
-
// 백업 실패는 무시
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
await fs.writeFile(filePath, fileContent, 'utf8');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Git 커밋 정보를 Feature 파일에 추가
|
|
104
|
-
* @param {string} featureId - Feature ID
|
|
105
|
-
* @param {Array<Object>} commits - 커밋 정보 배열
|
|
106
|
-
* @returns {Promise<boolean>} - 성공 여부
|
|
107
|
-
*/
|
|
108
|
-
async function updateFeatureWithCommits(featureId, commits) {
|
|
109
|
-
try {
|
|
110
|
-
const featureFile = await findFeatureFile(featureId);
|
|
111
|
-
if (!featureFile) {
|
|
112
|
-
console.error(`Feature file not found for ${featureId}`);
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const { frontmatter, content } = await readFeatureFile(featureFile.path);
|
|
117
|
-
|
|
118
|
-
// Frontmatter 업데이트
|
|
119
|
-
if (!frontmatter.git_commits) {
|
|
120
|
-
frontmatter.git_commits = [];
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// 커밋 객체로 저장 (사양에 맞게)
|
|
124
|
-
const existingHashes = new Set(frontmatter.git_commits.map(c =>
|
|
125
|
-
typeof c === 'string' ? c : c.hash
|
|
126
|
-
));
|
|
127
|
-
|
|
128
|
-
commits.forEach(commit => {
|
|
129
|
-
if (!existingHashes.has(commit.hash)) {
|
|
130
|
-
// 사양에 정의된 구조로 커밋 정보 저장
|
|
131
|
-
frontmatter.git_commits.push({
|
|
132
|
-
hash: commit.hash,
|
|
133
|
-
message: commit.message,
|
|
134
|
-
author: commit.author,
|
|
135
|
-
date: commit.date,
|
|
136
|
-
files_changed: 0, // 추후 git 명령어로 업데이트 가능
|
|
137
|
-
insertions: 0, // 추후 git 명령어로 업데이트 가능
|
|
138
|
-
deletions: 0 // 추후 git 명령어로 업데이트 가능
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// 마지막 업데이트 시간 갱신
|
|
144
|
-
frontmatter.last_updated = new Date().toISOString().replace('T', ' ').substring(0, 16);
|
|
145
|
-
|
|
146
|
-
// Git History 섹션 업데이트
|
|
147
|
-
let updatedContent = content;
|
|
148
|
-
const gitHistorySection = '## Git History';
|
|
149
|
-
const gitHistoryIndex = content.indexOf(gitHistorySection);
|
|
150
|
-
|
|
151
|
-
if (gitHistoryIndex === -1) {
|
|
152
|
-
// Git History 섹션이 없으면 추가
|
|
153
|
-
updatedContent += '\n\n## Git History\n### Commits\n';
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// 커밋 정보 추가
|
|
157
|
-
const commitLines = commits.map(commit => {
|
|
158
|
-
const date = new Date(commit.date).toISOString().substring(0, 10);
|
|
159
|
-
return `- \`${commit.hash.substring(0, 7)}\`: ${commit.message.split('\n')[0]} [${date}]`;
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
if (gitHistoryIndex !== -1) {
|
|
163
|
-
// 기존 섹션에 추가
|
|
164
|
-
const sections = updatedContent.split('##');
|
|
165
|
-
const gitHistorySectionIndex = sections.findIndex(s => s.trim().startsWith('Git History'));
|
|
166
|
-
|
|
167
|
-
if (gitHistorySectionIndex !== -1) {
|
|
168
|
-
const commitsSectionIndex = sections[gitHistorySectionIndex].indexOf('### Commits');
|
|
169
|
-
if (commitsSectionIndex !== -1) {
|
|
170
|
-
sections[gitHistorySectionIndex] += '\n' + commitLines.join('\n');
|
|
171
|
-
}
|
|
172
|
-
updatedContent = sections.join('##');
|
|
173
|
-
}
|
|
174
|
-
} else {
|
|
175
|
-
updatedContent += commitLines.join('\n');
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
await writeFeatureFile(featureFile.path, frontmatter, updatedContent);
|
|
179
|
-
return true;
|
|
180
|
-
} catch (error) {
|
|
181
|
-
console.error(`Error updating feature ${featureId}:`, error);
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Feature와 Git 커밋 동기화
|
|
188
|
-
* @param {string} featureId - Feature ID
|
|
189
|
-
* @returns {Promise<{success: boolean, commitCount: number}>}
|
|
190
|
-
*/
|
|
191
|
-
async function syncFeatureWithGit(featureId) {
|
|
192
|
-
try {
|
|
193
|
-
const commits = await getCommitsByFeatureId(featureId);
|
|
194
|
-
|
|
195
|
-
if (commits.length === 0) {
|
|
196
|
-
return { success: true, commitCount: 0 };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const success = await updateFeatureWithCommits(featureId, commits);
|
|
200
|
-
return { success, commitCount: commits.length };
|
|
201
|
-
} catch (error) {
|
|
202
|
-
console.error(`Error syncing feature ${featureId}:`, error);
|
|
203
|
-
return { success: false, commitCount: 0 };
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* 모든 활성 Feature를 Git과 동기화
|
|
209
|
-
* @returns {Promise<{total: number, synced: number, failed: number}>}
|
|
210
|
-
*/
|
|
211
|
-
async function syncAllActiveFeatures() {
|
|
212
|
-
const basePath = path.join(process.cwd(), '.aiwf', '06_FEATURE_LEDGERS', 'active');
|
|
213
|
-
const results = { total: 0, synced: 0, failed: 0 };
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
const files = await fs.readdir(basePath);
|
|
217
|
-
const featureFiles = files.filter(f => f.endsWith('.md'));
|
|
218
|
-
results.total = featureFiles.length;
|
|
219
|
-
|
|
220
|
-
for (const file of featureFiles) {
|
|
221
|
-
const featureId = file.split('_')[0];
|
|
222
|
-
const { success } = await syncFeatureWithGit(featureId);
|
|
223
|
-
|
|
224
|
-
if (success) {
|
|
225
|
-
results.synced++;
|
|
226
|
-
} else {
|
|
227
|
-
results.failed++;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
} catch (error) {
|
|
231
|
-
console.error('Error syncing all features:', error);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return results;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Feature 진행률 업데이트
|
|
239
|
-
* @param {string} featureId - Feature ID
|
|
240
|
-
* @param {number} percentage - 진행률 (0-100)
|
|
241
|
-
* @returns {Promise<boolean>} - 성공 여부
|
|
242
|
-
*/
|
|
243
|
-
async function updateFeatureProgress(featureId, percentage) {
|
|
244
|
-
try {
|
|
245
|
-
const featureFile = await findFeatureFile(featureId);
|
|
246
|
-
if (!featureFile) {
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const { frontmatter, content } = await readFeatureFile(featureFile.path);
|
|
251
|
-
|
|
252
|
-
frontmatter.progress_percentage = Math.min(100, Math.max(0, percentage));
|
|
253
|
-
frontmatter.last_updated = new Date().toISOString().replace('T', ' ').substring(0, 16);
|
|
254
|
-
|
|
255
|
-
await writeFeatureFile(featureFile.path, frontmatter, content);
|
|
256
|
-
return true;
|
|
257
|
-
} catch (error) {
|
|
258
|
-
console.error(`Error updating progress for ${featureId}:`, error);
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
module.exports = {
|
|
264
|
-
findFeatureFile,
|
|
265
|
-
readFeatureFile,
|
|
266
|
-
writeFeatureFile,
|
|
267
|
-
updateFeatureWithCommits,
|
|
268
|
-
syncFeatureWithGit,
|
|
269
|
-
syncAllActiveFeatures,
|
|
270
|
-
updateFeatureProgress
|
|
271
|
-
};
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { promises as fs } from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { exec, execSync } from 'child_process';
|
|
6
|
-
import { promisify } from 'util';
|
|
7
|
-
import { fileURLToPath } from 'url';
|
|
8
|
-
|
|
9
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const execAsync = promisify(exec);
|
|
11
|
-
|
|
12
|
-
// AIWF 경로 설정
|
|
13
|
-
const AIWF_ROOT = path.join(process.cwd(), '.aiwf');
|
|
14
|
-
const FEATURE_LEDGERS_PATH = path.join(AIWF_ROOT, '06_FEATURE_LEDGERS');
|
|
15
|
-
const ACTIVE_PATH = path.join(FEATURE_LEDGERS_PATH, 'active');
|
|
16
|
-
|
|
17
|
-
// 커밋 메시지에서 Feature ID를 추출하는 정규식 패턴들
|
|
18
|
-
const FEATURE_ID_PATTERNS = [
|
|
19
|
-
/FL\d{3}(?!\d)/, // FL001 (not followed by digit)
|
|
20
|
-
/\[FL\d{3}\]/, // [FL001]
|
|
21
|
-
/\(FL\d{3}\)/, // (FL001)
|
|
22
|
-
/(feat|fix|docs|style|refactor|test|chore)\(FL\d{3}\)/, // feat(FL001)
|
|
23
|
-
/FL\d{3}:/, // FL001:
|
|
24
|
-
/#FL\d{3}/, // #FL001
|
|
25
|
-
/relates to FL\d{3}/i, // relates to FL001
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
// Feature ID 추출 함수
|
|
29
|
-
function extractFeatureId(text) {
|
|
30
|
-
for (const pattern of FEATURE_ID_PATTERNS) {
|
|
31
|
-
const match = text.match(pattern);
|
|
32
|
-
if (match) {
|
|
33
|
-
// 순수 Feature ID만 추출 (FL###)
|
|
34
|
-
const idMatch = match[0].match(/FL\d{3}/);
|
|
35
|
-
if (idMatch) {
|
|
36
|
-
return idMatch[0];
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Feature 파일 경로 가져오기
|
|
44
|
-
async function getFeaturePath(featureId) {
|
|
45
|
-
// active 폴더에서 먼저 확인
|
|
46
|
-
const activePath = path.join(ACTIVE_PATH, `${featureId}_*.md`);
|
|
47
|
-
try {
|
|
48
|
-
const files = await fs.readdir(ACTIVE_PATH);
|
|
49
|
-
const featureFile = files.find(file => file.startsWith(`${featureId}_`));
|
|
50
|
-
if (featureFile) {
|
|
51
|
-
return path.join(ACTIVE_PATH, featureFile);
|
|
52
|
-
}
|
|
53
|
-
} catch (error) {
|
|
54
|
-
// 폴더가 없거나 읽기 실패 시 계속 진행
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// completed, archived 폴더도 확인
|
|
58
|
-
const otherPaths = ['completed', 'archived'];
|
|
59
|
-
for (const folder of otherPaths) {
|
|
60
|
-
const folderPath = path.join(FEATURE_LEDGERS_PATH, folder);
|
|
61
|
-
try {
|
|
62
|
-
const files = await fs.readdir(folderPath);
|
|
63
|
-
const featureFile = files.find(file => file.startsWith(`${featureId}_`));
|
|
64
|
-
if (featureFile) {
|
|
65
|
-
return path.join(folderPath, featureFile);
|
|
66
|
-
}
|
|
67
|
-
} catch (error) {
|
|
68
|
-
// 폴더가 없거나 읽기 실패 시 계속 진행
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Git 커밋 정보 가져오기
|
|
76
|
-
async function getCommitInfo(commitHash) {
|
|
77
|
-
try {
|
|
78
|
-
const { stdout } = await execAsync(`git show --no-patch --format="%H|%h|%an|%ae|%ad|%s" ${commitHash}`);
|
|
79
|
-
const [hash, shortHash, author, email, date, message] = stdout.trim().split('|');
|
|
80
|
-
return {
|
|
81
|
-
hash,
|
|
82
|
-
shortHash,
|
|
83
|
-
author,
|
|
84
|
-
email,
|
|
85
|
-
date: new Date(date).toISOString(),
|
|
86
|
-
message
|
|
87
|
-
};
|
|
88
|
-
} catch (error) {
|
|
89
|
-
console.error(`Error getting commit info for ${commitHash}:`, error.message);
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// YAML frontmatter 파싱
|
|
95
|
-
function parseYamlFrontmatter(content) {
|
|
96
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
97
|
-
if (!match) return null;
|
|
98
|
-
|
|
99
|
-
const yaml = match[1];
|
|
100
|
-
const metadata = {};
|
|
101
|
-
|
|
102
|
-
// 간단한 YAML 파싱 (복잡한 구조는 제외)
|
|
103
|
-
yaml.split('\n').forEach(line => {
|
|
104
|
-
const keyValue = line.match(/^(\w+):\s*(.*)$/);
|
|
105
|
-
if (keyValue) {
|
|
106
|
-
const [_, key, value] = keyValue;
|
|
107
|
-
metadata[key] = value.trim();
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
return metadata;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// YAML frontmatter 업데이트
|
|
115
|
-
function updateYamlFrontmatter(content, metadata) {
|
|
116
|
-
const existingMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
117
|
-
|
|
118
|
-
let yamlLines = [];
|
|
119
|
-
Object.entries(metadata).forEach(([key, value]) => {
|
|
120
|
-
if (value !== undefined && value !== null) {
|
|
121
|
-
yamlLines.push(`${key}: ${value}`);
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const newYaml = `---\n${yamlLines.join('\n')}\n---`;
|
|
126
|
-
|
|
127
|
-
if (existingMatch) {
|
|
128
|
-
return content.replace(/^---\n[\s\S]*?\n---/, newYaml);
|
|
129
|
-
} else {
|
|
130
|
-
return `${newYaml}\n\n${content}`;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Git 로그에서 Feature 관련 커밋 가져오기
|
|
135
|
-
async function getFeatureCommits(featureId, since = null) {
|
|
136
|
-
try {
|
|
137
|
-
let gitCommand = `git log --grep="${featureId}" --format="%H|%h|%an|%ad|%s"`;
|
|
138
|
-
if (since) {
|
|
139
|
-
gitCommand += ` --since="${since}"`;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const { stdout } = await execAsync(gitCommand);
|
|
143
|
-
if (!stdout.trim()) return [];
|
|
144
|
-
|
|
145
|
-
const commits = stdout.trim().split('\n').map(line => {
|
|
146
|
-
const [hash, shortHash, author, date, message] = line.split('|');
|
|
147
|
-
return {
|
|
148
|
-
hash,
|
|
149
|
-
shortHash,
|
|
150
|
-
author,
|
|
151
|
-
date: new Date(date).toISOString(),
|
|
152
|
-
message
|
|
153
|
-
};
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
return commits;
|
|
157
|
-
} catch (error) {
|
|
158
|
-
console.error(`Error getting commits for ${featureId}:`, error.message);
|
|
159
|
-
return [];
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export {
|
|
164
|
-
extractFeatureId,
|
|
165
|
-
getFeaturePath,
|
|
166
|
-
getCommitInfo,
|
|
167
|
-
parseYamlFrontmatter,
|
|
168
|
-
updateYamlFrontmatter,
|
|
169
|
-
getFeatureCommits,
|
|
170
|
-
AIWF_ROOT,
|
|
171
|
-
FEATURE_LEDGERS_PATH,
|
|
172
|
-
ACTIVE_PATH
|
|
173
|
-
};
|