commit-ai-agent 1.0.2 → 1.0.4
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/.env.example +1 -1
- package/README.md +82 -8
- package/bin/cli.js +20 -16
- package/package.json +2 -2
- package/public/app.js +2 -0
- package/public/icon.svg +4 -0
- package/public/index.html +141 -110
- package/src/git.js +46 -41
- package/src/server.js +84 -62
package/.env.example
CHANGED
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# ⚡ Commit
|
|
1
|
+
# ⚡ Commit AI Agent
|
|
2
2
|
|
|
3
3
|
AI가 git 커밋과 현재 변경사항을 한국어로 자동 분석해주는 개발자 도구입니다.
|
|
4
4
|
|
|
@@ -14,27 +14,34 @@ AI가 git 커밋과 현재 변경사항을 한국어로 자동 분석해주는
|
|
|
14
14
|
|
|
15
15
|
### 방법 A — npx (설치 없이 바로 실행)
|
|
16
16
|
|
|
17
|
+
- 프로젝트가 모여있는 디렉토리에서 아래 명령어 실행
|
|
18
|
+
- 해당 디렉토리에서 .env 파일 생성(.env.example 참고)
|
|
19
|
+
|
|
17
20
|
```bash
|
|
18
|
-
npx commit-
|
|
21
|
+
npx commit-ai-agent
|
|
19
22
|
```
|
|
20
23
|
|
|
21
24
|
### 방법 B — 전역 설치 후 명령어로 실행
|
|
22
25
|
|
|
26
|
+
- 전역으로 설치하면 어느 위치에서든 `commit-ai-agent` 명령어로 실행 가능
|
|
27
|
+
- 프로젝트가 모여있는 디렉토리에서 명령어 실행
|
|
28
|
+
- 해당 디렉토리에서 .env 파일 생성(.env.example 참고)
|
|
29
|
+
|
|
23
30
|
```bash
|
|
24
|
-
npm install -g commit-
|
|
25
|
-
commit-
|
|
31
|
+
npm install -g commit-ai-agent
|
|
32
|
+
commit-ai-agent
|
|
26
33
|
```
|
|
27
34
|
|
|
28
35
|
### 방법 C — 직접 클론
|
|
29
36
|
|
|
30
37
|
```bash
|
|
31
|
-
git clone https://github.com/cjy3458/commit-
|
|
32
|
-
cd commit-
|
|
38
|
+
git clone https://github.com/cjy3458/commit-ai-agent.git
|
|
39
|
+
cd commit-ai-agent
|
|
33
40
|
npm install
|
|
34
41
|
npm start
|
|
35
42
|
```
|
|
36
43
|
|
|
37
|
-
npm start 대신 Windows 사용자는 `start.bat
|
|
44
|
+
npm start 대신 Windows 사용자는 `start.bat`파일을 더블클릭하여 바로 실행할 수 있습니다.
|
|
38
45
|
|
|
39
46
|
---
|
|
40
47
|
|
|
@@ -44,7 +51,7 @@ npm start 대신 Windows 사용자는 `start.bat`를 더블클릭하여 바로
|
|
|
44
51
|
|
|
45
52
|
```env
|
|
46
53
|
GEMINI_API_KEY=여기에_API_키_입력
|
|
47
|
-
DEV_ROOT=C:/Users
|
|
54
|
+
DEV_ROOT=C:/Users/projects => 절대경로 입력
|
|
48
55
|
PORT=3000
|
|
49
56
|
```
|
|
50
57
|
|
|
@@ -84,6 +91,73 @@ PORT=3000
|
|
|
84
91
|
|
|
85
92
|
---
|
|
86
93
|
|
|
94
|
+
## 버그 제보 & 기능 제안
|
|
95
|
+
|
|
96
|
+
[GitHub Issues](https://github.com/cjy3458/commit-ai-agent/issues)를 통해 자유롭게 제보해 주세요.
|
|
97
|
+
|
|
98
|
+
**버그 제보 시 포함하면 좋은 정보:**
|
|
99
|
+
|
|
100
|
+
- OS / Node.js 버전
|
|
101
|
+
- 실행 방법 (npx / 전역 설치 / 직접 클론)
|
|
102
|
+
- 오류 메시지 전문 (터미널 출력)
|
|
103
|
+
- 재현 방법
|
|
104
|
+
|
|
105
|
+
**기능 제안 시:**
|
|
106
|
+
|
|
107
|
+
- 제안 배경 (어떤 문제를 해결하고 싶은지)
|
|
108
|
+
- 원하는 동작 방식
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 기여하기
|
|
113
|
+
|
|
114
|
+
PR은 언제나 환영합니다.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# 1. 저장소 포크 후 클론
|
|
118
|
+
git clone https://github.com/cjy3458/commit-ai-agent.git
|
|
119
|
+
cd commit-ai-agent
|
|
120
|
+
|
|
121
|
+
# 2. 의존성 설치
|
|
122
|
+
npm install
|
|
123
|
+
|
|
124
|
+
# 3. 환경 설정
|
|
125
|
+
cp .env.example .env
|
|
126
|
+
# .env에 GEMINI_API_KEY, DEV_ROOT 입력
|
|
127
|
+
|
|
128
|
+
# 4. 개발 서버 실행 (파일 변경 시 자동 재시작)
|
|
129
|
+
npm run dev
|
|
130
|
+
|
|
131
|
+
# 5. 브랜치 생성 → 작업 → PR
|
|
132
|
+
git checkout -b feat/my-feature
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 프로젝트 구조
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
commit-analyzer/
|
|
141
|
+
├── bin/
|
|
142
|
+
│ └── cli.js # CLI 진입점 (npx / npm install -g)
|
|
143
|
+
├── src/
|
|
144
|
+
│ ├── server.js # Express 서버 · API 라우트 · SSE 스트리밍
|
|
145
|
+
│ ├── analyzer.js # Gemini AI 분석 프롬프트 · 재시도 로직
|
|
146
|
+
│ └── git.js # simple-git 래퍼 (커밋 조회, status diff)
|
|
147
|
+
├── public/ # 프론트엔드 정적 파일 (HTML · CSS · JS)
|
|
148
|
+
├── .env.example # 환경변수 예시
|
|
149
|
+
└── package.json
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
| 파일 | 역할 |
|
|
153
|
+
| ----------------- | ----------------------------------------------------- |
|
|
154
|
+
| `bin/cli.js` | npx 실행 시 .env 로드, 브라우저 자동 오픈, 서버 시작 |
|
|
155
|
+
| `src/server.js` | REST API + SSE 엔드포인트, 리포트 저장 |
|
|
156
|
+
| `src/analyzer.js` | Gemini API 호출, 모델 폴백(2.5→2.0→lite), 지수 백오프 |
|
|
157
|
+
| `src/git.js` | 프로젝트 목록 탐색, 커밋 diff, working status diff |
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
87
161
|
## 라이선스
|
|
88
162
|
|
|
89
163
|
[MIT](./LICENSE)
|
package/bin/cli.js
CHANGED
|
@@ -1,36 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* commit-
|
|
4
|
-
* npx commit-
|
|
3
|
+
* commit-ai-agent CLI 진입점
|
|
4
|
+
* npx commit-ai-agent 또는 npm install -g 후 commit-ai-agent 명령으로 실행
|
|
5
5
|
*/
|
|
6
|
-
import dotenv from
|
|
7
|
-
import path from
|
|
8
|
-
import { fileURLToPath } from
|
|
9
|
-
import { spawn } from
|
|
6
|
+
import dotenv from "dotenv";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import { spawn } from "child_process";
|
|
10
10
|
|
|
11
11
|
// 사용자 현재 디렉토리의 .env 로드
|
|
12
|
-
dotenv.config({ path: path.resolve(process.cwd(),
|
|
12
|
+
dotenv.config({ path: path.resolve(process.cwd(), ".env") });
|
|
13
13
|
|
|
14
14
|
// 패키지 루트 경로를 환경변수로 전달 (server.js가 public/ 위치를 찾기 위함)
|
|
15
15
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
process.env.COMMIT_ANALYZER_ROOT = path.resolve(__dirname,
|
|
16
|
+
process.env.COMMIT_ANALYZER_ROOT = path.resolve(__dirname, "..");
|
|
17
17
|
|
|
18
18
|
const PORT = process.env.PORT || 3000;
|
|
19
19
|
|
|
20
|
-
console.log(
|
|
21
|
-
console.log(
|
|
20
|
+
console.log("");
|
|
21
|
+
console.log(" ⚡ Commit AI Agent 실행 중...");
|
|
22
22
|
console.log(` 🌐 http://localhost:${PORT}`);
|
|
23
|
-
console.log(
|
|
24
|
-
console.log(
|
|
23
|
+
console.log(" 종료: Ctrl+C");
|
|
24
|
+
console.log("");
|
|
25
25
|
|
|
26
26
|
// 브라우저 자동 오픈 (1초 지연 - 서버 준비 대기)
|
|
27
27
|
setTimeout(async () => {
|
|
28
28
|
const url = `http://localhost:${PORT}`;
|
|
29
29
|
const platform = process.platform;
|
|
30
|
-
const cmd =
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const cmd =
|
|
31
|
+
platform === "win32"
|
|
32
|
+
? "start"
|
|
33
|
+
: platform === "darwin"
|
|
34
|
+
? "open"
|
|
35
|
+
: "xdg-open";
|
|
36
|
+
spawn(cmd, [url], { shell: true, stdio: "ignore", detached: true });
|
|
33
37
|
}, 1200);
|
|
34
38
|
|
|
35
39
|
// 서버 시작
|
|
36
|
-
await import(
|
|
40
|
+
await import("../src/server.js");
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "commit-ai-agent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "AI-powered git commit & working status analyzer with web UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"commit-
|
|
8
|
+
"commit-ai-agent": "./bin/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"bin/",
|
package/public/app.js
CHANGED
|
@@ -201,6 +201,7 @@ async function startAnalysis(endpoint) {
|
|
|
201
201
|
resultCard.style.display = 'block';
|
|
202
202
|
analysisBody.innerHTML = '';
|
|
203
203
|
reportSaved.textContent = '';
|
|
204
|
+
document.getElementById('copy-btn').style.display = 'none'; // 분석 시작 시 숨김
|
|
204
205
|
setStatus('loading', '분석 준비 중...');
|
|
205
206
|
analyzeBtn.disabled = true;
|
|
206
207
|
btnIcon.textContent = '⏳';
|
|
@@ -251,6 +252,7 @@ async function startAnalysis(endpoint) {
|
|
|
251
252
|
}
|
|
252
253
|
} else if (data.type === 'done') {
|
|
253
254
|
setStatus('done', '✅ 분석 완료!');
|
|
255
|
+
document.getElementById('copy-btn').style.display = 'inline-flex'; // 완료 시에만 표시
|
|
254
256
|
} else if (data.type === 'error') {
|
|
255
257
|
setStatus('error', `오류: ${data.message}`);
|
|
256
258
|
}
|
package/public/icon.svg
ADDED
package/public/index.html
CHANGED
|
@@ -1,128 +1,159 @@
|
|
|
1
|
-
<!
|
|
1
|
+
<!doctype html>
|
|
2
2
|
<html lang="ko">
|
|
3
|
-
<head>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<!-- ── Main ── -->
|
|
32
|
-
<main class="main">
|
|
33
|
-
|
|
34
|
-
<!-- ── Tab: Analyze ── -->
|
|
35
|
-
<section class="tab-content active" id="tab-analyze">
|
|
36
|
-
|
|
37
|
-
<!-- API Key Warning -->
|
|
38
|
-
<div class="alert alert-warn" id="api-key-warn" style="display:none">
|
|
39
|
-
⚠️ <strong>.env</strong> 파일에 <code>GEMINI_API_KEY</code>가 설정되지 않았습니다.
|
|
40
|
-
<code>c:\dev\commit-analyzer\.env</code> 파일을 생성하고 키를 입력해 주세요.
|
|
41
|
-
</div>
|
|
42
|
-
|
|
43
|
-
<!-- Project Selector Card -->
|
|
44
|
-
<div class="card selector-card">
|
|
45
|
-
<div class="card-header">
|
|
46
|
-
<h2 class="card-title">🗂 프로젝트 선택</h2>
|
|
47
|
-
<button class="refresh-btn" id="refresh-projects" title="새로고침">↻</button>
|
|
48
|
-
</div>
|
|
49
|
-
<!-- Mode Toggle -->
|
|
50
|
-
<div class="mode-toggle">
|
|
51
|
-
<button class="mode-btn active" id="mode-commit" data-mode="commit">📦 최근 커밋 분석</button>
|
|
52
|
-
<button class="mode-btn" id="mode-status" data-mode="status">🔍 현재 변경사항 분석</button>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Commit Ai agent — AI 커밋 분석기</title>
|
|
7
|
+
<meta
|
|
8
|
+
name="description"
|
|
9
|
+
content="AI 기반 git 커밋 문서화 및 코드 리뷰 에이전트"
|
|
10
|
+
/>
|
|
11
|
+
<meta name="theme-color" content="#6366f1" />
|
|
12
|
+
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
|
13
|
+
<link rel="shortcut icon" href="/favicon.ico" />
|
|
14
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
15
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
16
|
+
<link
|
|
17
|
+
href="https://fonts.googleapis.com/css2?family=Pretendard:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
|
|
18
|
+
rel="stylesheet"
|
|
19
|
+
/>
|
|
20
|
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
21
|
+
<link rel="stylesheet" href="/style.css" />
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
<!-- ── Header ── -->
|
|
25
|
+
<header class="header">
|
|
26
|
+
<div class="header-inner">
|
|
27
|
+
<div class="logo">
|
|
28
|
+
<span class="logo-icon">⚡</span>
|
|
29
|
+
<span class="logo-text">Commit Ai agent</span>
|
|
30
|
+
<span class="logo-badge">AI</span>
|
|
53
31
|
</div>
|
|
54
|
-
<
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
<div class="card-footer">
|
|
60
|
-
<span class="selected-hint" id="selected-hint">프로젝트를 선택해 주세요</span>
|
|
61
|
-
<button class="btn-primary" id="analyze-btn" disabled>
|
|
62
|
-
<span class="btn-icon">🔍</span> 커밋 분석하기
|
|
32
|
+
<nav class="header-nav">
|
|
33
|
+
<button class="nav-btn active" data-tab="analyze" id="btn-analyze">
|
|
34
|
+
분석하기
|
|
63
35
|
</button>
|
|
64
|
-
|
|
36
|
+
<button class="nav-btn" data-tab="reports" id="btn-reports">
|
|
37
|
+
리포트
|
|
38
|
+
</button>
|
|
39
|
+
</nav>
|
|
65
40
|
</div>
|
|
41
|
+
</header>
|
|
66
42
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
<
|
|
43
|
+
<!-- ── Main ── -->
|
|
44
|
+
<main class="main">
|
|
45
|
+
<!-- ── Tab: Analyze ── -->
|
|
46
|
+
<section class="tab-content active" id="tab-analyze">
|
|
47
|
+
<!-- API Key Warning -->
|
|
48
|
+
<div class="alert alert-warn" id="api-key-warn" style="display: none">
|
|
49
|
+
⚠️ <strong>.env</strong> 파일에 <code>GEMINI_API_KEY</code>가 설정되지
|
|
50
|
+
않았습니다. <code>c:\dev\commit-analyzer\.env</code> 파일을 생성하고
|
|
51
|
+
키를 입력해 주세요.
|
|
74
52
|
</div>
|
|
75
|
-
</div>
|
|
76
53
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
54
|
+
<!-- Project Selector Card -->
|
|
55
|
+
<div class="card selector-card">
|
|
56
|
+
<div class="card-header">
|
|
57
|
+
<h2 class="card-title">🗂 프로젝트 선택</h2>
|
|
58
|
+
<button class="refresh-btn" id="refresh-projects" title="새로고침">
|
|
59
|
+
↻
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
<!-- Mode Toggle -->
|
|
63
|
+
<div class="mode-toggle">
|
|
64
|
+
<button class="mode-btn active" id="mode-commit" data-mode="commit">
|
|
65
|
+
📦 최근 커밋 분석
|
|
66
|
+
</button>
|
|
67
|
+
<button class="mode-btn" id="mode-status" data-mode="status">
|
|
68
|
+
🔍 현재 변경사항 분석
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="project-grid" id="project-grid">
|
|
72
|
+
<div class="skeleton-grid">
|
|
73
|
+
<div class="skeleton"></div>
|
|
74
|
+
<div class="skeleton"></div>
|
|
75
|
+
<div class="skeleton"></div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="card-footer">
|
|
79
|
+
<span class="selected-hint" id="selected-hint"
|
|
80
|
+
>프로젝트를 선택해 주세요</span
|
|
81
|
+
>
|
|
82
|
+
<button class="btn-primary" id="analyze-btn" disabled>
|
|
83
|
+
<span class="btn-icon">🔍</span> 커밋 분석하기
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
84
86
|
</div>
|
|
85
|
-
</div>
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<
|
|
91
|
-
<div class="
|
|
92
|
-
<button class="btn-ghost" id="
|
|
93
|
-
|
|
88
|
+
<!-- Working Status Card (hidden until project selected in status mode) -->
|
|
89
|
+
<div class="card commit-card" id="status-card" style="display: none">
|
|
90
|
+
<h2 class="card-title">🔧 현재 변경사항 (미커밋)</h2>
|
|
91
|
+
<div class="commit-meta" id="status-meta"></div>
|
|
92
|
+
<div class="diff-toggle">
|
|
93
|
+
<button class="btn-ghost" id="status-diff-toggle-btn">
|
|
94
|
+
diff 보기 ▾
|
|
95
|
+
</button>
|
|
96
|
+
<pre
|
|
97
|
+
class="diff-content"
|
|
98
|
+
id="status-diff-content"
|
|
99
|
+
style="display: none"
|
|
100
|
+
></pre>
|
|
94
101
|
</div>
|
|
95
102
|
</div>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
<
|
|
103
|
+
|
|
104
|
+
<!-- Commit Info Card (hidden until project selected) -->
|
|
105
|
+
<div class="card commit-card" id="commit-card" style="display: none">
|
|
106
|
+
<h2 class="card-title">📦 최근 커밋</h2>
|
|
107
|
+
<div class="commit-meta" id="commit-meta"></div>
|
|
108
|
+
<div class="diff-toggle">
|
|
109
|
+
<button class="btn-ghost" id="diff-toggle-btn">diff 보기 ▾</button>
|
|
110
|
+
<pre
|
|
111
|
+
class="diff-content"
|
|
112
|
+
id="diff-content"
|
|
113
|
+
style="display: none"
|
|
114
|
+
></pre>
|
|
115
|
+
</div>
|
|
100
116
|
</div>
|
|
101
|
-
<!-- Streaming output -->
|
|
102
|
-
<div class="analysis-body" id="analysis-body"></div>
|
|
103
|
-
</div>
|
|
104
117
|
|
|
105
|
-
|
|
118
|
+
<!-- Analysis Output -->
|
|
119
|
+
<div class="card result-card" id="result-card" style="display: none">
|
|
120
|
+
<div class="result-header">
|
|
121
|
+
<h2 class="card-title">📝 AI 분석 결과</h2>
|
|
122
|
+
<div class="result-actions">
|
|
123
|
+
<button class="btn-ghost" id="copy-btn" style="display: none">
|
|
124
|
+
📋 복사
|
|
125
|
+
</button>
|
|
126
|
+
<span class="report-saved" id="report-saved"></span>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<!-- Status bar -->
|
|
130
|
+
<div class="status-bar" id="status-bar">
|
|
131
|
+
<span class="status-dot loading"></span>
|
|
132
|
+
<span id="status-msg">분석 준비 중...</span>
|
|
133
|
+
</div>
|
|
134
|
+
<!-- Streaming output -->
|
|
135
|
+
<div class="analysis-body" id="analysis-body"></div>
|
|
136
|
+
</div>
|
|
137
|
+
</section>
|
|
106
138
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
139
|
+
<!-- ── Tab: Reports ── -->
|
|
140
|
+
<section class="tab-content" id="tab-reports">
|
|
141
|
+
<div class="card">
|
|
142
|
+
<h2 class="card-title">📁 저장된 리포트</h2>
|
|
143
|
+
<div id="reports-list" class="reports-list">
|
|
144
|
+
<p class="empty-state">아직 저장된 리포트가 없습니다.</p>
|
|
145
|
+
</div>
|
|
113
146
|
</div>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
147
|
+
<div class="card" id="report-viewer" style="display: none">
|
|
148
|
+
<div class="result-header">
|
|
149
|
+
<h2 class="card-title" id="report-viewer-title">리포트</h2>
|
|
150
|
+
<button class="btn-ghost" id="close-report-btn">✕ 닫기</button>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="analysis-body" id="report-viewer-body"></div>
|
|
119
153
|
</div>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
</section>
|
|
123
|
-
|
|
124
|
-
</main>
|
|
154
|
+
</section>
|
|
155
|
+
</main>
|
|
125
156
|
|
|
126
|
-
|
|
127
|
-
</body>
|
|
157
|
+
<script src="/app.js"></script>
|
|
158
|
+
</body>
|
|
128
159
|
</html>
|
package/src/git.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import simpleGit from
|
|
2
|
-
import path from
|
|
3
|
-
import fs from
|
|
1
|
+
import simpleGit from "simple-git";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* DEV_ROOT 하위의 git 프로젝트 목록을 반환합니다.
|
|
7
7
|
*/
|
|
8
8
|
export async function listGitProjects(devRoot) {
|
|
9
9
|
const entries = fs.readdirSync(devRoot, { withFileTypes: true });
|
|
@@ -12,7 +12,7 @@ export async function listGitProjects(devRoot) {
|
|
|
12
12
|
for (const entry of entries) {
|
|
13
13
|
if (!entry.isDirectory()) continue;
|
|
14
14
|
const fullPath = path.join(devRoot, entry.name);
|
|
15
|
-
const gitDir = path.join(fullPath,
|
|
15
|
+
const gitDir = path.join(fullPath, ".git");
|
|
16
16
|
if (fs.existsSync(gitDir)) {
|
|
17
17
|
projects.push({ name: entry.name, path: fullPath });
|
|
18
18
|
}
|
|
@@ -30,38 +30,38 @@ export async function getLatestCommit(projectPath) {
|
|
|
30
30
|
// 최신 커밋 메타데이터
|
|
31
31
|
const log = await git.log({ maxCount: 1 });
|
|
32
32
|
if (!log.latest) {
|
|
33
|
-
throw new Error(
|
|
33
|
+
throw new Error("커밋 기록이 없습니다.");
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const { hash, message, author_name, author_email, date } = log.latest;
|
|
37
37
|
|
|
38
38
|
// 이전 커밋과의 diff (파일 목록)
|
|
39
|
-
let diffStat =
|
|
40
|
-
let diffContent =
|
|
39
|
+
let diffStat = "";
|
|
40
|
+
let diffContent = "";
|
|
41
41
|
|
|
42
42
|
try {
|
|
43
43
|
// 부모 커밋이 있는지 확인
|
|
44
|
-
const parentCount = await git.raw([
|
|
44
|
+
const parentCount = await git.raw(["rev-list", "--count", "HEAD"]);
|
|
45
45
|
const count = parseInt(parentCount.trim(), 10);
|
|
46
46
|
|
|
47
47
|
if (count > 1) {
|
|
48
|
-
diffStat = await git.raw([
|
|
48
|
+
diffStat = await git.raw(["diff", "--stat", "HEAD~1", "HEAD"]);
|
|
49
49
|
// diff 내용은 너무 클 수 있으므로 최대 300줄 제한
|
|
50
|
-
const rawDiff = await git.raw([
|
|
51
|
-
const lines = rawDiff.split(
|
|
52
|
-
diffContent = lines.slice(0, 300).join(
|
|
50
|
+
const rawDiff = await git.raw(["diff", "HEAD~1", "HEAD"]);
|
|
51
|
+
const lines = rawDiff.split("\n");
|
|
52
|
+
diffContent = lines.slice(0, 300).join("\n");
|
|
53
53
|
if (lines.length > 300) {
|
|
54
|
-
diffContent +=
|
|
54
|
+
diffContent += "\n... (이하 생략, 너무 긴 diff)";
|
|
55
55
|
}
|
|
56
56
|
} else {
|
|
57
57
|
// 첫 번째 커밋인 경우
|
|
58
|
-
diffStat = await git.raw([
|
|
59
|
-
const rawShow = await git.raw([
|
|
60
|
-
const lines = rawShow.split(
|
|
61
|
-
diffContent = lines.slice(0, 300).join(
|
|
58
|
+
diffStat = await git.raw(["show", "--stat", "HEAD"]);
|
|
59
|
+
const rawShow = await git.raw(["show", "HEAD"]);
|
|
60
|
+
const lines = rawShow.split("\n");
|
|
61
|
+
diffContent = lines.slice(0, 300).join("\n");
|
|
62
62
|
}
|
|
63
63
|
} catch (e) {
|
|
64
|
-
diffContent =
|
|
64
|
+
diffContent = "(diff를 가져올 수 없습니다)";
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
return {
|
|
@@ -70,7 +70,7 @@ export async function getLatestCommit(projectPath) {
|
|
|
70
70
|
message,
|
|
71
71
|
author: author_name,
|
|
72
72
|
email: author_email,
|
|
73
|
-
date: new Date(date).toLocaleString(
|
|
73
|
+
date: new Date(date).toLocaleString("ko-KR"),
|
|
74
74
|
diffStat,
|
|
75
75
|
diffContent,
|
|
76
76
|
};
|
|
@@ -85,7 +85,8 @@ export async function getWorkingStatus(projectPath) {
|
|
|
85
85
|
|
|
86
86
|
// git status --short 로 파일 목록
|
|
87
87
|
const statusSummary = await git.status();
|
|
88
|
-
const { files, staged, modified, not_added, deleted, renamed } =
|
|
88
|
+
const { files, staged, modified, not_added, deleted, renamed } =
|
|
89
|
+
statusSummary;
|
|
89
90
|
|
|
90
91
|
if (files.length === 0) {
|
|
91
92
|
return null; // 변경사항 없음
|
|
@@ -96,44 +97,48 @@ export async function getWorkingStatus(projectPath) {
|
|
|
96
97
|
for (const f of files) {
|
|
97
98
|
statusLines.push(`${f.index}${f.working_dir} ${f.path}`);
|
|
98
99
|
}
|
|
99
|
-
const statusText = statusLines.join(
|
|
100
|
+
const statusText = statusLines.join("\n");
|
|
100
101
|
|
|
101
102
|
// staged diff (git diff --cached)
|
|
102
|
-
let stagedDiff =
|
|
103
|
+
let stagedDiff = "";
|
|
103
104
|
try {
|
|
104
|
-
const raw = await git.raw([
|
|
105
|
-
const lines = raw.split(
|
|
106
|
-
stagedDiff = lines.slice(0, 200).join(
|
|
107
|
-
if (lines.length > 200) stagedDiff +=
|
|
105
|
+
const raw = await git.raw(["diff", "--cached"]);
|
|
106
|
+
const lines = raw.split("\n");
|
|
107
|
+
stagedDiff = lines.slice(0, 200).join("\n");
|
|
108
|
+
if (lines.length > 200) stagedDiff += "\n... (이하 생략)";
|
|
108
109
|
} catch {}
|
|
109
110
|
|
|
110
111
|
// unstaged diff (git diff)
|
|
111
|
-
let unstagedDiff =
|
|
112
|
+
let unstagedDiff = "";
|
|
112
113
|
try {
|
|
113
|
-
const raw = await git.raw([
|
|
114
|
-
const lines = raw.split(
|
|
115
|
-
unstagedDiff = lines.slice(0, 200).join(
|
|
116
|
-
if (lines.length > 200) unstagedDiff +=
|
|
114
|
+
const raw = await git.raw(["diff"]);
|
|
115
|
+
const lines = raw.split("\n");
|
|
116
|
+
unstagedDiff = lines.slice(0, 200).join("\n");
|
|
117
|
+
if (lines.length > 200) unstagedDiff += "\n... (이하 생략)";
|
|
117
118
|
} catch {}
|
|
118
119
|
|
|
119
120
|
// untracked 파일 내용 (최대 3개)
|
|
120
|
-
let untrackedContent =
|
|
121
|
-
const untrackedFiles = files
|
|
121
|
+
let untrackedContent = "";
|
|
122
|
+
const untrackedFiles = files
|
|
123
|
+
.filter((f) => f.index === "?" && f.working_dir === "?")
|
|
124
|
+
.slice(0, 3);
|
|
122
125
|
for (const f of untrackedFiles) {
|
|
123
126
|
try {
|
|
124
127
|
const fullPath = path.join(projectPath, f.path);
|
|
125
|
-
const content = fs.readFileSync(fullPath,
|
|
126
|
-
const lines = content.split(
|
|
128
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
129
|
+
const lines = content.split("\n").slice(0, 80).join("\n");
|
|
127
130
|
untrackedContent += `\n--- ${f.path} (신규 파일) ---\n${lines}\n`;
|
|
128
131
|
} catch {}
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
const hasStagedChanges = staged.length > 0 || renamed.length > 0;
|
|
132
135
|
const diffContent = [
|
|
133
|
-
stagedDiff ? `# Staged Changes (git diff --cached)\n${stagedDiff}` :
|
|
134
|
-
unstagedDiff ? `# Unstaged Changes (git diff)\n${unstagedDiff}` :
|
|
135
|
-
untrackedContent ? `# New Files\n${untrackedContent}` :
|
|
136
|
-
]
|
|
136
|
+
stagedDiff ? `# Staged Changes (git diff --cached)\n${stagedDiff}` : "",
|
|
137
|
+
unstagedDiff ? `# Unstaged Changes (git diff)\n${unstagedDiff}` : "",
|
|
138
|
+
untrackedContent ? `# New Files\n${untrackedContent}` : "",
|
|
139
|
+
]
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
.join("\n\n");
|
|
137
142
|
|
|
138
143
|
return {
|
|
139
144
|
statusText,
|
|
@@ -143,7 +148,7 @@ export async function getWorkingStatus(projectPath) {
|
|
|
143
148
|
untrackedCount: not_added.length,
|
|
144
149
|
totalFiles: files.length,
|
|
145
150
|
hasStagedChanges,
|
|
146
|
-
diffContent: diffContent ||
|
|
151
|
+
diffContent: diffContent || "(diff 내용 없음)",
|
|
147
152
|
diffStat: statusText,
|
|
148
153
|
};
|
|
149
154
|
}
|
package/src/server.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import express from
|
|
3
|
-
import path from
|
|
4
|
-
import { fileURLToPath } from
|
|
5
|
-
import fs from
|
|
6
|
-
import { listGitProjects, getLatestCommit, getWorkingStatus } from
|
|
7
|
-
import { analyzeCommit, analyzeWorkingStatus } from
|
|
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
8
|
|
|
9
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const app = express();
|
|
@@ -14,10 +14,11 @@ const DEV_ROOT = process.env.DEV_ROOT;
|
|
|
14
14
|
|
|
15
15
|
// npx/global install 시: COMMIT_ANALYZER_ROOT = bin/cli.js가 설정한 패키지 루트
|
|
16
16
|
// 로컬 dev 시: __dirname/../ 사용
|
|
17
|
-
const PACKAGE_ROOT =
|
|
17
|
+
const PACKAGE_ROOT =
|
|
18
|
+
process.env.COMMIT_ANALYZER_ROOT || path.join(__dirname, "..");
|
|
18
19
|
|
|
19
20
|
// reports는 항상 사용자 현재 디렉토리에 저장
|
|
20
|
-
const REPORTS_DIR = path.join(process.cwd(),
|
|
21
|
+
const REPORTS_DIR = path.join(process.cwd(), "reports");
|
|
21
22
|
|
|
22
23
|
// 리포트 저장 디렉토리 생성
|
|
23
24
|
if (!fs.existsSync(REPORTS_DIR)) {
|
|
@@ -25,35 +26,37 @@ if (!fs.existsSync(REPORTS_DIR)) {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
app.use(express.json());
|
|
28
|
-
app.use(express.static(path.join(PACKAGE_ROOT,
|
|
29
|
+
app.use(express.static(path.join(PACKAGE_ROOT, "public")));
|
|
29
30
|
|
|
30
31
|
// ──────────────────────────────────────────────
|
|
31
32
|
// PWA 아이콘 (SVG를 PNG MIME으로 서빙)
|
|
32
33
|
// ──────────────────────────────────────────────
|
|
33
|
-
app.get(
|
|
34
|
-
const svgPath = path.join(__dirname,
|
|
35
|
-
res.setHeader(
|
|
34
|
+
app.get("/api/icon/:size", (req, res) => {
|
|
35
|
+
const svgPath = path.join(__dirname, "..", "public", "icon.svg");
|
|
36
|
+
res.setHeader("Content-Type", "image/svg+xml");
|
|
36
37
|
res.sendFile(svgPath);
|
|
37
38
|
});
|
|
38
39
|
|
|
39
|
-
app.get(
|
|
40
|
-
res.sendFile(path.join(__dirname,
|
|
40
|
+
app.get("/favicon.ico", (req, res) => {
|
|
41
|
+
res.sendFile(path.join(__dirname, "..", "public", "icon.svg"));
|
|
41
42
|
});
|
|
42
43
|
|
|
43
|
-
|
|
44
44
|
// ──────────────────────────────────────────────
|
|
45
45
|
// API: 설정 확인
|
|
46
46
|
|
|
47
47
|
// ──────────────────────────────────────────────
|
|
48
|
-
app.get(
|
|
49
|
-
const hasKey = !!(
|
|
48
|
+
app.get("/api/config", (req, res) => {
|
|
49
|
+
const hasKey = !!(
|
|
50
|
+
process.env.GEMINI_API_KEY &&
|
|
51
|
+
process.env.GEMINI_API_KEY !== "your_gemini_api_key_here"
|
|
52
|
+
);
|
|
50
53
|
res.json({ hasKey, devRoot: DEV_ROOT });
|
|
51
54
|
});
|
|
52
55
|
|
|
53
56
|
// ──────────────────────────────────────────────
|
|
54
57
|
// API: 프로젝트 목록
|
|
55
58
|
// ──────────────────────────────────────────────
|
|
56
|
-
app.get(
|
|
59
|
+
app.get("/api/projects", async (req, res) => {
|
|
57
60
|
try {
|
|
58
61
|
const projects = await listGitProjects(DEV_ROOT);
|
|
59
62
|
res.json({ projects });
|
|
@@ -65,7 +68,7 @@ app.get('/api/projects', async (req, res) => {
|
|
|
65
68
|
// ──────────────────────────────────────────────
|
|
66
69
|
// API: 최근 커밋 정보 조회
|
|
67
70
|
// ──────────────────────────────────────────────
|
|
68
|
-
app.get(
|
|
71
|
+
app.get("/api/projects/:name/commit", async (req, res) => {
|
|
69
72
|
try {
|
|
70
73
|
const projectPath = path.join(DEV_ROOT, req.params.name);
|
|
71
74
|
const commit = await getLatestCommit(projectPath);
|
|
@@ -78,7 +81,7 @@ app.get('/api/projects/:name/commit', async (req, res) => {
|
|
|
78
81
|
// ──────────────────────────────────────────────
|
|
79
82
|
// API: 현재 git status 조회
|
|
80
83
|
// ──────────────────────────────────────────────
|
|
81
|
-
app.get(
|
|
84
|
+
app.get("/api/projects/:name/status", async (req, res) => {
|
|
82
85
|
try {
|
|
83
86
|
const projectPath = path.join(DEV_ROOT, req.params.name);
|
|
84
87
|
const status = await getWorkingStatus(projectPath);
|
|
@@ -91,22 +94,24 @@ app.get('/api/projects/:name/status', async (req, res) => {
|
|
|
91
94
|
// ──────────────────────────────────────────────
|
|
92
95
|
// API: AI 분석 실행 (SSE 스트리밍)
|
|
93
96
|
// ──────────────────────────────────────────────
|
|
94
|
-
app.post(
|
|
97
|
+
app.post("/api/analyze", async (req, res) => {
|
|
95
98
|
const { projectName } = req.body;
|
|
96
99
|
const apiKey = process.env.GEMINI_API_KEY;
|
|
97
100
|
|
|
98
|
-
if (!apiKey || apiKey ===
|
|
99
|
-
return res
|
|
101
|
+
if (!apiKey || apiKey === "your_gemini_api_key_here") {
|
|
102
|
+
return res
|
|
103
|
+
.status(400)
|
|
104
|
+
.json({ error: "GEMINI_API_KEY가 .env 파일에 설정되지 않았습니다." });
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
if (!projectName) {
|
|
103
|
-
return res.status(400).json({ error:
|
|
108
|
+
return res.status(400).json({ error: "프로젝트명이 필요합니다." });
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
// Server-Sent Events 설정
|
|
107
|
-
res.setHeader(
|
|
108
|
-
res.setHeader(
|
|
109
|
-
res.setHeader(
|
|
112
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
113
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
114
|
+
res.setHeader("Connection", "keep-alive");
|
|
110
115
|
res.flushHeaders();
|
|
111
116
|
|
|
112
117
|
const send = (data) => {
|
|
@@ -114,27 +119,30 @@ app.post('/api/analyze', async (req, res) => {
|
|
|
114
119
|
};
|
|
115
120
|
|
|
116
121
|
try {
|
|
117
|
-
send({ type:
|
|
122
|
+
send({ type: "status", message: "커밋 정보를 가져오는 중..." });
|
|
118
123
|
const projectPath = path.join(DEV_ROOT, projectName);
|
|
119
124
|
const commit = await getLatestCommit(projectPath);
|
|
120
125
|
|
|
121
|
-
send({ type:
|
|
122
|
-
send({ type:
|
|
126
|
+
send({ type: "commit", commit });
|
|
127
|
+
send({ type: "status", message: "AI 분석 중... (30초~1분 소요)" });
|
|
123
128
|
|
|
124
129
|
const analysis = await analyzeCommit(commit, projectName, apiKey);
|
|
125
130
|
|
|
126
131
|
// 리포트 저장
|
|
127
|
-
const timestamp = new Date()
|
|
132
|
+
const timestamp = new Date()
|
|
133
|
+
.toISOString()
|
|
134
|
+
.replace(/[:.]/g, "-")
|
|
135
|
+
.slice(0, 19);
|
|
128
136
|
const reportFilename = `${projectName}-${timestamp}.md`;
|
|
129
137
|
const reportPath = path.join(REPORTS_DIR, reportFilename);
|
|
130
138
|
const fullReport = buildMarkdownReport(projectName, commit, analysis);
|
|
131
|
-
fs.writeFileSync(reportPath, fullReport,
|
|
139
|
+
fs.writeFileSync(reportPath, fullReport, "utf-8");
|
|
132
140
|
|
|
133
|
-
send({ type:
|
|
134
|
-
send({ type:
|
|
141
|
+
send({ type: "analysis", analysis, reportFilename });
|
|
142
|
+
send({ type: "done" });
|
|
135
143
|
res.end();
|
|
136
144
|
} catch (err) {
|
|
137
|
-
send({ type:
|
|
145
|
+
send({ type: "error", message: err.message });
|
|
138
146
|
res.end();
|
|
139
147
|
}
|
|
140
148
|
});
|
|
@@ -142,21 +150,23 @@ app.post('/api/analyze', async (req, res) => {
|
|
|
142
150
|
// ──────────────────────────────────────────────
|
|
143
151
|
// API: git status 변경사항 AI 분석 (SSE 스트리밍)
|
|
144
152
|
// ──────────────────────────────────────────────
|
|
145
|
-
app.post(
|
|
153
|
+
app.post("/api/analyze-status", async (req, res) => {
|
|
146
154
|
const { projectName } = req.body;
|
|
147
155
|
const apiKey = process.env.GEMINI_API_KEY;
|
|
148
156
|
|
|
149
|
-
if (!apiKey || apiKey ===
|
|
150
|
-
return res
|
|
157
|
+
if (!apiKey || apiKey === "your_gemini_api_key_here") {
|
|
158
|
+
return res
|
|
159
|
+
.status(400)
|
|
160
|
+
.json({ error: "GEMINI_API_KEY가 .env 파일에 설정되지 않았습니다." });
|
|
151
161
|
}
|
|
152
162
|
|
|
153
163
|
if (!projectName) {
|
|
154
|
-
return res.status(400).json({ error:
|
|
164
|
+
return res.status(400).json({ error: "프로젝트명이 필요합니다." });
|
|
155
165
|
}
|
|
156
166
|
|
|
157
|
-
res.setHeader(
|
|
158
|
-
res.setHeader(
|
|
159
|
-
res.setHeader(
|
|
167
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
168
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
169
|
+
res.setHeader("Connection", "keep-alive");
|
|
160
170
|
res.flushHeaders();
|
|
161
171
|
|
|
162
172
|
const send = (data) => {
|
|
@@ -164,32 +174,43 @@ app.post('/api/analyze-status', async (req, res) => {
|
|
|
164
174
|
};
|
|
165
175
|
|
|
166
176
|
try {
|
|
167
|
-
send({ type:
|
|
177
|
+
send({ type: "status", message: "변경사항을 가져오는 중..." });
|
|
168
178
|
const projectPath = path.join(DEV_ROOT, projectName);
|
|
169
179
|
const workingStatus = await getWorkingStatus(projectPath);
|
|
170
180
|
|
|
171
181
|
if (!workingStatus) {
|
|
172
|
-
send({
|
|
182
|
+
send({
|
|
183
|
+
type: "error",
|
|
184
|
+
message:
|
|
185
|
+
"현재 변경사항이 없습니다. 코드를 수정한 뒤 다시 시도해 주세요.",
|
|
186
|
+
});
|
|
173
187
|
return res.end();
|
|
174
188
|
}
|
|
175
189
|
|
|
176
|
-
send({ type:
|
|
177
|
-
send({ type:
|
|
190
|
+
send({ type: "working-status", workingStatus });
|
|
191
|
+
send({ type: "status", message: "AI 분석 중... (30초~1분 소요)" });
|
|
178
192
|
|
|
179
|
-
const analysis = await analyzeWorkingStatus(
|
|
193
|
+
const analysis = await analyzeWorkingStatus(
|
|
194
|
+
workingStatus,
|
|
195
|
+
projectName,
|
|
196
|
+
apiKey,
|
|
197
|
+
);
|
|
180
198
|
|
|
181
199
|
// 리포트 저장
|
|
182
|
-
const timestamp = new Date()
|
|
200
|
+
const timestamp = new Date()
|
|
201
|
+
.toISOString()
|
|
202
|
+
.replace(/[:.]/g, "-")
|
|
203
|
+
.slice(0, 19);
|
|
183
204
|
const reportFilename = `${projectName}-status-${timestamp}.md`;
|
|
184
205
|
const reportPath = path.join(REPORTS_DIR, reportFilename);
|
|
185
206
|
const fullReport = buildStatusReport(projectName, workingStatus, analysis);
|
|
186
|
-
fs.writeFileSync(reportPath, fullReport,
|
|
207
|
+
fs.writeFileSync(reportPath, fullReport, "utf-8");
|
|
187
208
|
|
|
188
|
-
send({ type:
|
|
189
|
-
send({ type:
|
|
209
|
+
send({ type: "analysis", analysis, reportFilename });
|
|
210
|
+
send({ type: "done" });
|
|
190
211
|
res.end();
|
|
191
212
|
} catch (err) {
|
|
192
|
-
send({ type:
|
|
213
|
+
send({ type: "error", message: err.message });
|
|
193
214
|
res.end();
|
|
194
215
|
}
|
|
195
216
|
});
|
|
@@ -197,10 +218,11 @@ app.post('/api/analyze-status', async (req, res) => {
|
|
|
197
218
|
// ──────────────────────────────────────────────
|
|
198
219
|
// API: 저장된 리포트 목록
|
|
199
220
|
// ──────────────────────────────────────────────
|
|
200
|
-
app.get(
|
|
221
|
+
app.get("/api/reports", (req, res) => {
|
|
201
222
|
try {
|
|
202
|
-
const files = fs
|
|
203
|
-
.
|
|
223
|
+
const files = fs
|
|
224
|
+
.readdirSync(REPORTS_DIR)
|
|
225
|
+
.filter((f) => f.endsWith(".md"))
|
|
204
226
|
.sort()
|
|
205
227
|
.reverse()
|
|
206
228
|
.slice(0, 20); // 최근 20개
|
|
@@ -213,13 +235,13 @@ app.get('/api/reports', (req, res) => {
|
|
|
213
235
|
// ──────────────────────────────────────────────
|
|
214
236
|
// API: 특정 리포트 읽기
|
|
215
237
|
// ──────────────────────────────────────────────
|
|
216
|
-
app.get(
|
|
238
|
+
app.get("/api/reports/:filename", (req, res) => {
|
|
217
239
|
try {
|
|
218
240
|
const filePath = path.join(REPORTS_DIR, req.params.filename);
|
|
219
241
|
if (!fs.existsSync(filePath)) {
|
|
220
|
-
return res.status(404).json({ error:
|
|
242
|
+
return res.status(404).json({ error: "리포트를 찾을 수 없습니다." });
|
|
221
243
|
}
|
|
222
|
-
const content = fs.readFileSync(filePath,
|
|
244
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
223
245
|
res.json({ content });
|
|
224
246
|
} catch (err) {
|
|
225
247
|
res.status(500).json({ error: err.message });
|
|
@@ -229,7 +251,7 @@ app.get('/api/reports/:filename', (req, res) => {
|
|
|
229
251
|
function buildMarkdownReport(projectName, commit, analysis) {
|
|
230
252
|
return `# 커밋 분석 리포트: ${projectName}
|
|
231
253
|
|
|
232
|
-
> 생성 시각: ${new Date().toLocaleString(
|
|
254
|
+
> 생성 시각: ${new Date().toLocaleString("ko-KR")}
|
|
233
255
|
|
|
234
256
|
## 커밋 정보
|
|
235
257
|
| 항목 | 내용 |
|
|
@@ -248,7 +270,7 @@ ${analysis}
|
|
|
248
270
|
function buildStatusReport(projectName, status, analysis) {
|
|
249
271
|
return `# 작업 중 변경사항 분석: ${projectName}
|
|
250
272
|
|
|
251
|
-
> 생성 시각: ${new Date().toLocaleString(
|
|
273
|
+
> 생성 시각: ${new Date().toLocaleString("ko-KR")}
|
|
252
274
|
|
|
253
275
|
## 변경사항 요약
|
|
254
276
|
| 항목 | 수량 |
|
|
@@ -269,7 +291,7 @@ ${analysis}
|
|
|
269
291
|
}
|
|
270
292
|
|
|
271
293
|
app.listen(PORT, () => {
|
|
272
|
-
console.log(`\n🚀 Commit
|
|
294
|
+
console.log(`\n🚀 Commit Ai Agent 실행 중`);
|
|
273
295
|
console.log(` 브라우저: http://localhost:${PORT}`);
|
|
274
296
|
console.log(` 분석 대상: ${DEV_ROOT}\n`);
|
|
275
297
|
});
|