commit-ai-agent 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -18
- package/bin/cli.js +97 -1
- package/package.json +1 -1
- package/public/app.js +183 -7
- package/public/index.html +68 -5
- package/public/style.css +162 -0
- package/src/hooks/installer.js +191 -0
- package/src/hooks/post-commit.js +90 -0
- package/src/hooks/pre-push.js +298 -0
- package/src/server.js +182 -0
package/README.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
AI가 git 커밋과 현재 변경사항을 한국어로 자동 분석해주는 개발자 도구입니다.
|
|
4
4
|
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## CLI AI와 무엇이 다른가요?
|
|
8
|
+
|
|
9
|
+
Claude Code나 Codex 같은 CLI AI가 있는데 왜 commit-ai-agent를 써야 할까요?
|
|
10
|
+
|
|
11
|
+
| 기능 | CLI AI (Claude Code / Codex) | commit-ai-agent |
|
|
12
|
+
| ---- | :--------------------------: | :-------------: |
|
|
13
|
+
| 커밋 분석 | 수동 명령어 입력 필요 | **자동** (git commit 직후 백그라운드 실행) |
|
|
14
|
+
| 분석 범위 | 현재 세션 단일 프로젝트 | 현재 프로젝트 자동 분석 |
|
|
15
|
+
| 상시 실행 | 세션 종료 시 중단 | **데몬 서버** — 항상 켜져 있음 |
|
|
16
|
+
| Secret 탐지 | 없음 | **pre-push 훅** — push 전 자격증명 자동 차단 |
|
|
17
|
+
| 오프라인 큐 | 없음 | 서버 꺼진 동안 커밋 → 재시작 시 **자동 처리** |
|
|
18
|
+
| 분석 UI | 터미널 텍스트 | **브라우저 GUI** — 검색·필터·저장 |
|
|
19
|
+
| AI 비용 | 구독료 or API 종량제 | Google Gemini **무료 티어** 지원 |
|
|
20
|
+
| 개인화 | 없음 | 프로젝트별 리포트를 `reports/`에 **자동 저장** |
|
|
21
|
+
|
|
22
|
+
> commit-ai-agent의 핵심 가치: **"커밋하면 알아서 분석된다"** — 개발자는 코드에만 집중하세요.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
5
26
|
## 요구 사항
|
|
6
27
|
|
|
7
28
|
- [Node.js](https://nodejs.org) 18 이상
|
|
@@ -14,7 +35,7 @@ AI가 git 커밋과 현재 변경사항을 한국어로 자동 분석해주는
|
|
|
14
35
|
|
|
15
36
|
### 방법 A — npx (설치 없이 바로 실행)
|
|
16
37
|
|
|
17
|
-
-
|
|
38
|
+
- 프로젝트 루트에서 명령어 실행
|
|
18
39
|
- 해당 디렉토리에서 .env 파일 생성(.env.example 참고)
|
|
19
40
|
|
|
20
41
|
```bash
|
|
@@ -24,7 +45,7 @@ npx commit-ai-agent
|
|
|
24
45
|
### 방법 B — 전역 설치 후 명령어로 실행
|
|
25
46
|
|
|
26
47
|
- 전역으로 설치하면 어느 위치에서든 `commit-ai-agent` 명령어로 실행 가능
|
|
27
|
-
-
|
|
48
|
+
- 프로젝트 루트에서 명령어 실행
|
|
28
49
|
- 해당 디렉토리에서 .env 파일 생성(.env.example 참고)
|
|
29
50
|
|
|
30
51
|
```bash
|
|
@@ -34,8 +55,6 @@ commit-ai-agent
|
|
|
34
55
|
|
|
35
56
|
### 방법 C — 직접 클론
|
|
36
57
|
|
|
37
|
-
프로젝트가 모여있는 디렉토리에 클론한 후, 해당 디렉토리에서 명령어 실행
|
|
38
|
-
|
|
39
58
|
```bash
|
|
40
59
|
git clone https://github.com/cjy3458/commit-ai-agent.git
|
|
41
60
|
cd commit-ai-agent
|
|
@@ -83,13 +102,103 @@ DEV_ROOT=C:/Users/projects => 선택 사항(미설정 시 현재 실행 디렉
|
|
|
83
102
|
|
|
84
103
|
---
|
|
85
104
|
|
|
105
|
+
## 새 기능 사용법
|
|
106
|
+
|
|
107
|
+
### Git Hook 자동화 — 커밋 후 자동 분석
|
|
108
|
+
|
|
109
|
+
서버가 실행 중인 상태에서 git hook을 설치하면, `git commit` 직후 자동으로 분석이 시작됩니다.
|
|
110
|
+
|
|
111
|
+
> hook은 **현재 디렉토리(CWD)** 프로젝트에만 설치됩니다. hook을 설치할 프로젝트 안에서 명령어를 실행하세요.
|
|
112
|
+
|
|
113
|
+
#### 1. Hook 설치
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# 현재 디렉토리 프로젝트에 설치
|
|
117
|
+
commit-ai-agent hook install
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### 2. Hook 상태 확인
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
commit-ai-agent hook status
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
훅 설치 상태:
|
|
128
|
+
──────────────────────────────────────────────────
|
|
129
|
+
my-app post-commit: ✅ pre-push: ✅
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### 3. Hook 제거
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
commit-ai-agent hook remove
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### 4. 동작 방식
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
git commit
|
|
142
|
+
↓
|
|
143
|
+
post-commit hook 실행
|
|
144
|
+
├── 서버가 켜져 있음 → 즉시 분석 시작 (브라우저 UI에서 확인)
|
|
145
|
+
└── 서버가 꺼져 있음 → 조용히 종료 (분석 생략)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
> Hook은 기존 `.git/hooks/post-commit`이 있어도 안전하게 추가됩니다 (기존 내용 유지).
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### Secret 유출 탐지 — push 전 자동 차단
|
|
153
|
+
|
|
154
|
+
`pre-push` hook이 설치되어 있으면 `git push` 전에 변경 파일을 자동 스캔합니다.
|
|
155
|
+
실제 자격증명이 감지되면 push가 차단되고 위치와 유형을 알려줍니다.
|
|
156
|
+
|
|
157
|
+
#### 탐지 패턴
|
|
158
|
+
|
|
159
|
+
| 유형 | 심각도 |
|
|
160
|
+
| ---- | ------ |
|
|
161
|
+
| AWS Access Key (`AKIA...`) | Critical |
|
|
162
|
+
| Google API Key (`AIza...`) | Critical |
|
|
163
|
+
| GitHub Personal Token (`ghp_...`) | Critical |
|
|
164
|
+
| Slack Token (`xox...`) | Critical |
|
|
165
|
+
| Stripe Secret Key (`sk_live_...`) | Critical |
|
|
166
|
+
| JWT Token | High |
|
|
167
|
+
| Private Key PEM | Critical |
|
|
168
|
+
| 패스워드 직접 할당 | Medium |
|
|
169
|
+
| API Key 직접 할당 | Medium |
|
|
170
|
+
|
|
171
|
+
#### 차단 메시지 예시
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
╔══════════════════════════════════════════════════════╗
|
|
175
|
+
║ ⛔ commit-ai-agent: SECRET DETECTED ║
|
|
176
|
+
╚══════════════════════════════════════════════════════╝
|
|
177
|
+
|
|
178
|
+
push가 차단됐습니다. 아래 항목을 확인하세요:
|
|
179
|
+
|
|
180
|
+
1. 파일: src/config.js:12 [CRITICAL]
|
|
181
|
+
유형: Google API Key
|
|
182
|
+
값: AIza****y8Xw
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### 오탐(false positive)인 경우 우회
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
SKIP_SECRET_SCAN=1 git push
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
> Gemini API 키가 설정되어 있으면 AI가 오탐 여부를 자동으로 검증하여 불필요한 차단을 줄입니다.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
86
195
|
## 문제 해결
|
|
87
196
|
|
|
88
197
|
**429 오류 (할당량 초과)**
|
|
89
198
|
→ Gemini 무료 티어 한도 도달. 잠시 후 재시도하거나 [유료 플랜](https://ai.google.dev)으로 업그레이드하세요.
|
|
90
199
|
|
|
91
200
|
**프로젝트 목록이 안 뜸**
|
|
92
|
-
→ 기본값은 실행 디렉토리입니다.
|
|
201
|
+
→ 기본값은 실행 디렉토리입니다. `DEV_ROOT` 환경변수로 프로젝트 경로를 직접 지정할 수 있습니다.
|
|
93
202
|
|
|
94
203
|
**`[DEP0190] DeprecationWarning` 경고 (Node.js 22+)**
|
|
95
204
|
→ `shell: true` 옵션과 args 배열을 함께 전달할 때 Node.js 22 이상에서 발생하는 보안 경고입니다. v1.0.7 이상으로 업데이트하면 해결됩니다.
|
|
@@ -142,24 +251,31 @@ git checkout -b feat/my-feature
|
|
|
142
251
|
## 프로젝트 구조
|
|
143
252
|
|
|
144
253
|
```
|
|
145
|
-
commit-
|
|
254
|
+
commit-ai-agent/
|
|
146
255
|
├── bin/
|
|
147
|
-
│ └── cli.js
|
|
256
|
+
│ └── cli.js # CLI 진입점 (npx / npm install -g)
|
|
148
257
|
├── src/
|
|
149
|
-
│ ├── server.js
|
|
150
|
-
│ ├── analyzer.js
|
|
151
|
-
│
|
|
152
|
-
|
|
153
|
-
├── .
|
|
258
|
+
│ ├── server.js # Express 서버 · API 라우트 · SSE 스트리밍
|
|
259
|
+
│ ├── analyzer.js # Gemini AI 분석 프롬프트 · 재시도 로직
|
|
260
|
+
│ ├── git.js # simple-git 래퍼 (커밋 조회, status diff)
|
|
261
|
+
│ └── hooks/
|
|
262
|
+
│ ├── installer.js # git hook 설치·제거·상태 확인
|
|
263
|
+
│ ├── post-commit.js # post-commit 훅 스크립트 (자동 분석 트리거)
|
|
264
|
+
│ └── pre-push.js # pre-push 훅 스크립트 (Secret 탐지)
|
|
265
|
+
├── public/ # 프론트엔드 정적 파일 (HTML · CSS · JS)
|
|
266
|
+
├── .env.example # 환경변수 예시
|
|
154
267
|
└── package.json
|
|
155
268
|
```
|
|
156
269
|
|
|
157
|
-
| 파일
|
|
158
|
-
|
|
|
159
|
-
| `bin/cli.js`
|
|
160
|
-
| `src/server.js`
|
|
161
|
-
| `src/analyzer.js`
|
|
162
|
-
| `src/git.js`
|
|
270
|
+
| 파일 | 역할 |
|
|
271
|
+
| ------------------------- | ------------------------------------------------------------- |
|
|
272
|
+
| `bin/cli.js` | npx 실행, hook 서브커맨드 (install / remove / status) |
|
|
273
|
+
| `src/server.js` | REST API + SSE 엔드포인트, 리포트 저장 |
|
|
274
|
+
| `src/analyzer.js` | Gemini API 호출, 모델 폴백(2.5→2.0→lite), 지수 백오프 |
|
|
275
|
+
| `src/git.js` | 프로젝트 목록 탐색, 커밋 diff, working status diff |
|
|
276
|
+
| `src/hooks/installer.js` | git hook 스크립트 설치·제거 (기존 hook 보존) |
|
|
277
|
+
| `src/hooks/post-commit.js`| 커밋 직후 서버 알림 (서버 없으면 조용히 종료) |
|
|
278
|
+
| `src/hooks/pre-push.js` | push 전 15가지 패턴으로 Secret 스캔, Gemini AI 오탐 필터링 |
|
|
163
279
|
|
|
164
280
|
---
|
|
165
281
|
|
package/bin/cli.js
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* commit-ai-agent CLI 진입점
|
|
4
4
|
* npx commit-ai-agent 또는 npm install -g 후 commit-ai-agent 명령으로 실행
|
|
5
|
+
*
|
|
6
|
+
* 서브커맨드:
|
|
7
|
+
* (없음) 웹 UI 서버 실행
|
|
8
|
+
* hook install 현재 디렉토리에 git hook 설치
|
|
9
|
+
* hook remove git hook 제거
|
|
10
|
+
* hook status git hook 설치 상태 확인
|
|
5
11
|
*/
|
|
6
12
|
import dotenv from "dotenv";
|
|
7
13
|
import path from "path";
|
|
@@ -11,10 +17,100 @@ import { spawn } from "child_process";
|
|
|
11
17
|
// 사용자 현재 디렉토리의 .env 로드
|
|
12
18
|
dotenv.config({ path: path.resolve(process.cwd(), ".env") });
|
|
13
19
|
|
|
14
|
-
// 패키지 루트 경로를 환경변수로 전달 (server.js가 public/ 위치를 찾기 위함)
|
|
15
20
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
// 패키지 루트 경로를 환경변수로 전달 (server.js가 public/ 위치를 찾기 위함)
|
|
16
22
|
process.env.COMMIT_ANALYZER_ROOT = path.resolve(__dirname, "..");
|
|
17
23
|
|
|
24
|
+
const [, , subCmd, ...subArgs] = process.argv;
|
|
25
|
+
|
|
26
|
+
// ──────────────────────────────────────────────
|
|
27
|
+
// hook 서브커맨드
|
|
28
|
+
// ──────────────────────────────────────────────
|
|
29
|
+
if (subCmd === "hook") {
|
|
30
|
+
const action = subArgs[0]; // install | remove | status
|
|
31
|
+
const { installHooks, removeHooks, getHookStatus } = await import(
|
|
32
|
+
"../src/hooks/installer.js"
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
function resolveTargets() {
|
|
36
|
+
return [process.cwd()];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (action === "install") {
|
|
40
|
+
const targets = await resolveTargets();
|
|
41
|
+
console.log("");
|
|
42
|
+
for (const target of targets) {
|
|
43
|
+
try {
|
|
44
|
+
const installed = await installHooks(target);
|
|
45
|
+
console.log(
|
|
46
|
+
` ✅ 설치됨: ${path.basename(target)} (${installed.join(", ")})`
|
|
47
|
+
);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error(` ❌ 실패: ${path.basename(target)} — ${err.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
console.log("");
|
|
53
|
+
console.log(" 이제 커밋할 때마다 자동으로 분석됩니다.");
|
|
54
|
+
console.log(" push 전에 secret 유출도 자동으로 검사됩니다.");
|
|
55
|
+
console.log("");
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (action === "remove") {
|
|
60
|
+
const targets = await resolveTargets();
|
|
61
|
+
console.log("");
|
|
62
|
+
for (const target of targets) {
|
|
63
|
+
try {
|
|
64
|
+
const removed = await removeHooks(target);
|
|
65
|
+
if (removed.length > 0) {
|
|
66
|
+
console.log(
|
|
67
|
+
` ✅ 제거됨: ${path.basename(target)} (${removed.join(", ")})`
|
|
68
|
+
);
|
|
69
|
+
} else {
|
|
70
|
+
console.log(` ℹ️ 훅 없음: ${path.basename(target)}`);
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error(` ❌ 실패: ${path.basename(target)} — ${err.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
console.log("");
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (action === "status") {
|
|
81
|
+
const targets = await resolveTargets();
|
|
82
|
+
console.log("");
|
|
83
|
+
console.log(" 훅 설치 상태:");
|
|
84
|
+
console.log(" " + "─".repeat(50));
|
|
85
|
+
for (const target of targets) {
|
|
86
|
+
try {
|
|
87
|
+
const status = await getHookStatus(target);
|
|
88
|
+
const pc = status.postCommit.installed ? "✅" : "❌";
|
|
89
|
+
const pp = status.prePush.installed ? "✅" : "❌";
|
|
90
|
+
console.log(
|
|
91
|
+
` ${path.basename(target).padEnd(24)} post-commit: ${pc} pre-push: ${pp}`
|
|
92
|
+
);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error(` ❌ 오류: ${path.basename(target)} — ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
console.log("");
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 알 수 없는 action
|
|
102
|
+
console.log("");
|
|
103
|
+
console.log(" 사용법:");
|
|
104
|
+
console.log(" commit-ai-agent hook install # 현재 디렉토리에 훅 설치");
|
|
105
|
+
console.log(" commit-ai-agent hook remove # 훅 제거");
|
|
106
|
+
console.log(" commit-ai-agent hook status # 상태 확인");
|
|
107
|
+
console.log("");
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ──────────────────────────────────────────────
|
|
112
|
+
// 기본 동작: 웹 UI 서버 실행
|
|
113
|
+
// ──────────────────────────────────────────────
|
|
18
114
|
const PORT = process.env.PORT || 3000;
|
|
19
115
|
|
|
20
116
|
console.log("");
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -78,6 +78,9 @@ async function init() {
|
|
|
78
78
|
setAriaState("idle");
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// SSE: post-commit 자동 분석 이벤트 수신
|
|
82
|
+
connectAutoAnalysisEvents();
|
|
83
|
+
|
|
81
84
|
// wire static event listeners (elements guaranteed to exist now)
|
|
82
85
|
document
|
|
83
86
|
.getElementById("refresh-projects")
|
|
@@ -97,6 +100,9 @@ async function init() {
|
|
|
97
100
|
.addEventListener("click", () => {
|
|
98
101
|
togglePre("status-diff-content", "status-diff-toggle-btn");
|
|
99
102
|
});
|
|
103
|
+
document
|
|
104
|
+
.getElementById("refresh-hooks")
|
|
105
|
+
.addEventListener("click", loadHookStatus);
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
// ── Config check ──
|
|
@@ -116,26 +122,27 @@ async function checkConfig() {
|
|
|
116
122
|
|
|
117
123
|
// ── Single Project Mode ──
|
|
118
124
|
async function enterSingleProjectMode() {
|
|
119
|
-
// 프로젝트 선택 UI 숨김
|
|
125
|
+
// 프로젝트 선택 UI만 숨김 (모드 토글은 유지)
|
|
120
126
|
const header = document.querySelector(".selector-card .card-header");
|
|
121
|
-
const modeToggle = document.querySelector(".mode-toggle");
|
|
122
127
|
const projectGrid = document.getElementById("project-grid");
|
|
123
128
|
const selectedHint = document.getElementById("selected-hint");
|
|
124
129
|
if (header) header.style.display = "none";
|
|
125
|
-
if (modeToggle) modeToggle.style.display = "none";
|
|
126
130
|
if (projectGrid) projectGrid.style.display = "none";
|
|
127
131
|
if (selectedHint) selectedHint.style.display = "none";
|
|
128
132
|
|
|
129
133
|
// 현재 디렉토리를 프로젝트로 자동 선택
|
|
130
134
|
selectedProject = "__self__";
|
|
131
|
-
analyzeMode = "status";
|
|
132
135
|
|
|
133
136
|
document.getElementById("analyze-btn").disabled = false;
|
|
134
137
|
const btnText = document.getElementById("analyze-btn-text");
|
|
135
|
-
if (btnText) btnText.textContent = "Hanni에게
|
|
138
|
+
if (btnText) btnText.textContent = "Hanni에게 분석 요청";
|
|
136
139
|
|
|
137
|
-
//
|
|
138
|
-
|
|
140
|
+
// 현재 모드에 따라 미리 로드
|
|
141
|
+
if (analyzeMode === "commit") {
|
|
142
|
+
await fetchCommitPreview();
|
|
143
|
+
} else {
|
|
144
|
+
await fetchStatusPreview();
|
|
145
|
+
}
|
|
139
146
|
}
|
|
140
147
|
|
|
141
148
|
// ── Projects ──
|
|
@@ -439,6 +446,100 @@ function onCopy() {
|
|
|
439
446
|
});
|
|
440
447
|
}
|
|
441
448
|
|
|
449
|
+
// ── Auto Analysis via SSE (+ polling fallback) ──
|
|
450
|
+
let autoAnalysisPollTimer = null;
|
|
451
|
+
let autoAnalysisShownFilename = null; // 이미 표시한 리포트 중복 방지
|
|
452
|
+
|
|
453
|
+
function connectAutoAnalysisEvents() {
|
|
454
|
+
const evtSource = new EventSource("/api/events");
|
|
455
|
+
|
|
456
|
+
evtSource.onmessage = (e) => {
|
|
457
|
+
try {
|
|
458
|
+
const data = JSON.parse(e.data);
|
|
459
|
+
handleAutoAnalysisEvent(data);
|
|
460
|
+
} catch {}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
evtSource.onerror = () => {
|
|
464
|
+
// EventSource 자동 재연결됨 — 별도 처리 불필요
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// SSE와 관계없이 5초마다 상태 폴링 (SSE 놓쳐도 반드시 동작)
|
|
468
|
+
startAutoAnalysisPoll();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function handleAutoAnalysisEvent(data) {
|
|
472
|
+
if (isAnalyzing) return; // 수동 분석 중엔 방해 안 함
|
|
473
|
+
if (data.type === "analysis-started") {
|
|
474
|
+
showAutoAnalysisStarted(data.projectName);
|
|
475
|
+
startAutoAnalysisPoll(); // SSE가 끊겨도 완료를 폴링으로 감지
|
|
476
|
+
} else if (data.type === "analysis-done") {
|
|
477
|
+
stopAutoAnalysisPoll();
|
|
478
|
+
if (data.filename !== autoAnalysisShownFilename) {
|
|
479
|
+
autoAnalysisShownFilename = data.filename;
|
|
480
|
+
showAutoAnalysisDone(data);
|
|
481
|
+
}
|
|
482
|
+
} else if (data.type === "analysis-error") {
|
|
483
|
+
stopAutoAnalysisPoll();
|
|
484
|
+
setStatus("error", `자동 분석 오류: ${data.message}`);
|
|
485
|
+
setAriaState("error");
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// 5초마다 서버 상태 폴링 (SSE 대비 폴백)
|
|
490
|
+
function startAutoAnalysisPoll() {
|
|
491
|
+
stopAutoAnalysisPoll();
|
|
492
|
+
autoAnalysisPollTimer = setInterval(async () => {
|
|
493
|
+
if (isAnalyzing) return;
|
|
494
|
+
try {
|
|
495
|
+
const res = await fetch("/api/auto-analysis/state");
|
|
496
|
+
const state = await res.json();
|
|
497
|
+
if (state.status === "analyzing" && autoAnalysisShownFilename !== "__loading__") {
|
|
498
|
+
autoAnalysisShownFilename = "__loading__";
|
|
499
|
+
showAutoAnalysisStarted(state.projectName);
|
|
500
|
+
} else if (state.status === "done" && state.filename !== autoAnalysisShownFilename) {
|
|
501
|
+
autoAnalysisShownFilename = state.filename;
|
|
502
|
+
showAutoAnalysisDone(state);
|
|
503
|
+
}
|
|
504
|
+
} catch {}
|
|
505
|
+
}, 5000);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function stopAutoAnalysisPoll() {
|
|
509
|
+
if (autoAnalysisPollTimer) {
|
|
510
|
+
clearInterval(autoAnalysisPollTimer);
|
|
511
|
+
autoAnalysisPollTimer = null;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function showAutoAnalysisStarted(projectName) {
|
|
516
|
+
const resultCard = document.getElementById("result-card");
|
|
517
|
+
const analysisBody = document.getElementById("analysis-body");
|
|
518
|
+
const reportSaved = document.getElementById("report-saved");
|
|
519
|
+
resultCard.style.display = "block";
|
|
520
|
+
analysisBody.innerHTML = "";
|
|
521
|
+
reportSaved.textContent = "";
|
|
522
|
+
document.getElementById("copy-btn").style.display = "none";
|
|
523
|
+
setStatus("loading", `${projectName} 커밋 자동 분석 중...`);
|
|
524
|
+
setAriaState("thinking");
|
|
525
|
+
resultCard.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function showAutoAnalysisDone({ filename, content }) {
|
|
529
|
+
const resultCard = document.getElementById("result-card");
|
|
530
|
+
const analysisBody = document.getElementById("analysis-body");
|
|
531
|
+
const reportSaved = document.getElementById("report-saved");
|
|
532
|
+
const copyBtn = document.getElementById("copy-btn");
|
|
533
|
+
resultCard.style.display = "block"; // ← 핵심: 카드 표시
|
|
534
|
+
analysisBody.innerHTML = marked.parse(content);
|
|
535
|
+
reportSaved.textContent = `✓ 저장됨: ${filename}`;
|
|
536
|
+
setStatus("done", "✅ 자동 분석 완료!");
|
|
537
|
+
setAriaState("done");
|
|
538
|
+
copyBtn.style.display = "inline-flex";
|
|
539
|
+
copyBtn._text = content;
|
|
540
|
+
resultCard.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
541
|
+
}
|
|
542
|
+
|
|
442
543
|
// ── Reports Tab ──
|
|
443
544
|
async function loadReports() {
|
|
444
545
|
const listEl = document.getElementById("reports-list");
|
|
@@ -505,10 +606,85 @@ function setupTabs() {
|
|
|
505
606
|
btn.classList.add("active");
|
|
506
607
|
document.getElementById("tab-" + tab).classList.add("active");
|
|
507
608
|
if (tab === "reports") loadReports();
|
|
609
|
+
if (tab === "hooks") loadHookStatus();
|
|
508
610
|
});
|
|
509
611
|
});
|
|
510
612
|
}
|
|
511
613
|
|
|
614
|
+
// ── Hooks Tab ──
|
|
615
|
+
async function loadHookStatus() {
|
|
616
|
+
const listEl = document.getElementById("hook-projects-list");
|
|
617
|
+
listEl.innerHTML = '<p class="empty-state">불러오는 중...</p>';
|
|
618
|
+
try {
|
|
619
|
+
const res = await fetch("/api/hooks/status");
|
|
620
|
+
const { projects } = await res.json();
|
|
621
|
+
if (!projects || projects.length === 0) {
|
|
622
|
+
listEl.innerHTML =
|
|
623
|
+
'<p class="empty-state">git 프로젝트를 찾을 수 없습니다.</p>';
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
listEl.innerHTML = projects
|
|
627
|
+
.map((p) => {
|
|
628
|
+
const pcInstalled = p.postCommit?.installed;
|
|
629
|
+
const ppInstalled = p.prePush?.installed;
|
|
630
|
+
const allInstalled = pcInstalled && ppInstalled;
|
|
631
|
+
const noneInstalled = !pcInstalled && !ppInstalled;
|
|
632
|
+
const statusBadge = allInstalled
|
|
633
|
+
? '<span class="hook-badge installed">설치됨</span>'
|
|
634
|
+
: noneInstalled
|
|
635
|
+
? '<span class="hook-badge not-installed">미설치</span>'
|
|
636
|
+
: '<span class="hook-badge partial">일부 설치</span>';
|
|
637
|
+
|
|
638
|
+
const displayName = p.displayName || p.name;
|
|
639
|
+
return `<div class="hook-project-row" data-name="${escHtml(p.name)}">
|
|
640
|
+
<div class="hook-project-info">
|
|
641
|
+
<span class="hook-project-name">${escHtml(displayName)}</span>
|
|
642
|
+
${statusBadge}
|
|
643
|
+
<span class="hook-detail">post-commit: ${pcInstalled ? "✅" : "❌"} pre-push: ${ppInstalled ? "✅" : "❌"}</span>
|
|
644
|
+
</div>
|
|
645
|
+
<div class="hook-project-actions">
|
|
646
|
+
${
|
|
647
|
+
allInstalled
|
|
648
|
+
? `<button class="btn-ghost hook-remove-btn" data-name="${escHtml(p.name)}">제거</button>`
|
|
649
|
+
: `<button class="btn-secondary hook-install-btn" data-name="${escHtml(p.name)}">설치</button>`
|
|
650
|
+
}
|
|
651
|
+
</div>
|
|
652
|
+
</div>`;
|
|
653
|
+
})
|
|
654
|
+
.join("");
|
|
655
|
+
|
|
656
|
+
listEl.querySelectorAll(".hook-install-btn").forEach((btn) => {
|
|
657
|
+
btn.addEventListener("click", () => handleHookAction("install", btn.dataset.name, btn));
|
|
658
|
+
});
|
|
659
|
+
listEl.querySelectorAll(".hook-remove-btn").forEach((btn) => {
|
|
660
|
+
btn.addEventListener("click", () => handleHookAction("remove", btn.dataset.name, btn));
|
|
661
|
+
});
|
|
662
|
+
} catch {
|
|
663
|
+
listEl.innerHTML =
|
|
664
|
+
'<p class="empty-state" style="color:var(--danger)">훅 상태를 불러오지 못했습니다.</p>';
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function handleHookAction(action, projectName, btn) {
|
|
669
|
+
const original = btn.textContent;
|
|
670
|
+
btn.disabled = true;
|
|
671
|
+
btn.textContent = action === "install" ? "설치 중..." : "제거 중...";
|
|
672
|
+
try {
|
|
673
|
+
const res = await fetch(`/api/hooks/${action}`, {
|
|
674
|
+
method: "POST",
|
|
675
|
+
headers: { "Content-Type": "application/json" },
|
|
676
|
+
body: JSON.stringify({ projectName }),
|
|
677
|
+
});
|
|
678
|
+
const data = await res.json();
|
|
679
|
+
if (!res.ok) throw new Error(data.error || "요청 실패");
|
|
680
|
+
await loadHookStatus(); // 새로고침
|
|
681
|
+
} catch (err) {
|
|
682
|
+
btn.disabled = false;
|
|
683
|
+
btn.textContent = original;
|
|
684
|
+
alert(`오류: ${err.message}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
512
688
|
// ── Helpers ──
|
|
513
689
|
function togglePre(preId, btnId) {
|
|
514
690
|
const pre = document.getElementById(preId);
|
package/public/index.html
CHANGED
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
<link rel="stylesheet" href="/style.css" />
|
|
22
22
|
</head>
|
|
23
23
|
<body>
|
|
24
|
-
<!-- ── Header ── -->
|
|
25
24
|
<header class="header">
|
|
26
25
|
<div class="header-inner">
|
|
27
26
|
<div class="logo">
|
|
@@ -36,6 +35,9 @@
|
|
|
36
35
|
<button class="nav-btn" data-tab="reports" id="btn-reports">
|
|
37
36
|
리포트
|
|
38
37
|
</button>
|
|
38
|
+
<button class="nav-btn" data-tab="hooks" id="btn-hooks">
|
|
39
|
+
훅 관리
|
|
40
|
+
</button>
|
|
39
41
|
</nav>
|
|
40
42
|
<div class="aria-chip" id="aria-chip">
|
|
41
43
|
<span class="aria-chip-dot" id="aria-chip-dot"></span>
|
|
@@ -44,11 +46,8 @@
|
|
|
44
46
|
</div>
|
|
45
47
|
</header>
|
|
46
48
|
|
|
47
|
-
<!-- ── Main ── -->
|
|
48
49
|
<main class="main">
|
|
49
|
-
<!-- ── Tab: Analyze ── -->
|
|
50
50
|
<section class="tab-content active" id="tab-analyze">
|
|
51
|
-
<!-- ── Aria Hero ── -->
|
|
52
51
|
<div class="aria-hero">
|
|
53
52
|
<div class="aria-robot-wrap idle" id="aria-robot-wrap">
|
|
54
53
|
<svg
|
|
@@ -80,7 +79,6 @@
|
|
|
80
79
|
<stop offset="100%" stop-color="#0e7490" />
|
|
81
80
|
</linearGradient>
|
|
82
81
|
</defs>
|
|
83
|
-
<!-- Antenna -->
|
|
84
82
|
<line
|
|
85
83
|
x1="45"
|
|
86
84
|
y1="7"
|
|
@@ -265,6 +263,71 @@
|
|
|
265
263
|
</div>
|
|
266
264
|
</section>
|
|
267
265
|
|
|
266
|
+
<!-- ── Tab: Hooks ── -->
|
|
267
|
+
<section class="tab-content" id="tab-hooks">
|
|
268
|
+
<div class="card">
|
|
269
|
+
<div class="card-header">
|
|
270
|
+
<h2 class="card-title">🪝 Git Hook 관리</h2>
|
|
271
|
+
<button class="btn-ghost" id="refresh-hooks" title="새로고침">
|
|
272
|
+
↻
|
|
273
|
+
</button>
|
|
274
|
+
</div>
|
|
275
|
+
<p class="hook-desc">
|
|
276
|
+
훅을 설치하면 <strong>커밋할 때마다 자동으로 AI 분석</strong>이
|
|
277
|
+
실행되고, <strong>push 전에 secret 유출을 자동 차단</strong>합니다.
|
|
278
|
+
</p>
|
|
279
|
+
|
|
280
|
+
<div class="hook-section">
|
|
281
|
+
<div class="hook-section-title">
|
|
282
|
+
<span class="hook-icon">⚡</span>
|
|
283
|
+
post-commit — 자동 커밋 분석
|
|
284
|
+
</div>
|
|
285
|
+
<p class="hook-section-desc">
|
|
286
|
+
커밋 직후 Gemini AI가 백그라운드에서 분석 리포트를 자동
|
|
287
|
+
생성합니다.
|
|
288
|
+
</p>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div class="hook-section">
|
|
292
|
+
<div class="hook-section-title">
|
|
293
|
+
<span class="hook-icon">⛔</span>
|
|
294
|
+
pre-push — Secret 유출 탐지
|
|
295
|
+
</div>
|
|
296
|
+
<p class="hook-section-desc">
|
|
297
|
+
push 전에 API 키, 비밀번호, private key 등의 유출을 탐지하고
|
|
298
|
+
차단합니다.
|
|
299
|
+
</p>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<div id="hook-projects-list" class="hook-projects-list">
|
|
303
|
+
<p class="empty-state">불러오는 중...</p>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<div class="card hook-cli-card">
|
|
308
|
+
<h2 class="card-title">💻 CLI 사용법</h2>
|
|
309
|
+
<div class="hook-cli-block">
|
|
310
|
+
<div class="hook-cli-item">
|
|
311
|
+
<code>commit-ai-agent hook install</code>
|
|
312
|
+
<span>현재 디렉토리에 훅 설치</span>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<div class="hook-cli-item">
|
|
316
|
+
<code>commit-ai-agent hook remove</code>
|
|
317
|
+
<span>훅 제거</span>
|
|
318
|
+
</div>
|
|
319
|
+
<div class="hook-cli-item">
|
|
320
|
+
<code>commit-ai-agent hook status</code>
|
|
321
|
+
<span>설치 상태 확인</span>
|
|
322
|
+
</div>
|
|
323
|
+
<div class="hook-cli-item">
|
|
324
|
+
<code>SKIP_SECRET_SCAN=1 git push</code>
|
|
325
|
+
<span>Secret 스캔 일시 스킵 (오탐 시)</span>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
</section>
|
|
330
|
+
|
|
268
331
|
<!-- ── Tab: Reports ── -->
|
|
269
332
|
<section class="tab-content" id="tab-reports">
|
|
270
333
|
<div class="card">
|