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 CHANGED
@@ -1,4 +1,2 @@
1
1
  GEMINI_API_KEY=your_gemini_api_key_here
2
2
  PORT=3000
3
- # Optional: 미설정 시 실행 디렉토리(process.cwd())를 자동 사용
4
- # DEV_ROOT=C:/dev
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commit-ai-agent",
3
- "version": "1.0.9",
3
+ "version": "2.0.0",
4
4
  "description": "AI-powered git commit & working status analyzer with web UI",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
package/public/app.js CHANGED
@@ -1,11 +1,10 @@
1
1
  /* global marked */
2
2
 
3
3
  // ── State ──
4
- let selectedProject = null;
4
+ let selectedProject = "__self__";
5
5
  let isAnalyzing = false;
6
- let analyzeMode = "commit"; // 'commit' | 'status'
7
- let isSingleProject = false;
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 (isSingleProject) {
75
- await enterSingleProjectMode();
73
+ if (analyzeMode === "commit") {
74
+ await fetchCommitPreview();
76
75
  } else {
77
- await loadProjects();
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
- if (data.isSingleProject) {
117
- isSingleProject = true;
118
- singleProjectName = data.singleProjectName || "project";
119
- }
110
+ projectName = data.projectName || "project";
120
111
  } catch {}
121
112
  }
122
113
 
123
- // ── Single Project Mode ──
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: displayName });
124
+ setAriaState("ready-commit", { project: projectName });
225
125
  } catch (e) {
226
126
  console.warn("commit preview failed:", e.message);
227
- setAriaState("ready-commit", { project: displayName });
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: displayName,
142
+ project: projectName,
244
143
  n: status.totalFiles,
245
144
  });
246
145
  } else {
247
- const hint = document.getElementById("selected-hint");
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: displayName, n: 0 });
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 || !selectedProject) return;
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(); // SSE가 끊겨도 완료를 폴링으로 감지
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(projectName) {
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", `${projectName} 커밋 자동 분석 중...`);
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, projectName, btn) {
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
- >어떤 프로젝트의 커밋을 분석해드릴까요?</span
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
- <!-- Project Selector Card -->
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
- <span class="selected-hint" id="selected-hint"
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(targetPath);
8
+ stat = fs.statSync(devRoot);
25
9
  } catch {
26
- throw new Error(`${sourceLabel} 경로를 찾을 수 없습니다: ${targetPath}`);
10
+ throw new Error(`경로를 찾을 수 없습니다: ${devRoot}`);
27
11
  }
28
-
29
12
  if (!stat.isDirectory()) {
30
- throw new Error(`${sourceLabel} 경로가 디렉토리가 아닙니다: ${targetPath}`);
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 { listGitProjects, getLatestCommit, getWorkingStatus } from "./git.js";
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 { devRoot: DEV_ROOT, source: DEV_ROOT_SOURCE } = resolveDevRoot();
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
- const trimmedName = projectName.trim();
51
- if (!trimmedName) {
52
- throw createBadRequestError("프로젝트명이 비어 있습니다.");
50
+ if (projectName.trim() !== "__self__") {
51
+ throw createBadRequestError("유효하지 않은 프로젝트명입니다.");
53
52
  }
54
53
 
55
- // Single-project mode: "__self__" = DEV_ROOT 자체 (DEV_ROOT git 저장소인 경우)
56
- if (trimmedName === "__self__") {
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 projectPath;
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
- // DEV_ROOT 내부인지 검증
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 = isSelf ? path.basename(DEV_ROOT) : path.basename(projectPath);
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
- devRoot: DEV_ROOT,
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
  });