commit-ai-agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/server.js ADDED
@@ -0,0 +1,275 @@
1
+ import 'dotenv/config';
2
+ import express from 'express';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import fs from 'fs';
6
+ import { listGitProjects, getLatestCommit, getWorkingStatus } from './git.js';
7
+ import { analyzeCommit, analyzeWorkingStatus } from './analyzer.js';
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const app = express();
11
+
12
+ const PORT = process.env.PORT || 3000;
13
+ const DEV_ROOT = process.env.DEV_ROOT;
14
+
15
+ // npx/global install 시: COMMIT_ANALYZER_ROOT = bin/cli.js가 설정한 패키지 루트
16
+ // 로컬 dev 시: __dirname/../ 사용
17
+ const PACKAGE_ROOT = process.env.COMMIT_ANALYZER_ROOT || path.join(__dirname, '..');
18
+
19
+ // reports는 항상 사용자 현재 디렉토리에 저장
20
+ const REPORTS_DIR = path.join(process.cwd(), 'reports');
21
+
22
+ // 리포트 저장 디렉토리 생성
23
+ if (!fs.existsSync(REPORTS_DIR)) {
24
+ fs.mkdirSync(REPORTS_DIR, { recursive: true });
25
+ }
26
+
27
+ app.use(express.json());
28
+ app.use(express.static(path.join(PACKAGE_ROOT, 'public')));
29
+
30
+ // ──────────────────────────────────────────────
31
+ // PWA 아이콘 (SVG를 PNG MIME으로 서빙)
32
+ // ──────────────────────────────────────────────
33
+ app.get('/api/icon/:size', (req, res) => {
34
+ const svgPath = path.join(__dirname, '..', 'public', 'icon.svg');
35
+ res.setHeader('Content-Type', 'image/svg+xml');
36
+ res.sendFile(svgPath);
37
+ });
38
+
39
+ app.get('/favicon.ico', (req, res) => {
40
+ res.sendFile(path.join(__dirname, '..', 'public', 'icon.svg'));
41
+ });
42
+
43
+
44
+ // ──────────────────────────────────────────────
45
+ // API: 설정 확인
46
+
47
+ // ──────────────────────────────────────────────
48
+ app.get('/api/config', (req, res) => {
49
+ const hasKey = !!(process.env.GEMINI_API_KEY && process.env.GEMINI_API_KEY !== 'your_gemini_api_key_here');
50
+ res.json({ hasKey, devRoot: DEV_ROOT });
51
+ });
52
+
53
+ // ──────────────────────────────────────────────
54
+ // API: 프로젝트 목록
55
+ // ──────────────────────────────────────────────
56
+ app.get('/api/projects', async (req, res) => {
57
+ try {
58
+ const projects = await listGitProjects(DEV_ROOT);
59
+ res.json({ projects });
60
+ } catch (err) {
61
+ res.status(500).json({ error: err.message });
62
+ }
63
+ });
64
+
65
+ // ──────────────────────────────────────────────
66
+ // API: 최근 커밋 정보 조회
67
+ // ──────────────────────────────────────────────
68
+ app.get('/api/projects/:name/commit', async (req, res) => {
69
+ try {
70
+ const projectPath = path.join(DEV_ROOT, req.params.name);
71
+ const commit = await getLatestCommit(projectPath);
72
+ res.json({ commit });
73
+ } catch (err) {
74
+ res.status(500).json({ error: err.message });
75
+ }
76
+ });
77
+
78
+ // ──────────────────────────────────────────────
79
+ // API: 현재 git status 조회
80
+ // ──────────────────────────────────────────────
81
+ app.get('/api/projects/:name/status', async (req, res) => {
82
+ try {
83
+ const projectPath = path.join(DEV_ROOT, req.params.name);
84
+ const status = await getWorkingStatus(projectPath);
85
+ res.json({ status });
86
+ } catch (err) {
87
+ res.status(500).json({ error: err.message });
88
+ }
89
+ });
90
+
91
+ // ──────────────────────────────────────────────
92
+ // API: AI 분석 실행 (SSE 스트리밍)
93
+ // ──────────────────────────────────────────────
94
+ app.post('/api/analyze', async (req, res) => {
95
+ const { projectName } = req.body;
96
+ const apiKey = process.env.GEMINI_API_KEY;
97
+
98
+ if (!apiKey || apiKey === 'your_gemini_api_key_here') {
99
+ return res.status(400).json({ error: 'GEMINI_API_KEY가 .env 파일에 설정되지 않았습니다.' });
100
+ }
101
+
102
+ if (!projectName) {
103
+ return res.status(400).json({ error: '프로젝트명이 필요합니다.' });
104
+ }
105
+
106
+ // Server-Sent Events 설정
107
+ res.setHeader('Content-Type', 'text/event-stream');
108
+ res.setHeader('Cache-Control', 'no-cache');
109
+ res.setHeader('Connection', 'keep-alive');
110
+ res.flushHeaders();
111
+
112
+ const send = (data) => {
113
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
114
+ };
115
+
116
+ try {
117
+ send({ type: 'status', message: '커밋 정보를 가져오는 중...' });
118
+ const projectPath = path.join(DEV_ROOT, projectName);
119
+ const commit = await getLatestCommit(projectPath);
120
+
121
+ send({ type: 'commit', commit });
122
+ send({ type: 'status', message: 'AI 분석 중... (30초~1분 소요)' });
123
+
124
+ const analysis = await analyzeCommit(commit, projectName, apiKey);
125
+
126
+ // 리포트 저장
127
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
128
+ const reportFilename = `${projectName}-${timestamp}.md`;
129
+ const reportPath = path.join(REPORTS_DIR, reportFilename);
130
+ const fullReport = buildMarkdownReport(projectName, commit, analysis);
131
+ fs.writeFileSync(reportPath, fullReport, 'utf-8');
132
+
133
+ send({ type: 'analysis', analysis, reportFilename });
134
+ send({ type: 'done' });
135
+ res.end();
136
+ } catch (err) {
137
+ send({ type: 'error', message: err.message });
138
+ res.end();
139
+ }
140
+ });
141
+
142
+ // ──────────────────────────────────────────────
143
+ // API: git status 변경사항 AI 분석 (SSE 스트리밍)
144
+ // ──────────────────────────────────────────────
145
+ app.post('/api/analyze-status', async (req, res) => {
146
+ const { projectName } = req.body;
147
+ const apiKey = process.env.GEMINI_API_KEY;
148
+
149
+ if (!apiKey || apiKey === 'your_gemini_api_key_here') {
150
+ return res.status(400).json({ error: 'GEMINI_API_KEY가 .env 파일에 설정되지 않았습니다.' });
151
+ }
152
+
153
+ if (!projectName) {
154
+ return res.status(400).json({ error: '프로젝트명이 필요합니다.' });
155
+ }
156
+
157
+ res.setHeader('Content-Type', 'text/event-stream');
158
+ res.setHeader('Cache-Control', 'no-cache');
159
+ res.setHeader('Connection', 'keep-alive');
160
+ res.flushHeaders();
161
+
162
+ const send = (data) => {
163
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
164
+ };
165
+
166
+ try {
167
+ send({ type: 'status', message: '변경사항을 가져오는 중...' });
168
+ const projectPath = path.join(DEV_ROOT, projectName);
169
+ const workingStatus = await getWorkingStatus(projectPath);
170
+
171
+ if (!workingStatus) {
172
+ send({ type: 'error', message: '현재 변경사항이 없습니다. 코드를 수정한 뒤 다시 시도해 주세요.' });
173
+ return res.end();
174
+ }
175
+
176
+ send({ type: 'working-status', workingStatus });
177
+ send({ type: 'status', message: 'AI 분석 중... (30초~1분 소요)' });
178
+
179
+ const analysis = await analyzeWorkingStatus(workingStatus, projectName, apiKey);
180
+
181
+ // 리포트 저장
182
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
183
+ const reportFilename = `${projectName}-status-${timestamp}.md`;
184
+ const reportPath = path.join(REPORTS_DIR, reportFilename);
185
+ const fullReport = buildStatusReport(projectName, workingStatus, analysis);
186
+ fs.writeFileSync(reportPath, fullReport, 'utf-8');
187
+
188
+ send({ type: 'analysis', analysis, reportFilename });
189
+ send({ type: 'done' });
190
+ res.end();
191
+ } catch (err) {
192
+ send({ type: 'error', message: err.message });
193
+ res.end();
194
+ }
195
+ });
196
+
197
+ // ──────────────────────────────────────────────
198
+ // API: 저장된 리포트 목록
199
+ // ──────────────────────────────────────────────
200
+ app.get('/api/reports', (req, res) => {
201
+ try {
202
+ const files = fs.readdirSync(REPORTS_DIR)
203
+ .filter(f => f.endsWith('.md'))
204
+ .sort()
205
+ .reverse()
206
+ .slice(0, 20); // 최근 20개
207
+ res.json({ reports: files });
208
+ } catch {
209
+ res.json({ reports: [] });
210
+ }
211
+ });
212
+
213
+ // ──────────────────────────────────────────────
214
+ // API: 특정 리포트 읽기
215
+ // ──────────────────────────────────────────────
216
+ app.get('/api/reports/:filename', (req, res) => {
217
+ try {
218
+ const filePath = path.join(REPORTS_DIR, req.params.filename);
219
+ if (!fs.existsSync(filePath)) {
220
+ return res.status(404).json({ error: '리포트를 찾을 수 없습니다.' });
221
+ }
222
+ const content = fs.readFileSync(filePath, 'utf-8');
223
+ res.json({ content });
224
+ } catch (err) {
225
+ res.status(500).json({ error: err.message });
226
+ }
227
+ });
228
+
229
+ function buildMarkdownReport(projectName, commit, analysis) {
230
+ return `# 커밋 분석 리포트: ${projectName}
231
+
232
+ > 생성 시각: ${new Date().toLocaleString('ko-KR')}
233
+
234
+ ## 커밋 정보
235
+ | 항목 | 내용 |
236
+ |---|---|
237
+ | 해시 | \`${commit.shortHash}\` |
238
+ | 메시지 | ${commit.message} |
239
+ | 작성자 | ${commit.author} |
240
+ | 날짜 | ${commit.date} |
241
+
242
+ ---
243
+
244
+ ${analysis}
245
+ `;
246
+ }
247
+
248
+ function buildStatusReport(projectName, status, analysis) {
249
+ return `# 작업 중 변경사항 분석: ${projectName}
250
+
251
+ > 생성 시각: ${new Date().toLocaleString('ko-KR')}
252
+
253
+ ## 변경사항 요약
254
+ | 항목 | 수량 |
255
+ |---|---|
256
+ | Staged | ${status.stagedCount}개 |
257
+ | Modified (unstaged) | ${status.modifiedCount}개 |
258
+ | Deleted | ${status.deletedCount}개 |
259
+ | Untracked (신규) | ${status.untrackedCount}개 |
260
+
261
+ \`\`\`
262
+ ${status.statusText}
263
+ \`\`\`
264
+
265
+ ---
266
+
267
+ ${analysis}
268
+ `;
269
+ }
270
+
271
+ app.listen(PORT, () => {
272
+ console.log(`\n🚀 Commit Analyzer 실행 중`);
273
+ console.log(` 브라우저: http://localhost:${PORT}`);
274
+ console.log(` 분석 대상: ${DEV_ROOT}\n`);
275
+ });