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 CHANGED
@@ -1,3 +1,3 @@
1
1
  GEMINI_API_KEY=your_gemini_api_key_here
2
2
  PORT=3000
3
- DEV_ROOT=C:/dev
3
+ DEV_ROOT=C:/dev // 절대경로 입력
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # ⚡ Commit Analyzer
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-analyzer
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-analyzer
25
- commit-analyzer
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-analyzer.git
32
- cd commit-analyzer
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/이름/dev
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-analyzer CLI 진입점
4
- * npx commit-analyzer 또는 npm install -g 후 commit-analyzer 명령으로 실행
3
+ * commit-ai-agent CLI 진입점
4
+ * npx commit-ai-agent 또는 npm install -g 후 commit-ai-agent 명령으로 실행
5
5
  */
6
- import dotenv from 'dotenv';
7
- import path from 'path';
8
- import { fileURLToPath } from 'url';
9
- import { spawn } from 'child_process';
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(), '.env') });
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(' ⚡ Commit Analyzer');
20
+ console.log("");
21
+ console.log(" ⚡ Commit AI Agent 실행 중...");
22
22
  console.log(` 🌐 http://localhost:${PORT}`);
23
- console.log(' 종료: Ctrl+C');
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 = platform === 'win32' ? 'start' :
31
- platform === 'darwin' ? 'open' : 'xdg-open';
32
- spawn(cmd, [url], { shell: true, stdio: 'ignore', detached: true });
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('../src/server.js');
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.2",
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-analyzer": "./bin/cli.js"
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
  }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <rect width="32" height="32" rx="7" fill="#6366f1"/>
3
+ <path d="M20 2 L8 17 L15 17 L12 30 L24 15 L17 15 Z" fill="white"/>
4
+ </svg>
package/public/index.html CHANGED
@@ -1,128 +1,159 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="ko">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Commit Analyzer — AI 커밋 분석기</title>
7
- <meta name="description" content="AI 기반 git 커밋 문서화 및 코드 리뷰 에이전트" />
8
- <meta name="theme-color" content="#6366f1" />
9
- <link rel="preconnect" href="https://fonts.googleapis.com" />
10
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
- <link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
12
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
13
- <link rel="stylesheet" href="/style.css" />
14
- </head>
15
- <body>
16
- <!-- ── Header ── -->
17
- <header class="header">
18
- <div class="header-inner">
19
- <div class="logo">
20
- <span class="logo-icon">⚡</span>
21
- <span class="logo-text">Commit Analyzer</span>
22
- <span class="logo-badge">AI</span>
23
- </div>
24
- <nav class="header-nav">
25
- <button class="nav-btn active" data-tab="analyze" id="btn-analyze">분석하기</button>
26
- <button class="nav-btn" data-tab="reports" id="btn-reports">리포트</button>
27
- </nav>
28
- </div>
29
- </header>
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
- <div class="project-grid" id="project-grid">
55
- <div class="skeleton-grid">
56
- <div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div>
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
- </div>
36
+ <button class="nav-btn" data-tab="reports" id="btn-reports">
37
+ 리포트
38
+ </button>
39
+ </nav>
65
40
  </div>
41
+ </header>
66
42
 
67
- <!-- Working Status Card (hidden until project selected in status mode) -->
68
- <div class="card commit-card" id="status-card" style="display:none">
69
- <h2 class="card-title">🔧 현재 변경사항 (미커밋)</h2>
70
- <div class="commit-meta" id="status-meta"></div>
71
- <div class="diff-toggle">
72
- <button class="btn-ghost" id="status-diff-toggle-btn">diff 보기 ▾</button>
73
- <pre class="diff-content" id="status-diff-content" style="display:none"></pre>
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
- <!-- Commit Info Card (hidden until project selected) -->
78
- <div class="card commit-card" id="commit-card" style="display:none">
79
- <h2 class="card-title">📦 최근 커밋</h2>
80
- <div class="commit-meta" id="commit-meta"></div>
81
- <div class="diff-toggle">
82
- <button class="btn-ghost" id="diff-toggle-btn">diff 보기 ▾</button>
83
- <pre class="diff-content" id="diff-content" style="display:none"></pre>
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
- <!-- Analysis Output -->
88
- <div class="card result-card" id="result-card" style="display:none">
89
- <div class="result-header">
90
- <h2 class="card-title">📝 AI 분석 결과</h2>
91
- <div class="result-actions">
92
- <button class="btn-ghost" id="copy-btn">📋 복사</button>
93
- <span class="report-saved" id="report-saved"></span>
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
- <!-- Status bar -->
97
- <div class="status-bar" id="status-bar">
98
- <span class="status-dot loading"></span>
99
- <span id="status-msg">분석 준비 중...</span>
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
- </section>
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
- <!-- ── Tab: Reports ── -->
108
- <section class="tab-content" id="tab-reports">
109
- <div class="card">
110
- <h2 class="card-title">📁 저장된 리포트</h2>
111
- <div id="reports-list" class="reports-list">
112
- <p class="empty-state">아직 저장된 리포트가 없습니다.</p>
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
- </div>
115
- <div class="card" id="report-viewer" style="display:none">
116
- <div class="result-header">
117
- <h2 class="card-title" id="report-viewer-title">리포트</h2>
118
- <button class="btn-ghost" id="close-report-btn">✕ 닫기</button>
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
- <div class="analysis-body" id="report-viewer-body"></div>
121
- </div>
122
- </section>
123
-
124
- </main>
154
+ </section>
155
+ </main>
125
156
 
126
- <script src="/app.js"></script>
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 'simple-git';
2
- import path from 'path';
3
- import fs from 'fs';
1
+ import simpleGit from "simple-git";
2
+ import path from "path";
3
+ import fs from "fs";
4
4
 
5
5
  /**
6
- * c:\dev 하위의 git 프로젝트 목록을 반환합니다.
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, '.git');
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(['rev-list', '--count', 'HEAD']);
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(['diff', '--stat', 'HEAD~1', 'HEAD']);
48
+ diffStat = await git.raw(["diff", "--stat", "HEAD~1", "HEAD"]);
49
49
  // diff 내용은 너무 클 수 있으므로 최대 300줄 제한
50
- const rawDiff = await git.raw(['diff', 'HEAD~1', 'HEAD']);
51
- const lines = rawDiff.split('\n');
52
- diffContent = lines.slice(0, 300).join('\n');
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 += '\n... (이하 생략, 너무 긴 diff)';
54
+ diffContent += "\n... (이하 생략, 너무 긴 diff)";
55
55
  }
56
56
  } else {
57
57
  // 첫 번째 커밋인 경우
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');
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 = '(diff를 가져올 수 없습니다)';
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('ko-KR'),
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 } = statusSummary;
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('\n');
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(['diff', '--cached']);
105
- const lines = raw.split('\n');
106
- stagedDiff = lines.slice(0, 200).join('\n');
107
- if (lines.length > 200) stagedDiff += '\n... (이하 생략)';
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(['diff']);
114
- const lines = raw.split('\n');
115
- unstagedDiff = lines.slice(0, 200).join('\n');
116
- if (lines.length > 200) unstagedDiff += '\n... (이하 생략)';
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.filter(f => f.index === '?' && f.working_dir === '?').slice(0, 3);
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, 'utf-8');
126
- const lines = content.split('\n').slice(0, 80).join('\n');
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
- ].filter(Boolean).join('\n\n');
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 || '(diff 내용 없음)',
151
+ diffContent: diffContent || "(diff 내용 없음)",
147
152
  diffStat: statusText,
148
153
  };
149
154
  }
package/src/server.js CHANGED
@@ -1,10 +1,10 @@
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';
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 = process.env.COMMIT_ANALYZER_ROOT || path.join(__dirname, '..');
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(), 'reports');
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, 'public')));
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('/api/icon/:size', (req, res) => {
34
- const svgPath = path.join(__dirname, '..', 'public', 'icon.svg');
35
- res.setHeader('Content-Type', 'image/svg+xml');
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('/favicon.ico', (req, res) => {
40
- res.sendFile(path.join(__dirname, '..', 'public', 'icon.svg'));
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('/api/config', (req, res) => {
49
- const hasKey = !!(process.env.GEMINI_API_KEY && process.env.GEMINI_API_KEY !== 'your_gemini_api_key_here');
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('/api/projects', async (req, res) => {
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('/api/projects/:name/commit', async (req, res) => {
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('/api/projects/:name/status', async (req, res) => {
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('/api/analyze', async (req, res) => {
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 === 'your_gemini_api_key_here') {
99
- return res.status(400).json({ error: 'GEMINI_API_KEY가 .env 파일에 설정되지 않았습니다.' });
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('Content-Type', 'text/event-stream');
108
- res.setHeader('Cache-Control', 'no-cache');
109
- res.setHeader('Connection', 'keep-alive');
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: 'status', message: '커밋 정보를 가져오는 중...' });
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: 'commit', commit });
122
- send({ type: 'status', message: 'AI 분석 중... (30초~1분 소요)' });
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().toISOString().replace(/[:.]/g, '-').slice(0, 19);
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, 'utf-8');
139
+ fs.writeFileSync(reportPath, fullReport, "utf-8");
132
140
 
133
- send({ type: 'analysis', analysis, reportFilename });
134
- send({ type: 'done' });
141
+ send({ type: "analysis", analysis, reportFilename });
142
+ send({ type: "done" });
135
143
  res.end();
136
144
  } catch (err) {
137
- send({ type: 'error', message: err.message });
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('/api/analyze-status', async (req, res) => {
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 === 'your_gemini_api_key_here') {
150
- return res.status(400).json({ error: 'GEMINI_API_KEY가 .env 파일에 설정되지 않았습니다.' });
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('Content-Type', 'text/event-stream');
158
- res.setHeader('Cache-Control', 'no-cache');
159
- res.setHeader('Connection', 'keep-alive');
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: 'status', message: '변경사항을 가져오는 중...' });
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({ type: 'error', message: '현재 변경사항이 없습니다. 코드를 수정한 뒤 다시 시도해 주세요.' });
182
+ send({
183
+ type: "error",
184
+ message:
185
+ "현재 변경사항이 없습니다. 코드를 수정한 뒤 다시 시도해 주세요.",
186
+ });
173
187
  return res.end();
174
188
  }
175
189
 
176
- send({ type: 'working-status', workingStatus });
177
- send({ type: 'status', message: 'AI 분석 중... (30초~1분 소요)' });
190
+ send({ type: "working-status", workingStatus });
191
+ send({ type: "status", message: "AI 분석 중... (30초~1분 소요)" });
178
192
 
179
- const analysis = await analyzeWorkingStatus(workingStatus, projectName, apiKey);
193
+ const analysis = await analyzeWorkingStatus(
194
+ workingStatus,
195
+ projectName,
196
+ apiKey,
197
+ );
180
198
 
181
199
  // 리포트 저장
182
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
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, 'utf-8');
207
+ fs.writeFileSync(reportPath, fullReport, "utf-8");
187
208
 
188
- send({ type: 'analysis', analysis, reportFilename });
189
- send({ type: 'done' });
209
+ send({ type: "analysis", analysis, reportFilename });
210
+ send({ type: "done" });
190
211
  res.end();
191
212
  } catch (err) {
192
- send({ type: 'error', message: err.message });
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('/api/reports', (req, res) => {
221
+ app.get("/api/reports", (req, res) => {
201
222
  try {
202
- const files = fs.readdirSync(REPORTS_DIR)
203
- .filter(f => f.endsWith('.md'))
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('/api/reports/:filename', (req, res) => {
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, 'utf-8');
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('ko-KR')}
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('ko-KR')}
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 Analyzer 실행 중`);
294
+ console.log(`\n🚀 Commit Ai Agent 실행 중`);
273
295
  console.log(` 브라우저: http://localhost:${PORT}`);
274
296
  console.log(` 분석 대상: ${DEV_ROOT}\n`);
275
297
  });