commit-ai-agent 1.0.9 → 2.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 +0 -2
- package/package.json +1 -1
- package/public/app.js +26 -131
- package/public/index.html +3 -19
- package/src/config.js +4 -20
- package/src/git.js +0 -19
- package/src/server.js +11 -60
package/.env.example
CHANGED
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/* global marked */
|
|
2
2
|
|
|
3
3
|
// ── State ──
|
|
4
|
-
let selectedProject =
|
|
4
|
+
let selectedProject = "__self__";
|
|
5
5
|
let isAnalyzing = false;
|
|
6
|
-
let analyzeMode = "commit";
|
|
7
|
-
let
|
|
8
|
-
let singleProjectName = "";
|
|
6
|
+
let analyzeMode = "commit";
|
|
7
|
+
let projectName = "";
|
|
9
8
|
|
|
10
9
|
// ── Aria State Machine ──
|
|
11
10
|
function setAriaState(state, opts = {}) {
|
|
@@ -37,7 +36,7 @@ function setAriaState(state, opts = {}) {
|
|
|
37
36
|
// Bubble messages
|
|
38
37
|
const p = opts.project ? `<strong>${opts.project}</strong>` : "";
|
|
39
38
|
const msgMap = {
|
|
40
|
-
idle: "
|
|
39
|
+
idle: "분석을 시작할까요?",
|
|
41
40
|
"ready-commit": `${p} 최근 커밋을 확인했어요. 분석을 시작할까요? 👀`,
|
|
42
41
|
"ready-status":
|
|
43
42
|
opts.n > 0
|
|
@@ -71,20 +70,15 @@ async function init() {
|
|
|
71
70
|
setupModeToggle();
|
|
72
71
|
await checkConfig();
|
|
73
72
|
|
|
74
|
-
if (
|
|
75
|
-
await
|
|
73
|
+
if (analyzeMode === "commit") {
|
|
74
|
+
await fetchCommitPreview();
|
|
76
75
|
} else {
|
|
77
|
-
await
|
|
78
|
-
setAriaState("idle");
|
|
76
|
+
await fetchStatusPreview();
|
|
79
77
|
}
|
|
80
78
|
|
|
81
79
|
// SSE: post-commit 자동 분석 이벤트 수신
|
|
82
80
|
connectAutoAnalysisEvents();
|
|
83
81
|
|
|
84
|
-
// wire static event listeners (elements guaranteed to exist now)
|
|
85
|
-
document
|
|
86
|
-
.getElementById("refresh-projects")
|
|
87
|
-
.addEventListener("click", loadProjects);
|
|
88
82
|
document
|
|
89
83
|
.getElementById("analyze-btn")
|
|
90
84
|
.addEventListener("click", onAnalyzeClick);
|
|
@@ -113,106 +107,12 @@ async function checkConfig() {
|
|
|
113
107
|
if (!data.hasKey) {
|
|
114
108
|
document.getElementById("api-key-warn").style.display = "flex";
|
|
115
109
|
}
|
|
116
|
-
|
|
117
|
-
isSingleProject = true;
|
|
118
|
-
singleProjectName = data.singleProjectName || "project";
|
|
119
|
-
}
|
|
110
|
+
projectName = data.projectName || "project";
|
|
120
111
|
} catch {}
|
|
121
112
|
}
|
|
122
113
|
|
|
123
|
-
// ──
|
|
124
|
-
async function enterSingleProjectMode() {
|
|
125
|
-
// 프로젝트 선택 UI만 숨김 (모드 토글은 유지)
|
|
126
|
-
const header = document.querySelector(".selector-card .card-header");
|
|
127
|
-
const projectGrid = document.getElementById("project-grid");
|
|
128
|
-
const selectedHint = document.getElementById("selected-hint");
|
|
129
|
-
if (header) header.style.display = "none";
|
|
130
|
-
if (projectGrid) projectGrid.style.display = "none";
|
|
131
|
-
if (selectedHint) selectedHint.style.display = "none";
|
|
132
|
-
|
|
133
|
-
// 현재 디렉토리를 프로젝트로 자동 선택
|
|
134
|
-
selectedProject = "__self__";
|
|
135
|
-
|
|
136
|
-
document.getElementById("analyze-btn").disabled = false;
|
|
137
|
-
const btnText = document.getElementById("analyze-btn-text");
|
|
138
|
-
if (btnText) btnText.textContent = "Hanni에게 분석 요청";
|
|
139
|
-
|
|
140
|
-
// 현재 모드에 따라 미리 로드
|
|
141
|
-
if (analyzeMode === "commit") {
|
|
142
|
-
await fetchCommitPreview();
|
|
143
|
-
} else {
|
|
144
|
-
await fetchStatusPreview();
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ── Projects ──
|
|
149
|
-
async function loadProjects() {
|
|
150
|
-
const grid = document.getElementById("project-grid");
|
|
151
|
-
grid.innerHTML =
|
|
152
|
-
'<div class="skeleton-grid"><div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div></div>';
|
|
153
|
-
try {
|
|
154
|
-
const res = await fetch("/api/projects");
|
|
155
|
-
const { projects } = await res.json();
|
|
156
|
-
renderProjects(projects);
|
|
157
|
-
} catch (err) {
|
|
158
|
-
grid.innerHTML = `<p style="color:var(--danger);font-size:14px">프로젝트 목록을 불러오지 못했습니다: ${err.message}</p>`;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function renderProjects(projects) {
|
|
163
|
-
const grid = document.getElementById("project-grid");
|
|
164
|
-
if (!projects || projects.length === 0) {
|
|
165
|
-
grid.innerHTML =
|
|
166
|
-
'<p style="color:var(--text3);font-size:14px">git 프로젝트가 없습니다.</p>';
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
grid.innerHTML = projects
|
|
170
|
-
.map(
|
|
171
|
-
(p) => `
|
|
172
|
-
<div class="project-item" data-name="${p.name}">
|
|
173
|
-
<span class="proj-icon">${getProjectIcon(p.name)}</span>
|
|
174
|
-
<span class="proj-name">${p.name}</span>
|
|
175
|
-
</div>
|
|
176
|
-
`,
|
|
177
|
-
)
|
|
178
|
-
.join("");
|
|
179
|
-
grid.querySelectorAll(".project-item").forEach((el) => {
|
|
180
|
-
el.addEventListener("click", () => selectProject(el));
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function getProjectIcon(name) {
|
|
185
|
-
if (name.includes("next") || name.includes("react")) return "⚛️";
|
|
186
|
-
if (name.includes("nest") || name.includes("api")) return "🐉";
|
|
187
|
-
if (name.includes("hook")) return "🪝";
|
|
188
|
-
if (name.includes("portfolio")) return "🎨";
|
|
189
|
-
if (name.includes("todo")) return "✅";
|
|
190
|
-
if (name.includes("doc")) return "📚";
|
|
191
|
-
return "📁";
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// ── Project Selection ──
|
|
195
|
-
async function selectProject(el) {
|
|
196
|
-
document
|
|
197
|
-
.querySelectorAll(".project-item")
|
|
198
|
-
.forEach((e) => e.classList.remove("selected"));
|
|
199
|
-
el.classList.add("selected");
|
|
200
|
-
selectedProject = el.dataset.name;
|
|
201
|
-
document.getElementById("selected-hint").textContent =
|
|
202
|
-
`선택됨: ${selectedProject}`;
|
|
203
|
-
document.getElementById("analyze-btn").disabled = false;
|
|
204
|
-
document.getElementById("commit-card").style.display = "none";
|
|
205
|
-
document.getElementById("status-card").style.display = "none";
|
|
206
|
-
|
|
207
|
-
if (analyzeMode === "commit") {
|
|
208
|
-
await fetchCommitPreview();
|
|
209
|
-
} else {
|
|
210
|
-
await fetchStatusPreview();
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
114
|
+
// ── Preview loading ──
|
|
214
115
|
async function fetchCommitPreview() {
|
|
215
|
-
const displayName = isSingleProject ? singleProjectName : selectedProject;
|
|
216
116
|
try {
|
|
217
117
|
const res = await fetch(
|
|
218
118
|
`/api/projects/${encodeURIComponent(selectedProject)}/commit`,
|
|
@@ -221,15 +121,14 @@ async function fetchCommitPreview() {
|
|
|
221
121
|
if (error) throw new Error(error);
|
|
222
122
|
renderCommitCard(commit);
|
|
223
123
|
document.getElementById("commit-card").style.display = "block";
|
|
224
|
-
setAriaState("ready-commit", { project:
|
|
124
|
+
setAriaState("ready-commit", { project: projectName });
|
|
225
125
|
} catch (e) {
|
|
226
126
|
console.warn("commit preview failed:", e.message);
|
|
227
|
-
setAriaState("ready-commit", { project:
|
|
127
|
+
setAriaState("ready-commit", { project: projectName });
|
|
228
128
|
}
|
|
229
129
|
}
|
|
230
130
|
|
|
231
131
|
async function fetchStatusPreview() {
|
|
232
|
-
const displayName = isSingleProject ? singleProjectName : selectedProject;
|
|
233
132
|
try {
|
|
234
133
|
const res = await fetch(
|
|
235
134
|
`/api/projects/${encodeURIComponent(selectedProject)}/status`,
|
|
@@ -240,17 +139,15 @@ async function fetchStatusPreview() {
|
|
|
240
139
|
renderStatusCard(status);
|
|
241
140
|
document.getElementById("status-card").style.display = "block";
|
|
242
141
|
setAriaState("ready-status", {
|
|
243
|
-
project:
|
|
142
|
+
project: projectName,
|
|
244
143
|
n: status.totalFiles,
|
|
245
144
|
});
|
|
246
145
|
} else {
|
|
247
|
-
|
|
248
|
-
if (hint) hint.textContent = `${displayName} — 변경사항 없음`;
|
|
249
|
-
setAriaState("ready-status", { project: displayName, n: 0 });
|
|
146
|
+
setAriaState("ready-status", { project: projectName, n: 0 });
|
|
250
147
|
}
|
|
251
148
|
} catch (e) {
|
|
252
149
|
console.warn("status preview failed:", e.message);
|
|
253
|
-
setAriaState("ready-status", { project:
|
|
150
|
+
setAriaState("ready-status", { project: projectName, n: 0 });
|
|
254
151
|
}
|
|
255
152
|
}
|
|
256
153
|
|
|
@@ -324,7 +221,6 @@ function switchMode(mode) {
|
|
|
324
221
|
btnText.textContent =
|
|
325
222
|
mode === "commit" ? "Hanni에게 분석 요청" : "Hanni에게 리뷰 요청";
|
|
326
223
|
|
|
327
|
-
if (!selectedProject) return;
|
|
328
224
|
if (mode === "commit") fetchCommitPreview();
|
|
329
225
|
else fetchStatusPreview();
|
|
330
226
|
}
|
|
@@ -337,7 +233,7 @@ function onAnalyzeClick() {
|
|
|
337
233
|
|
|
338
234
|
// ── Generic SSE Analysis ──
|
|
339
235
|
async function startAnalysis(endpoint) {
|
|
340
|
-
if (isAnalyzing
|
|
236
|
+
if (isAnalyzing) return;
|
|
341
237
|
isAnalyzing = true;
|
|
342
238
|
|
|
343
239
|
const resultCard = document.getElementById("result-card");
|
|
@@ -349,7 +245,7 @@ async function startAnalysis(endpoint) {
|
|
|
349
245
|
resultCard.style.display = "block";
|
|
350
246
|
analysisBody.innerHTML = "";
|
|
351
247
|
reportSaved.textContent = "";
|
|
352
|
-
document.getElementById("copy-btn").style.display = "none";
|
|
248
|
+
document.getElementById("copy-btn").style.display = "none";
|
|
353
249
|
setStatus("loading", "Hanni가 코드를 살펴보고 있어요...");
|
|
354
250
|
setAriaState("thinking");
|
|
355
251
|
analyzeBtn.disabled = true;
|
|
@@ -404,7 +300,7 @@ async function startAnalysis(endpoint) {
|
|
|
404
300
|
} else if (data.type === "done") {
|
|
405
301
|
setStatus("done", "✅ 분석 완료!");
|
|
406
302
|
setAriaState("done");
|
|
407
|
-
document.getElementById("copy-btn").style.display = "inline-flex";
|
|
303
|
+
document.getElementById("copy-btn").style.display = "inline-flex";
|
|
408
304
|
} else if (data.type === "error") {
|
|
409
305
|
setStatus("error", `오류: ${data.message}`);
|
|
410
306
|
setAriaState("error");
|
|
@@ -448,7 +344,7 @@ function onCopy() {
|
|
|
448
344
|
|
|
449
345
|
// ── Auto Analysis via SSE (+ polling fallback) ──
|
|
450
346
|
let autoAnalysisPollTimer = null;
|
|
451
|
-
let autoAnalysisShownFilename = null;
|
|
347
|
+
let autoAnalysisShownFilename = null;
|
|
452
348
|
|
|
453
349
|
function connectAutoAnalysisEvents() {
|
|
454
350
|
const evtSource = new EventSource("/api/events");
|
|
@@ -469,10 +365,10 @@ function connectAutoAnalysisEvents() {
|
|
|
469
365
|
}
|
|
470
366
|
|
|
471
367
|
function handleAutoAnalysisEvent(data) {
|
|
472
|
-
if (isAnalyzing) return;
|
|
368
|
+
if (isAnalyzing) return;
|
|
473
369
|
if (data.type === "analysis-started") {
|
|
474
370
|
showAutoAnalysisStarted(data.projectName);
|
|
475
|
-
startAutoAnalysisPoll();
|
|
371
|
+
startAutoAnalysisPoll();
|
|
476
372
|
} else if (data.type === "analysis-done") {
|
|
477
373
|
stopAutoAnalysisPoll();
|
|
478
374
|
if (data.filename !== autoAnalysisShownFilename) {
|
|
@@ -486,7 +382,6 @@ function handleAutoAnalysisEvent(data) {
|
|
|
486
382
|
}
|
|
487
383
|
}
|
|
488
384
|
|
|
489
|
-
// 5초마다 서버 상태 폴링 (SSE 대비 폴백)
|
|
490
385
|
function startAutoAnalysisPoll() {
|
|
491
386
|
stopAutoAnalysisPoll();
|
|
492
387
|
autoAnalysisPollTimer = setInterval(async () => {
|
|
@@ -512,7 +407,7 @@ function stopAutoAnalysisPoll() {
|
|
|
512
407
|
}
|
|
513
408
|
}
|
|
514
409
|
|
|
515
|
-
function showAutoAnalysisStarted(
|
|
410
|
+
function showAutoAnalysisStarted(pName) {
|
|
516
411
|
const resultCard = document.getElementById("result-card");
|
|
517
412
|
const analysisBody = document.getElementById("analysis-body");
|
|
518
413
|
const reportSaved = document.getElementById("report-saved");
|
|
@@ -520,7 +415,7 @@ function showAutoAnalysisStarted(projectName) {
|
|
|
520
415
|
analysisBody.innerHTML = "";
|
|
521
416
|
reportSaved.textContent = "";
|
|
522
417
|
document.getElementById("copy-btn").style.display = "none";
|
|
523
|
-
setStatus("loading", `${
|
|
418
|
+
setStatus("loading", `${pName} 커밋 자동 분석 중...`);
|
|
524
419
|
setAriaState("thinking");
|
|
525
420
|
resultCard.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
526
421
|
}
|
|
@@ -530,7 +425,7 @@ function showAutoAnalysisDone({ filename, content }) {
|
|
|
530
425
|
const analysisBody = document.getElementById("analysis-body");
|
|
531
426
|
const reportSaved = document.getElementById("report-saved");
|
|
532
427
|
const copyBtn = document.getElementById("copy-btn");
|
|
533
|
-
resultCard.style.display = "block";
|
|
428
|
+
resultCard.style.display = "block";
|
|
534
429
|
analysisBody.innerHTML = marked.parse(content);
|
|
535
430
|
reportSaved.textContent = `✓ 저장됨: ${filename}`;
|
|
536
431
|
setStatus("done", "✅ 자동 분석 완료!");
|
|
@@ -665,7 +560,7 @@ async function loadHookStatus() {
|
|
|
665
560
|
}
|
|
666
561
|
}
|
|
667
562
|
|
|
668
|
-
async function handleHookAction(action,
|
|
563
|
+
async function handleHookAction(action, pName, btn) {
|
|
669
564
|
const original = btn.textContent;
|
|
670
565
|
btn.disabled = true;
|
|
671
566
|
btn.textContent = action === "install" ? "설치 중..." : "제거 중...";
|
|
@@ -673,11 +568,11 @@ async function handleHookAction(action, projectName, btn) {
|
|
|
673
568
|
const res = await fetch(`/api/hooks/${action}`, {
|
|
674
569
|
method: "POST",
|
|
675
570
|
headers: { "Content-Type": "application/json" },
|
|
676
|
-
body: JSON.stringify({ projectName }),
|
|
571
|
+
body: JSON.stringify({ projectName: pName }),
|
|
677
572
|
});
|
|
678
573
|
const data = await res.json();
|
|
679
574
|
if (!res.ok) throw new Error(data.error || "요청 실패");
|
|
680
|
-
await loadHookStatus();
|
|
575
|
+
await loadHookStatus();
|
|
681
576
|
} catch (err) {
|
|
682
577
|
btn.disabled = false;
|
|
683
578
|
btn.textContent = original;
|
package/public/index.html
CHANGED
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
<div class="aria-bubble-wrap">
|
|
157
157
|
<div class="aria-bubble">
|
|
158
158
|
<span class="aria-bubble-text" id="aria-bubble-text"
|
|
159
|
-
|
|
159
|
+
>분석을 시작할까요?</span
|
|
160
160
|
>
|
|
161
161
|
<span
|
|
162
162
|
class="aria-typing-dots"
|
|
@@ -177,14 +177,8 @@
|
|
|
177
177
|
키를 입력해 주세요.
|
|
178
178
|
</div>
|
|
179
179
|
|
|
180
|
-
<!--
|
|
180
|
+
<!-- Selector Card -->
|
|
181
181
|
<div class="card selector-card">
|
|
182
|
-
<div class="card-header">
|
|
183
|
-
<h2 class="card-title">🗂 프로젝트 선택</h2>
|
|
184
|
-
<button class="refresh-btn" id="refresh-projects" title="새로고침">
|
|
185
|
-
↻
|
|
186
|
-
</button>
|
|
187
|
-
</div>
|
|
188
182
|
<!-- Mode Toggle -->
|
|
189
183
|
<div class="mode-toggle">
|
|
190
184
|
<button class="mode-btn active" id="mode-commit" data-mode="commit">
|
|
@@ -194,18 +188,8 @@
|
|
|
194
188
|
🔍 현재 변경사항 분석
|
|
195
189
|
</button>
|
|
196
190
|
</div>
|
|
197
|
-
<div class="project-grid" id="project-grid">
|
|
198
|
-
<div class="skeleton-grid">
|
|
199
|
-
<div class="skeleton"></div>
|
|
200
|
-
<div class="skeleton"></div>
|
|
201
|
-
<div class="skeleton"></div>
|
|
202
|
-
</div>
|
|
203
|
-
</div>
|
|
204
191
|
<div class="card-footer">
|
|
205
|
-
<
|
|
206
|
-
>프로젝트를 선택해 주세요</span
|
|
207
|
-
>
|
|
208
|
-
<button class="btn-primary" id="analyze-btn" disabled>
|
|
192
|
+
<button class="btn-primary" id="analyze-btn">
|
|
209
193
|
<span class="btn-icon">🤖</span>
|
|
210
194
|
<span id="analyze-btn-text">Hanni에게 분석 요청</span>
|
|
211
195
|
</button>
|
package/src/config.js
CHANGED
|
@@ -2,31 +2,15 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
|
|
4
4
|
export function resolveDevRoot() {
|
|
5
|
-
const fromEnv = process.env.DEV_ROOT?.trim();
|
|
6
|
-
if (fromEnv) {
|
|
7
|
-
const devRoot = path.resolve(fromEnv);
|
|
8
|
-
validateDirectory(devRoot, "DEV_ROOT");
|
|
9
|
-
return { devRoot, source: "env" };
|
|
10
|
-
}
|
|
11
|
-
|
|
12
5
|
const devRoot = path.resolve(process.cwd());
|
|
13
|
-
validateDirectory(devRoot, "cwd");
|
|
14
|
-
return { devRoot, source: "cwd" };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function validateDirectory(targetPath, sourceLabel) {
|
|
18
|
-
if (!targetPath) {
|
|
19
|
-
throw new Error(`${sourceLabel} 경로가 비어 있습니다.`);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
6
|
let stat;
|
|
23
7
|
try {
|
|
24
|
-
stat = fs.statSync(
|
|
8
|
+
stat = fs.statSync(devRoot);
|
|
25
9
|
} catch {
|
|
26
|
-
throw new Error(
|
|
10
|
+
throw new Error(`경로를 찾을 수 없습니다: ${devRoot}`);
|
|
27
11
|
}
|
|
28
|
-
|
|
29
12
|
if (!stat.isDirectory()) {
|
|
30
|
-
throw new Error(
|
|
13
|
+
throw new Error(`경로가 디렉토리가 아닙니다: ${devRoot}`);
|
|
31
14
|
}
|
|
15
|
+
return devRoot;
|
|
32
16
|
}
|
package/src/git.js
CHANGED
|
@@ -2,25 +2,6 @@ import simpleGit from "simple-git";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* DEV_ROOT 하위의 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
5
|
/**
|
|
25
6
|
* 특정 프로젝트의 최신 커밋 정보와 diff를 가져옵니다.
|
|
26
7
|
*/
|
package/src/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import express from "express";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import fs from "fs";
|
|
6
|
-
import {
|
|
6
|
+
import { getLatestCommit, getWorkingStatus } from "./git.js";
|
|
7
7
|
import { analyzeCommit, analyzeWorkingStatus } from "./analyzer.js";
|
|
8
8
|
import { resolveDevRoot } from "./config.js";
|
|
9
9
|
import { installHooks, removeHooks, getHookStatus } from "./hooks/installer.js";
|
|
@@ -12,7 +12,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
12
12
|
const app = express();
|
|
13
13
|
|
|
14
14
|
const PORT = process.env.PORT || 50324;
|
|
15
|
-
const
|
|
15
|
+
const DEV_ROOT = resolveDevRoot();
|
|
16
16
|
const BAD_REQUEST_PREFIX = "[BAD_REQUEST]";
|
|
17
17
|
|
|
18
18
|
// npx/global install 시: COMMIT_ANALYZER_ROOT = bin/cli.js가 설정한 패키지 루트
|
|
@@ -47,37 +47,15 @@ function resolveProjectPath(projectName) {
|
|
|
47
47
|
throw createBadRequestError("프로젝트명이 필요합니다.");
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
throw createBadRequestError("프로젝트명이 비어 있습니다.");
|
|
50
|
+
if (projectName.trim() !== "__self__") {
|
|
51
|
+
throw createBadRequestError("유효하지 않은 프로젝트명입니다.");
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (!fs.existsSync(path.join(DEV_ROOT, ".git"))) {
|
|
58
|
-
throw createBadRequestError("현재 디렉토리가 Git 저장소가 아닙니다.");
|
|
59
|
-
}
|
|
60
|
-
return DEV_ROOT;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const projectPath = path.resolve(DEV_ROOT, trimmedName);
|
|
64
|
-
const relativePath = path.relative(DEV_ROOT, projectPath);
|
|
65
|
-
const isOutsideRoot =
|
|
66
|
-
relativePath.startsWith("..") || path.isAbsolute(relativePath);
|
|
67
|
-
|
|
68
|
-
if (isOutsideRoot) {
|
|
69
|
-
throw createBadRequestError("유효하지 않은 프로젝트 경로입니다.");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) {
|
|
73
|
-
throw createBadRequestError("프로젝트를 찾을 수 없습니다.");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (!fs.existsSync(path.join(projectPath, ".git"))) {
|
|
77
|
-
throw createBadRequestError("Git 저장소가 아닌 프로젝트입니다.");
|
|
54
|
+
if (!fs.existsSync(path.join(DEV_ROOT, ".git"))) {
|
|
55
|
+
throw createBadRequestError("현재 디렉토리가 Git 저장소가 아닙니다.");
|
|
78
56
|
}
|
|
79
57
|
|
|
80
|
-
return
|
|
58
|
+
return DEV_ROOT;
|
|
81
59
|
}
|
|
82
60
|
|
|
83
61
|
// ──────────────────────────────────────────────
|
|
@@ -254,18 +232,11 @@ app.post("/api/hooks/post-commit-notify", (req, res) => {
|
|
|
254
232
|
return res.status(400).json({ error: "projectPath가 필요합니다." });
|
|
255
233
|
}
|
|
256
234
|
|
|
257
|
-
|
|
258
|
-
const relative = path.relative(DEV_ROOT, path.resolve(projectPath));
|
|
259
|
-
const isOutside = relative.startsWith("..") || path.isAbsolute(relative);
|
|
260
|
-
|
|
261
|
-
// single-project 모드 (DEV_ROOT 자체가 git repo)
|
|
262
|
-
const isSelf = path.resolve(projectPath) === path.resolve(DEV_ROOT);
|
|
263
|
-
|
|
264
|
-
if (isOutside && !isSelf) {
|
|
235
|
+
if (path.resolve(projectPath) !== path.resolve(DEV_ROOT)) {
|
|
265
236
|
return res.status(400).json({ error: "유효하지 않은 projectPath입니다." });
|
|
266
237
|
}
|
|
267
238
|
|
|
268
|
-
const projectName =
|
|
239
|
+
const projectName = path.basename(DEV_ROOT);
|
|
269
240
|
analysisQueue.push({ projectPath: path.resolve(projectPath), projectName });
|
|
270
241
|
|
|
271
242
|
// 즉시 응답 후 백그라운드에서 처리
|
|
@@ -275,36 +246,18 @@ app.post("/api/hooks/post-commit-notify", (req, res) => {
|
|
|
275
246
|
|
|
276
247
|
// ──────────────────────────────────────────────
|
|
277
248
|
// API: 설정 확인
|
|
278
|
-
|
|
279
249
|
// ──────────────────────────────────────────────
|
|
280
250
|
app.get("/api/config", (req, res) => {
|
|
281
251
|
const hasKey = !!(
|
|
282
252
|
process.env.GEMINI_API_KEY &&
|
|
283
253
|
process.env.GEMINI_API_KEY !== "your_gemini_api_key_here"
|
|
284
254
|
);
|
|
285
|
-
const isSingleProject =
|
|
286
|
-
DEV_ROOT_SOURCE === "cwd" && fs.existsSync(path.join(DEV_ROOT, ".git"));
|
|
287
255
|
res.json({
|
|
288
256
|
hasKey,
|
|
289
|
-
|
|
290
|
-
devRootSource: DEV_ROOT_SOURCE,
|
|
291
|
-
isSingleProject,
|
|
292
|
-
singleProjectName: isSingleProject ? path.basename(DEV_ROOT) : null,
|
|
257
|
+
projectName: path.basename(DEV_ROOT),
|
|
293
258
|
});
|
|
294
259
|
});
|
|
295
260
|
|
|
296
|
-
// ──────────────────────────────────────────────
|
|
297
|
-
// API: 프로젝트 목록
|
|
298
|
-
// ──────────────────────────────────────────────
|
|
299
|
-
app.get("/api/projects", async (req, res) => {
|
|
300
|
-
try {
|
|
301
|
-
const projects = await listGitProjects(DEV_ROOT);
|
|
302
|
-
res.json({ projects });
|
|
303
|
-
} catch (err) {
|
|
304
|
-
res.status(500).json({ error: err.message });
|
|
305
|
-
}
|
|
306
|
-
});
|
|
307
|
-
|
|
308
261
|
// ──────────────────────────────────────────────
|
|
309
262
|
// API: 최근 커밋 정보 조회
|
|
310
263
|
// ──────────────────────────────────────────────
|
|
@@ -561,7 +514,5 @@ ${analysis}
|
|
|
561
514
|
app.listen(PORT, () => {
|
|
562
515
|
console.log(`\n🚀 Commit Ai Agent 실행 중`);
|
|
563
516
|
console.log(` 브라우저: http://localhost:${PORT}`);
|
|
564
|
-
console.log(` 분석 대상: ${DEV_ROOT}`);
|
|
565
|
-
console.log(` DEV_ROOT source: ${DEV_ROOT_SOURCE}\n`);
|
|
566
|
-
|
|
517
|
+
console.log(` 분석 대상: ${DEV_ROOT}\n`);
|
|
567
518
|
});
|