commit-ai-agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -0
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/bin/cli.js +36 -0
- package/package.json +39 -0
- package/public/app.js +363 -0
- package/public/index.html +128 -0
- package/public/style.css +293 -0
- package/src/analyzer.js +173 -0
- package/src/git.js +149 -0
- package/src/server.js +275 -0
package/public/style.css
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/* ── Reset & Variables ── */
|
|
2
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
3
|
+
|
|
4
|
+
:root {
|
|
5
|
+
--bg: #0f1117;
|
|
6
|
+
--bg2: #161b27;
|
|
7
|
+
--bg3: #1e2535;
|
|
8
|
+
--border: #2a3245;
|
|
9
|
+
--primary: #6366f1;
|
|
10
|
+
--primary-h: #818cf8;
|
|
11
|
+
--accent: #22d3ee;
|
|
12
|
+
--success: #34d399;
|
|
13
|
+
--warn: #fb923c;
|
|
14
|
+
--danger: #f87171;
|
|
15
|
+
--text: #e2e8f0;
|
|
16
|
+
--text2: #94a3b8;
|
|
17
|
+
--text3: #475569;
|
|
18
|
+
--radius: 12px;
|
|
19
|
+
--shadow: 0 4px 24px rgba(0,0,0,0.4);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
html { scroll-behavior: smooth; }
|
|
23
|
+
body {
|
|
24
|
+
font-family: 'Pretendard', -apple-system, sans-serif;
|
|
25
|
+
background: var(--bg);
|
|
26
|
+
color: var(--text);
|
|
27
|
+
min-height: 100vh;
|
|
28
|
+
line-height: 1.6;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* ── Scrollbar ── */
|
|
32
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
33
|
+
::-webkit-scrollbar-track { background: var(--bg2); }
|
|
34
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
35
|
+
|
|
36
|
+
/* ── Header ── */
|
|
37
|
+
.header {
|
|
38
|
+
position: sticky; top: 0; z-index: 50;
|
|
39
|
+
background: rgba(15,17,23,0.85);
|
|
40
|
+
backdrop-filter: blur(12px);
|
|
41
|
+
border-bottom: 1px solid var(--border);
|
|
42
|
+
}
|
|
43
|
+
.header-inner {
|
|
44
|
+
max-width: 1100px; margin: 0 auto;
|
|
45
|
+
padding: 0 24px;
|
|
46
|
+
height: 60px;
|
|
47
|
+
display: flex; align-items: center; gap: 20px;
|
|
48
|
+
}
|
|
49
|
+
.logo { display: flex; align-items: center; gap: 8px; }
|
|
50
|
+
.logo-icon { font-size: 22px; }
|
|
51
|
+
.logo-text { font-size: 17px; font-weight: 700; letter-spacing: -0.3px; }
|
|
52
|
+
.logo-badge {
|
|
53
|
+
font-size: 10px; font-weight: 700; letter-spacing: 0.5px;
|
|
54
|
+
background: linear-gradient(135deg, var(--primary), var(--accent));
|
|
55
|
+
color: #fff; padding: 2px 7px; border-radius: 99px;
|
|
56
|
+
}
|
|
57
|
+
.header-nav { display: flex; gap: 4px; margin-left: auto; }
|
|
58
|
+
.nav-btn {
|
|
59
|
+
background: none; border: none; color: var(--text2);
|
|
60
|
+
padding: 6px 14px; border-radius: 8px; cursor: pointer;
|
|
61
|
+
font-size: 14px; font-weight: 500; transition: all 0.2s;
|
|
62
|
+
}
|
|
63
|
+
.nav-btn:hover { background: var(--bg3); color: var(--text); }
|
|
64
|
+
.nav-btn.active { background: var(--bg3); color: var(--primary); }
|
|
65
|
+
|
|
66
|
+
.install-btn {
|
|
67
|
+
background: linear-gradient(135deg, var(--primary), var(--accent));
|
|
68
|
+
border: none; color: #fff; padding: 7px 14px;
|
|
69
|
+
border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 600;
|
|
70
|
+
transition: opacity 0.2s;
|
|
71
|
+
}
|
|
72
|
+
.install-btn:hover { opacity: 0.85; }
|
|
73
|
+
|
|
74
|
+
/* ── Main ── */
|
|
75
|
+
.main { max-width: 1100px; margin: 0 auto; padding: 28px 24px 60px; }
|
|
76
|
+
|
|
77
|
+
/* ── Tabs ── */
|
|
78
|
+
.tab-content { display: none; }
|
|
79
|
+
.tab-content.active { display: block; }
|
|
80
|
+
|
|
81
|
+
/* ── Alert ── */
|
|
82
|
+
.alert {
|
|
83
|
+
padding: 14px 18px; border-radius: var(--radius);
|
|
84
|
+
margin-bottom: 20px; font-size: 14px;
|
|
85
|
+
display: flex; align-items: center; gap: 8px;
|
|
86
|
+
}
|
|
87
|
+
.alert-warn { background: rgba(251,146,60,0.12); border: 1px solid rgba(251,146,60,0.3); color: var(--warn); }
|
|
88
|
+
|
|
89
|
+
/* ── Card ── */
|
|
90
|
+
.card {
|
|
91
|
+
background: var(--bg2);
|
|
92
|
+
border: 1px solid var(--border);
|
|
93
|
+
border-radius: var(--radius);
|
|
94
|
+
padding: 24px;
|
|
95
|
+
margin-bottom: 20px;
|
|
96
|
+
box-shadow: var(--shadow);
|
|
97
|
+
}
|
|
98
|
+
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
|
|
99
|
+
.card-title { font-size: 16px; font-weight: 700; display: flex; align-items: center; gap: 8px; }
|
|
100
|
+
.card-footer {
|
|
101
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
102
|
+
margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--border);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ── Mode Toggle ── */
|
|
106
|
+
.mode-toggle {
|
|
107
|
+
display: flex; gap: 6px; margin-bottom: 16px;
|
|
108
|
+
background: var(--bg3); padding: 4px; border-radius: 10px;
|
|
109
|
+
width: fit-content;
|
|
110
|
+
}
|
|
111
|
+
.mode-btn {
|
|
112
|
+
background: none; border: none; color: var(--text2);
|
|
113
|
+
padding: 7px 16px; border-radius: 8px; cursor: pointer;
|
|
114
|
+
font-size: 13px; font-weight: 500; transition: all 0.2s;
|
|
115
|
+
white-space: nowrap;
|
|
116
|
+
}
|
|
117
|
+
.mode-btn:hover { color: var(--text); }
|
|
118
|
+
.mode-btn.active {
|
|
119
|
+
background: var(--bg2); color: var(--primary);
|
|
120
|
+
box-shadow: 0 1px 4px rgba(0,0,0,0.3);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
/* ── Project Grid ── */
|
|
125
|
+
.project-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 10px; }
|
|
126
|
+
.project-item {
|
|
127
|
+
background: var(--bg3); border: 1px solid var(--border);
|
|
128
|
+
border-radius: 10px; padding: 14px 16px;
|
|
129
|
+
cursor: pointer; transition: all 0.2s;
|
|
130
|
+
font-size: 14px; font-weight: 500;
|
|
131
|
+
display: flex; align-items: flex-start; gap: 10px;
|
|
132
|
+
}
|
|
133
|
+
.project-item:hover { border-color: var(--primary); background: rgba(99,102,241,0.08); transform: translateY(-1px); }
|
|
134
|
+
.project-item.selected { border-color: var(--primary); background: rgba(99,102,241,0.15); }
|
|
135
|
+
.project-item .proj-icon { font-size: 18px; flex-shrink: 0; margin-top: 1px; }
|
|
136
|
+
.project-item .proj-name { word-break: break-all; line-height: 1.4; }
|
|
137
|
+
|
|
138
|
+
/* Skeleton */
|
|
139
|
+
.skeleton-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 10px; }
|
|
140
|
+
.skeleton { height: 54px; border-radius: 10px; background: linear-gradient(90deg, var(--bg3) 25%, var(--border) 50%, var(--bg3) 75%); background-size: 200%; animation: shimmer 1.4s infinite; }
|
|
141
|
+
@keyframes shimmer { 0%{background-position:200%}100%{background-position:-200%} }
|
|
142
|
+
|
|
143
|
+
/* ── Buttons ── */
|
|
144
|
+
.btn-primary {
|
|
145
|
+
background: linear-gradient(135deg, var(--primary), #818cf8);
|
|
146
|
+
color: #fff; border: none; padding: 10px 22px;
|
|
147
|
+
border-radius: 10px; cursor: pointer; font-size: 14px; font-weight: 600;
|
|
148
|
+
display: flex; align-items: center; gap: 8px;
|
|
149
|
+
transition: opacity 0.2s, transform 0.15s;
|
|
150
|
+
}
|
|
151
|
+
.btn-primary:hover:not(:disabled) { opacity: 0.9; transform: translateY(-1px); }
|
|
152
|
+
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
|
|
153
|
+
.btn-primary.loading { pointer-events: none; }
|
|
154
|
+
|
|
155
|
+
.btn-ghost {
|
|
156
|
+
background: none; border: 1px solid var(--border);
|
|
157
|
+
color: var(--text2); padding: 6px 14px; border-radius: 8px;
|
|
158
|
+
cursor: pointer; font-size: 13px; transition: all 0.2s;
|
|
159
|
+
}
|
|
160
|
+
.btn-ghost:hover { background: var(--bg3); color: var(--text); }
|
|
161
|
+
|
|
162
|
+
.refresh-btn {
|
|
163
|
+
background: none; border: none; color: var(--text2);
|
|
164
|
+
font-size: 18px; cursor: pointer; padding: 4px 8px;
|
|
165
|
+
border-radius: 6px; transition: all 0.2s;
|
|
166
|
+
}
|
|
167
|
+
.refresh-btn:hover { color: var(--primary); transform: rotate(90deg); }
|
|
168
|
+
|
|
169
|
+
.selected-hint { font-size: 13px; color: var(--text2); }
|
|
170
|
+
|
|
171
|
+
/* ── Commit Card ── */
|
|
172
|
+
.commit-meta {
|
|
173
|
+
display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
174
|
+
gap: 12px; margin-bottom: 16px;
|
|
175
|
+
}
|
|
176
|
+
.meta-item { background: var(--bg3); border-radius: 8px; padding: 12px 14px; }
|
|
177
|
+
.meta-label { font-size: 11px; color: var(--text3); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
|
|
178
|
+
.meta-value { font-size: 14px; font-weight: 500; word-break: break-all; }
|
|
179
|
+
.meta-value.hash { font-family: 'JetBrains Mono', monospace; color: var(--accent); }
|
|
180
|
+
|
|
181
|
+
/* ── Status Chips ── */
|
|
182
|
+
.stat-chip {
|
|
183
|
+
font-size: 12px; font-weight: 600; padding: 3px 10px;
|
|
184
|
+
border-radius: 99px; border: 1px solid;
|
|
185
|
+
}
|
|
186
|
+
.stat-chip.staged { background: rgba(99,102,241,0.15); color: var(--primary-h); border-color: rgba(99,102,241,0.3); }
|
|
187
|
+
.stat-chip.modified { background: rgba(251,146,60,0.12); color: var(--warn); border-color: rgba(251,146,60,0.3); }
|
|
188
|
+
.stat-chip.deleted { background: rgba(248,113,113,0.12); color: var(--danger); border-color: rgba(248,113,113,0.3); }
|
|
189
|
+
.stat-chip.untracked{ background: rgba(52,211,153,0.12); color: var(--success); border-color: rgba(52,211,153,0.3); }
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
.diff-toggle { margin-top: 4px; }
|
|
193
|
+
|
|
194
|
+
.diff-content {
|
|
195
|
+
margin-top: 12px; padding: 16px; border-radius: 10px;
|
|
196
|
+
background: var(--bg); border: 1px solid var(--border);
|
|
197
|
+
font-family: 'JetBrains Mono', monospace; font-size: 12px;
|
|
198
|
+
line-height: 1.7; overflow-x: auto; white-space: pre;
|
|
199
|
+
max-height: 400px; overflow-y: auto;
|
|
200
|
+
color: var(--text2);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* ── Result Card ── */
|
|
204
|
+
.result-card { overflow: hidden; }
|
|
205
|
+
.result-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
|
|
206
|
+
.result-actions { display: flex; align-items: center; gap: 10px; }
|
|
207
|
+
.report-saved { font-size: 12px; color: var(--success); }
|
|
208
|
+
|
|
209
|
+
.status-bar {
|
|
210
|
+
display: flex; align-items: center; gap: 10px;
|
|
211
|
+
padding: 10px 14px; border-radius: 8px;
|
|
212
|
+
background: var(--bg3); margin-bottom: 20px;
|
|
213
|
+
font-size: 13px; color: var(--text2);
|
|
214
|
+
}
|
|
215
|
+
.status-bar.done { background: rgba(52,211,153,0.1); color: var(--success); }
|
|
216
|
+
.status-bar.error { background: rgba(248,113,113,0.1); color: var(--danger); }
|
|
217
|
+
|
|
218
|
+
.status-dot {
|
|
219
|
+
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
|
|
220
|
+
background: var(--text3);
|
|
221
|
+
}
|
|
222
|
+
.status-dot.loading { background: var(--primary); animation: pulse 1.2s infinite; }
|
|
223
|
+
.status-dot.done { background: var(--success); }
|
|
224
|
+
.status-dot.error { background: var(--danger); }
|
|
225
|
+
@keyframes pulse { 0%,100%{opacity:1}50%{opacity:0.3} }
|
|
226
|
+
|
|
227
|
+
/* ── Markdown Rendered Output ── */
|
|
228
|
+
.analysis-body {
|
|
229
|
+
line-height: 1.75;
|
|
230
|
+
}
|
|
231
|
+
.analysis-body h1, .analysis-body h2 {
|
|
232
|
+
font-size: 18px; font-weight: 700; margin: 28px 0 12px;
|
|
233
|
+
padding-bottom: 8px; border-bottom: 1px solid var(--border);
|
|
234
|
+
color: var(--text);
|
|
235
|
+
}
|
|
236
|
+
.analysis-body h2:first-child { margin-top: 0; }
|
|
237
|
+
.analysis-body h3 { font-size: 15px; font-weight: 600; margin: 20px 0 8px; color: var(--text); }
|
|
238
|
+
.analysis-body p { color: var(--text2); margin-bottom: 12px; }
|
|
239
|
+
.analysis-body ul, .analysis-body ol { padding-left: 20px; color: var(--text2); margin-bottom: 12px; }
|
|
240
|
+
.analysis-body li { margin-bottom: 6px; }
|
|
241
|
+
.analysis-body code {
|
|
242
|
+
font-family: 'JetBrains Mono', monospace; font-size: 13px;
|
|
243
|
+
background: var(--bg3); color: var(--accent);
|
|
244
|
+
padding: 2px 6px; border-radius: 4px;
|
|
245
|
+
}
|
|
246
|
+
.analysis-body pre {
|
|
247
|
+
background: var(--bg); border: 1px solid var(--border);
|
|
248
|
+
border-radius: 10px; padding: 16px; margin: 12px 0;
|
|
249
|
+
overflow-x: auto;
|
|
250
|
+
}
|
|
251
|
+
.analysis-body pre code {
|
|
252
|
+
background: none; color: var(--text); padding: 0; font-size: 13px;
|
|
253
|
+
}
|
|
254
|
+
.analysis-body strong { color: var(--text); }
|
|
255
|
+
.analysis-body blockquote {
|
|
256
|
+
border-left: 3px solid var(--primary); padding-left: 14px;
|
|
257
|
+
color: var(--text2); margin: 12px 0;
|
|
258
|
+
}
|
|
259
|
+
.analysis-body table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 14px; }
|
|
260
|
+
.analysis-body th { background: var(--bg3); padding: 10px 14px; text-align: left; border: 1px solid var(--border); }
|
|
261
|
+
.analysis-body td { padding: 9px 14px; border: 1px solid var(--border); color: var(--text2); }
|
|
262
|
+
|
|
263
|
+
/* ── Section Tags ── */
|
|
264
|
+
.section-tag {
|
|
265
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
266
|
+
font-size: 11px; font-weight: 700; letter-spacing: 0.5px;
|
|
267
|
+
padding: 3px 10px; border-radius: 99px; margin-right: 8px;
|
|
268
|
+
vertical-align: middle;
|
|
269
|
+
}
|
|
270
|
+
.tag-review { background: rgba(248,113,113,0.15); color: var(--danger); border: 1px solid rgba(248,113,113,0.3); }
|
|
271
|
+
.tag-doc { background: rgba(99,102,241,0.15); color: var(--primary-h); border: 1px solid rgba(99,102,241,0.3); }
|
|
272
|
+
|
|
273
|
+
/* ── Reports List ── */
|
|
274
|
+
.reports-list { display: flex; flex-direction: column; gap: 8px; }
|
|
275
|
+
.report-item {
|
|
276
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
277
|
+
padding: 12px 16px; background: var(--bg3);
|
|
278
|
+
border: 1px solid var(--border); border-radius: 10px;
|
|
279
|
+
font-size: 14px; cursor: pointer; transition: all 0.2s;
|
|
280
|
+
}
|
|
281
|
+
.report-item:hover { border-color: var(--primary); }
|
|
282
|
+
.report-item-name { font-weight: 500; }
|
|
283
|
+
.report-item-date { font-size: 12px; color: var(--text3); font-family: 'JetBrains Mono', monospace; }
|
|
284
|
+
.empty-state { color: var(--text3); font-size: 14px; text-align: center; padding: 32px; }
|
|
285
|
+
|
|
286
|
+
/* ── Responsive ── */
|
|
287
|
+
@media (max-width: 640px) {
|
|
288
|
+
.header-inner { padding: 0 16px; }
|
|
289
|
+
.main { padding: 16px 16px 48px; }
|
|
290
|
+
.card { padding: 18px; }
|
|
291
|
+
.card-footer { flex-direction: column; align-items: stretch; gap: 12px; }
|
|
292
|
+
.commit-meta { grid-template-columns: 1fr 1fr; }
|
|
293
|
+
}
|
package/src/analyzer.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
2
|
+
|
|
3
|
+
// 사용할 모델 우선순위
|
|
4
|
+
const MODEL_PRIORITY = [
|
|
5
|
+
'gemini-2.5-flash',
|
|
6
|
+
'gemini-2.0-flash',
|
|
7
|
+
'gemini-2.0-flash-lite',
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
let genAI = null;
|
|
11
|
+
|
|
12
|
+
function getClient(apiKey) {
|
|
13
|
+
if (!genAI) {
|
|
14
|
+
genAI = new GoogleGenerativeAI(apiKey);
|
|
15
|
+
}
|
|
16
|
+
return genAI;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 429 Too Many Requests 시 지수 백오프로 재시도합니다.
|
|
21
|
+
*/
|
|
22
|
+
async function generateWithRetry(apiKey, prompt, maxRetries = 2) {
|
|
23
|
+
const client = getClient(apiKey);
|
|
24
|
+
|
|
25
|
+
for (const modelName of MODEL_PRIORITY) {
|
|
26
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
27
|
+
try {
|
|
28
|
+
const model = client.getGenerativeModel({ model: modelName });
|
|
29
|
+
const result = await model.generateContent(prompt);
|
|
30
|
+
return result.response.text();
|
|
31
|
+
} catch (err) {
|
|
32
|
+
const is429 = err.message?.includes('429') || err.message?.includes('quota');
|
|
33
|
+
const isLastModel = modelName === MODEL_PRIORITY[MODEL_PRIORITY.length - 1];
|
|
34
|
+
const isLastAttempt = attempt === maxRetries;
|
|
35
|
+
|
|
36
|
+
if (is429 && !isLastAttempt) {
|
|
37
|
+
// 재시도 대기 (429 응답의 retryDelay 참고)
|
|
38
|
+
const waitSec = Math.pow(2, attempt + 1) * 5; // 10s, 20s
|
|
39
|
+
console.log(`[${modelName}] 429 할당량 초과, ${waitSec}초 후 재시도...`);
|
|
40
|
+
await new Promise(r => setTimeout(r, waitSec * 1000));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (is429 && isLastAttempt && !isLastModel) {
|
|
45
|
+
// 다음 모델로 전환
|
|
46
|
+
console.log(`[${modelName}] 할당량 소진, 다음 모델로 전환...`);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
throw err; // 429가 아닌 오류는 즉시 throw
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw new Error('모든 모델의 할당량이 초과되었습니다. 잠시 후 다시 시도해 주세요.\n\n💡 해결 방법:\n- Google AI Studio에서 새 API 키를 발급받거나\n- https://ai.google.dev 에서 유료 플랜으로 업그레이드하세요.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 커밋 정보를 Gemini AI로 분석하여 문서화 + 코드 리뷰를 생성합니다.
|
|
60
|
+
*/
|
|
61
|
+
export async function analyzeCommit(commit, projectName, apiKey) {
|
|
62
|
+
const prompt = `
|
|
63
|
+
당신은 시니어 소프트웨어 엔지니어입니다. 아래 git 커밋 정보를 분석하여 **반드시 한국어로** 상세한 문서와 코드 리뷰를 작성하세요.
|
|
64
|
+
|
|
65
|
+
## 프로젝트 정보
|
|
66
|
+
- 프로젝트명: ${projectName}
|
|
67
|
+
- 커밋 해시: ${commit.shortHash}
|
|
68
|
+
- 커밋 메시지: ${commit.message}
|
|
69
|
+
- 작성자: ${commit.author} (${commit.email})
|
|
70
|
+
- 날짜: ${commit.date}
|
|
71
|
+
|
|
72
|
+
## 변경 파일 요약 (diff --stat)
|
|
73
|
+
\`\`\`
|
|
74
|
+
${commit.diffStat}
|
|
75
|
+
\`\`\`
|
|
76
|
+
|
|
77
|
+
## 코드 변경 내용 (diff)
|
|
78
|
+
\`\`\`diff
|
|
79
|
+
${commit.diffContent}
|
|
80
|
+
\`\`\`
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
위 커밋을 분석하여 아래 형식으로 **정확히** 작성하세요. 각 섹션은 "##"로 시작합니다.
|
|
85
|
+
|
|
86
|
+
## 의도
|
|
87
|
+
이 커밋이 작성된 목적과 배경을 설명합니다. (왜 이 변경이 필요했는가?)
|
|
88
|
+
|
|
89
|
+
## 근거
|
|
90
|
+
이 구현 방식이나 접근법을 선택한 이유와 기술적 배경을 설명합니다.
|
|
91
|
+
|
|
92
|
+
## 작성한 코드
|
|
93
|
+
변경된 주요 코드를 파일별로 정리하고, 핵심 코드 블록을 인용하여 설명합니다.
|
|
94
|
+
|
|
95
|
+
## 코드 기능
|
|
96
|
+
각 코드가 실제로 어떤 동작을 수행하는지 사용자 관점에서 기능을 설명합니다.
|
|
97
|
+
|
|
98
|
+
## 코드 리뷰: 예외 처리
|
|
99
|
+
현재 코드에서 예외 처리가 부족하거나 개선이 필요한 부분을 구체적으로 지적하고, 개선된 코드 예시를 제시합니다.
|
|
100
|
+
|
|
101
|
+
## 코드 리뷰: 성능 개선
|
|
102
|
+
현재 코드에서 성능 관점에서 개선 가능한 부분을 구체적으로 지적하고, 개선 방향을 제안합니다.
|
|
103
|
+
|
|
104
|
+
## 코드 리뷰: 코드 품질 개선
|
|
105
|
+
가독성, 유지보수성, 네이밍, 구조 등 코드 품질 측면에서 개선할 수 있는 부분을 제안합니다.
|
|
106
|
+
|
|
107
|
+
모든 내용은 **한국어**로 작성하고, 코드 예시는 적절한 Markdown 코드 블록으로 감싸주세요.
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
return generateWithRetry(apiKey, prompt);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 현재 git status (작업 중인 변경사항)를 Gemini AI로 분석합니다.
|
|
115
|
+
*/
|
|
116
|
+
export async function analyzeWorkingStatus(status, projectName, apiKey) {
|
|
117
|
+
const prompt = `
|
|
118
|
+
당신은 시니어 소프트웨어 엔지니어입니다. 아래는 현재 작업 중인 \`git status\` 변경사항입니다. **반드시 한국어로** 상세한 분석과 코드 리뷰를 작성하세요.
|
|
119
|
+
|
|
120
|
+
## 프로젝트 정보
|
|
121
|
+
- 프로젝트명: ${projectName}
|
|
122
|
+
- 분석 모드: 현재 작업 중인 변경사항 (미커밋)
|
|
123
|
+
- 변경된 파일 수: ${status.totalFiles}개
|
|
124
|
+
- Staged: ${status.stagedCount}개
|
|
125
|
+
- Modified (unstaged): ${status.modifiedCount}개
|
|
126
|
+
- Deleted: ${status.deletedCount}개
|
|
127
|
+
- Untracked (신규): ${status.untrackedCount}개
|
|
128
|
+
|
|
129
|
+
## git status
|
|
130
|
+
\`\`\`
|
|
131
|
+
${status.statusText}
|
|
132
|
+
\`\`\`
|
|
133
|
+
|
|
134
|
+
## 코드 변경 내용
|
|
135
|
+
\`\`\`diff
|
|
136
|
+
${status.diffContent}
|
|
137
|
+
\`\`\`
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
위 변경사항을 분석하여 아래 형식으로 **정확히** 작성하세요. 각 섹션은 "##"로 시작합니다.
|
|
142
|
+
|
|
143
|
+
## 의도
|
|
144
|
+
현재 작업 중인 내용이 무엇인지, 어떤 기능/버그픽스/리팩토링으로 보이는지 설명합니다.
|
|
145
|
+
|
|
146
|
+
## 근거
|
|
147
|
+
이 구현 방식이나 접근법을 선택한 이유와 기술적 배경을 추론하여 설명합니다.
|
|
148
|
+
|
|
149
|
+
## 작성한 코드
|
|
150
|
+
변경된 주요 코드를 파일별로 정리하고, 핵심 코드 블록을 인용하여 설명합니다.
|
|
151
|
+
|
|
152
|
+
## 코드 기능
|
|
153
|
+
각 코드가 실제로 어떤 동작을 수행하는지 사용자 관점에서 기능을 설명합니다.
|
|
154
|
+
|
|
155
|
+
## 코드 리뷰: 예외 처리
|
|
156
|
+
현재 코드에서 예외 처리가 부족하거나 개선이 필요한 부분을 구체적으로 지적하고, 개선된 코드 예시를 제시합니다.
|
|
157
|
+
|
|
158
|
+
## 코드 리뷰: 성능 개선
|
|
159
|
+
현재 코드에서 성능 관점에서 개선 가능한 부분을 구체적으로 지적하고, 개선 방향을 제안합니다.
|
|
160
|
+
|
|
161
|
+
## 코드 리뷰: 코드 품질 개선
|
|
162
|
+
가독성, 유지보수성, 네이밍, 구조 등 코드 품질 측면에서 개선할 수 있는 부분을 제안합니다.
|
|
163
|
+
|
|
164
|
+
## 커밋 메시지 제안
|
|
165
|
+
현재 변경사항을 바탕으로 적절한 커밋 메시지를 Conventional Commits 형식으로 3가지 제안합니다.
|
|
166
|
+
예시: \`feat: 사용자 로그인 기능 추가\`, \`fix: 입력값 검증 로직 수정\`
|
|
167
|
+
|
|
168
|
+
모든 내용은 **한국어**로 작성하고, 코드 예시는 적절한 Markdown 코드 블록으로 감싸주세요.
|
|
169
|
+
`;
|
|
170
|
+
|
|
171
|
+
return generateWithRetry(apiKey, prompt);
|
|
172
|
+
}
|
|
173
|
+
|
package/src/git.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import simpleGit from 'simple-git';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* c:\dev 하위의 git 프로젝트 목록을 반환합니다.
|
|
7
|
+
*/
|
|
8
|
+
export async function listGitProjects(devRoot) {
|
|
9
|
+
const entries = fs.readdirSync(devRoot, { withFileTypes: true });
|
|
10
|
+
const projects = [];
|
|
11
|
+
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
if (!entry.isDirectory()) continue;
|
|
14
|
+
const fullPath = path.join(devRoot, entry.name);
|
|
15
|
+
const gitDir = path.join(fullPath, '.git');
|
|
16
|
+
if (fs.existsSync(gitDir)) {
|
|
17
|
+
projects.push({ name: entry.name, path: fullPath });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return projects;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 특정 프로젝트의 최신 커밋 정보와 diff를 가져옵니다.
|
|
26
|
+
*/
|
|
27
|
+
export async function getLatestCommit(projectPath) {
|
|
28
|
+
const git = simpleGit(projectPath);
|
|
29
|
+
|
|
30
|
+
// 최신 커밋 메타데이터
|
|
31
|
+
const log = await git.log({ maxCount: 1 });
|
|
32
|
+
if (!log.latest) {
|
|
33
|
+
throw new Error('커밋 기록이 없습니다.');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { hash, message, author_name, author_email, date } = log.latest;
|
|
37
|
+
|
|
38
|
+
// 이전 커밋과의 diff (파일 목록)
|
|
39
|
+
let diffStat = '';
|
|
40
|
+
let diffContent = '';
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// 부모 커밋이 있는지 확인
|
|
44
|
+
const parentCount = await git.raw(['rev-list', '--count', 'HEAD']);
|
|
45
|
+
const count = parseInt(parentCount.trim(), 10);
|
|
46
|
+
|
|
47
|
+
if (count > 1) {
|
|
48
|
+
diffStat = await git.raw(['diff', '--stat', 'HEAD~1', 'HEAD']);
|
|
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');
|
|
53
|
+
if (lines.length > 300) {
|
|
54
|
+
diffContent += '\n... (이하 생략, 너무 긴 diff)';
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
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');
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {
|
|
64
|
+
diffContent = '(diff를 가져올 수 없습니다)';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
hash,
|
|
69
|
+
shortHash: hash.slice(0, 7),
|
|
70
|
+
message,
|
|
71
|
+
author: author_name,
|
|
72
|
+
email: author_email,
|
|
73
|
+
date: new Date(date).toLocaleString('ko-KR'),
|
|
74
|
+
diffStat,
|
|
75
|
+
diffContent,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 현재 Working Directory의 변경사항 (git status + diff)을 가져옵니다.
|
|
81
|
+
* staged + unstaged 변경사항 모두 포함합니다.
|
|
82
|
+
*/
|
|
83
|
+
export async function getWorkingStatus(projectPath) {
|
|
84
|
+
const git = simpleGit(projectPath);
|
|
85
|
+
|
|
86
|
+
// git status --short 로 파일 목록
|
|
87
|
+
const statusSummary = await git.status();
|
|
88
|
+
const { files, staged, modified, not_added, deleted, renamed } = statusSummary;
|
|
89
|
+
|
|
90
|
+
if (files.length === 0) {
|
|
91
|
+
return null; // 변경사항 없음
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 상태 텍스트 구성
|
|
95
|
+
const statusLines = [];
|
|
96
|
+
for (const f of files) {
|
|
97
|
+
statusLines.push(`${f.index}${f.working_dir} ${f.path}`);
|
|
98
|
+
}
|
|
99
|
+
const statusText = statusLines.join('\n');
|
|
100
|
+
|
|
101
|
+
// staged diff (git diff --cached)
|
|
102
|
+
let stagedDiff = '';
|
|
103
|
+
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... (이하 생략)';
|
|
108
|
+
} catch {}
|
|
109
|
+
|
|
110
|
+
// unstaged diff (git diff)
|
|
111
|
+
let unstagedDiff = '';
|
|
112
|
+
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... (이하 생략)';
|
|
117
|
+
} catch {}
|
|
118
|
+
|
|
119
|
+
// untracked 파일 내용 (최대 3개)
|
|
120
|
+
let untrackedContent = '';
|
|
121
|
+
const untrackedFiles = files.filter(f => f.index === '?' && f.working_dir === '?').slice(0, 3);
|
|
122
|
+
for (const f of untrackedFiles) {
|
|
123
|
+
try {
|
|
124
|
+
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');
|
|
127
|
+
untrackedContent += `\n--- ${f.path} (신규 파일) ---\n${lines}\n`;
|
|
128
|
+
} catch {}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const hasStagedChanges = staged.length > 0 || renamed.length > 0;
|
|
132
|
+
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');
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
statusText,
|
|
140
|
+
stagedCount: staged.length + renamed.length,
|
|
141
|
+
modifiedCount: modified.length,
|
|
142
|
+
deletedCount: deleted.length,
|
|
143
|
+
untrackedCount: not_added.length,
|
|
144
|
+
totalFiles: files.length,
|
|
145
|
+
hasStagedChanges,
|
|
146
|
+
diffContent: diffContent || '(diff 내용 없음)',
|
|
147
|
+
diffStat: statusText,
|
|
148
|
+
};
|
|
149
|
+
}
|