commit-ai-agent 1.0.3 → 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 +10 -3
- package/bin/cli.js +2 -2
- package/package.json +1 -1
- package/public/icon.svg +4 -0
- package/public/index.html +5 -1
- package/src/git.js +46 -41
- package/src/server.js +84 -62
package/.env.example
CHANGED
package/README.md
CHANGED
|
@@ -14,15 +14,22 @@ AI가 git 커밋과 현재 변경사항을 한국어로 자동 분석해주는
|
|
|
14
14
|
|
|
15
15
|
### 방법 A — npx (설치 없이 바로 실행)
|
|
16
16
|
|
|
17
|
+
- 프로젝트가 모여있는 디렉토리에서 아래 명령어 실행
|
|
18
|
+
- 해당 디렉토리에서 .env 파일 생성(.env.example 참고)
|
|
19
|
+
|
|
17
20
|
```bash
|
|
18
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
31
|
npm install -g commit-ai-agent
|
|
25
|
-
commit-
|
|
32
|
+
commit-ai-agent
|
|
26
33
|
```
|
|
27
34
|
|
|
28
35
|
### 방법 C — 직접 클론
|
|
@@ -34,7 +41,7 @@ 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
|
|
package/bin/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
6
|
import dotenv from "dotenv";
|
|
7
7
|
import path from "path";
|
package/package.json
CHANGED
package/public/icon.svg
ADDED
package/public/index.html
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
content="AI 기반 git 커밋 문서화 및 코드 리뷰 에이전트"
|
|
10
10
|
/>
|
|
11
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" />
|
|
12
14
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
13
15
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
14
16
|
<link
|
|
@@ -118,7 +120,9 @@
|
|
|
118
120
|
<div class="result-header">
|
|
119
121
|
<h2 class="card-title">📝 AI 분석 결과</h2>
|
|
120
122
|
<div class="result-actions">
|
|
121
|
-
<button class="btn-ghost" id="copy-btn" style="display:none"
|
|
123
|
+
<button class="btn-ghost" id="copy-btn" style="display: none">
|
|
124
|
+
📋 복사
|
|
125
|
+
</button>
|
|
122
126
|
<span class="report-saved" id="report-saved"></span>
|
|
123
127
|
</div>
|
|
124
128
|
</div>
|
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
|
});
|