commit-ai-agent 1.0.8 → 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/README.md +134 -18
- package/bin/cli.js +97 -1
- package/package.json +1 -1
- package/public/app.js +191 -121
- package/public/index.html +71 -24
- package/public/style.css +161 -0
- package/src/config.js +4 -20
- package/src/git.js +0 -19
- 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 +178 -45
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,17 +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
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
.addEventListener("click", loadProjects);
|
|
79
|
+
// SSE: post-commit 자동 분석 이벤트 수신
|
|
80
|
+
connectAutoAnalysisEvents();
|
|
81
|
+
|
|
85
82
|
document
|
|
86
83
|
.getElementById("analyze-btn")
|
|
87
84
|
.addEventListener("click", onAnalyzeClick);
|
|
@@ -97,6 +94,9 @@ async function init() {
|
|
|
97
94
|
.addEventListener("click", () => {
|
|
98
95
|
togglePre("status-diff-content", "status-diff-toggle-btn");
|
|
99
96
|
});
|
|
97
|
+
document
|
|
98
|
+
.getElementById("refresh-hooks")
|
|
99
|
+
.addEventListener("click", loadHookStatus);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
// ── Config check ──
|
|
@@ -107,106 +107,12 @@ async function checkConfig() {
|
|
|
107
107
|
if (!data.hasKey) {
|
|
108
108
|
document.getElementById("api-key-warn").style.display = "flex";
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
isSingleProject = true;
|
|
112
|
-
singleProjectName = data.singleProjectName || "project";
|
|
113
|
-
}
|
|
110
|
+
projectName = data.projectName || "project";
|
|
114
111
|
} catch {}
|
|
115
112
|
}
|
|
116
113
|
|
|
117
|
-
// ──
|
|
118
|
-
async function enterSingleProjectMode() {
|
|
119
|
-
// 프로젝트 선택 UI만 숨김 (모드 토글은 유지)
|
|
120
|
-
const header = document.querySelector(".selector-card .card-header");
|
|
121
|
-
const projectGrid = document.getElementById("project-grid");
|
|
122
|
-
const selectedHint = document.getElementById("selected-hint");
|
|
123
|
-
if (header) header.style.display = "none";
|
|
124
|
-
if (projectGrid) projectGrid.style.display = "none";
|
|
125
|
-
if (selectedHint) selectedHint.style.display = "none";
|
|
126
|
-
|
|
127
|
-
// 현재 디렉토리를 프로젝트로 자동 선택
|
|
128
|
-
selectedProject = "__self__";
|
|
129
|
-
|
|
130
|
-
document.getElementById("analyze-btn").disabled = false;
|
|
131
|
-
const btnText = document.getElementById("analyze-btn-text");
|
|
132
|
-
if (btnText) btnText.textContent = "Hanni에게 분석 요청";
|
|
133
|
-
|
|
134
|
-
// 현재 모드에 따라 미리 로드
|
|
135
|
-
if (analyzeMode === "commit") {
|
|
136
|
-
await fetchCommitPreview();
|
|
137
|
-
} else {
|
|
138
|
-
await fetchStatusPreview();
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ── Projects ──
|
|
143
|
-
async function loadProjects() {
|
|
144
|
-
const grid = document.getElementById("project-grid");
|
|
145
|
-
grid.innerHTML =
|
|
146
|
-
'<div class="skeleton-grid"><div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div></div>';
|
|
147
|
-
try {
|
|
148
|
-
const res = await fetch("/api/projects");
|
|
149
|
-
const { projects } = await res.json();
|
|
150
|
-
renderProjects(projects);
|
|
151
|
-
} catch (err) {
|
|
152
|
-
grid.innerHTML = `<p style="color:var(--danger);font-size:14px">프로젝트 목록을 불러오지 못했습니다: ${err.message}</p>`;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function renderProjects(projects) {
|
|
157
|
-
const grid = document.getElementById("project-grid");
|
|
158
|
-
if (!projects || projects.length === 0) {
|
|
159
|
-
grid.innerHTML =
|
|
160
|
-
'<p style="color:var(--text3);font-size:14px">git 프로젝트가 없습니다.</p>';
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
grid.innerHTML = projects
|
|
164
|
-
.map(
|
|
165
|
-
(p) => `
|
|
166
|
-
<div class="project-item" data-name="${p.name}">
|
|
167
|
-
<span class="proj-icon">${getProjectIcon(p.name)}</span>
|
|
168
|
-
<span class="proj-name">${p.name}</span>
|
|
169
|
-
</div>
|
|
170
|
-
`,
|
|
171
|
-
)
|
|
172
|
-
.join("");
|
|
173
|
-
grid.querySelectorAll(".project-item").forEach((el) => {
|
|
174
|
-
el.addEventListener("click", () => selectProject(el));
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function getProjectIcon(name) {
|
|
179
|
-
if (name.includes("next") || name.includes("react")) return "⚛️";
|
|
180
|
-
if (name.includes("nest") || name.includes("api")) return "🐉";
|
|
181
|
-
if (name.includes("hook")) return "🪝";
|
|
182
|
-
if (name.includes("portfolio")) return "🎨";
|
|
183
|
-
if (name.includes("todo")) return "✅";
|
|
184
|
-
if (name.includes("doc")) return "📚";
|
|
185
|
-
return "📁";
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ── Project Selection ──
|
|
189
|
-
async function selectProject(el) {
|
|
190
|
-
document
|
|
191
|
-
.querySelectorAll(".project-item")
|
|
192
|
-
.forEach((e) => e.classList.remove("selected"));
|
|
193
|
-
el.classList.add("selected");
|
|
194
|
-
selectedProject = el.dataset.name;
|
|
195
|
-
document.getElementById("selected-hint").textContent =
|
|
196
|
-
`선택됨: ${selectedProject}`;
|
|
197
|
-
document.getElementById("analyze-btn").disabled = false;
|
|
198
|
-
document.getElementById("commit-card").style.display = "none";
|
|
199
|
-
document.getElementById("status-card").style.display = "none";
|
|
200
|
-
|
|
201
|
-
if (analyzeMode === "commit") {
|
|
202
|
-
await fetchCommitPreview();
|
|
203
|
-
} else {
|
|
204
|
-
await fetchStatusPreview();
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
114
|
+
// ── Preview loading ──
|
|
208
115
|
async function fetchCommitPreview() {
|
|
209
|
-
const displayName = isSingleProject ? singleProjectName : selectedProject;
|
|
210
116
|
try {
|
|
211
117
|
const res = await fetch(
|
|
212
118
|
`/api/projects/${encodeURIComponent(selectedProject)}/commit`,
|
|
@@ -215,15 +121,14 @@ async function fetchCommitPreview() {
|
|
|
215
121
|
if (error) throw new Error(error);
|
|
216
122
|
renderCommitCard(commit);
|
|
217
123
|
document.getElementById("commit-card").style.display = "block";
|
|
218
|
-
setAriaState("ready-commit", { project:
|
|
124
|
+
setAriaState("ready-commit", { project: projectName });
|
|
219
125
|
} catch (e) {
|
|
220
126
|
console.warn("commit preview failed:", e.message);
|
|
221
|
-
setAriaState("ready-commit", { project:
|
|
127
|
+
setAriaState("ready-commit", { project: projectName });
|
|
222
128
|
}
|
|
223
129
|
}
|
|
224
130
|
|
|
225
131
|
async function fetchStatusPreview() {
|
|
226
|
-
const displayName = isSingleProject ? singleProjectName : selectedProject;
|
|
227
132
|
try {
|
|
228
133
|
const res = await fetch(
|
|
229
134
|
`/api/projects/${encodeURIComponent(selectedProject)}/status`,
|
|
@@ -234,17 +139,15 @@ async function fetchStatusPreview() {
|
|
|
234
139
|
renderStatusCard(status);
|
|
235
140
|
document.getElementById("status-card").style.display = "block";
|
|
236
141
|
setAriaState("ready-status", {
|
|
237
|
-
project:
|
|
142
|
+
project: projectName,
|
|
238
143
|
n: status.totalFiles,
|
|
239
144
|
});
|
|
240
145
|
} else {
|
|
241
|
-
|
|
242
|
-
if (hint) hint.textContent = `${displayName} — 변경사항 없음`;
|
|
243
|
-
setAriaState("ready-status", { project: displayName, n: 0 });
|
|
146
|
+
setAriaState("ready-status", { project: projectName, n: 0 });
|
|
244
147
|
}
|
|
245
148
|
} catch (e) {
|
|
246
149
|
console.warn("status preview failed:", e.message);
|
|
247
|
-
setAriaState("ready-status", { project:
|
|
150
|
+
setAriaState("ready-status", { project: projectName, n: 0 });
|
|
248
151
|
}
|
|
249
152
|
}
|
|
250
153
|
|
|
@@ -318,7 +221,6 @@ function switchMode(mode) {
|
|
|
318
221
|
btnText.textContent =
|
|
319
222
|
mode === "commit" ? "Hanni에게 분석 요청" : "Hanni에게 리뷰 요청";
|
|
320
223
|
|
|
321
|
-
if (!selectedProject) return;
|
|
322
224
|
if (mode === "commit") fetchCommitPreview();
|
|
323
225
|
else fetchStatusPreview();
|
|
324
226
|
}
|
|
@@ -331,7 +233,7 @@ function onAnalyzeClick() {
|
|
|
331
233
|
|
|
332
234
|
// ── Generic SSE Analysis ──
|
|
333
235
|
async function startAnalysis(endpoint) {
|
|
334
|
-
if (isAnalyzing
|
|
236
|
+
if (isAnalyzing) return;
|
|
335
237
|
isAnalyzing = true;
|
|
336
238
|
|
|
337
239
|
const resultCard = document.getElementById("result-card");
|
|
@@ -343,7 +245,7 @@ async function startAnalysis(endpoint) {
|
|
|
343
245
|
resultCard.style.display = "block";
|
|
344
246
|
analysisBody.innerHTML = "";
|
|
345
247
|
reportSaved.textContent = "";
|
|
346
|
-
document.getElementById("copy-btn").style.display = "none";
|
|
248
|
+
document.getElementById("copy-btn").style.display = "none";
|
|
347
249
|
setStatus("loading", "Hanni가 코드를 살펴보고 있어요...");
|
|
348
250
|
setAriaState("thinking");
|
|
349
251
|
analyzeBtn.disabled = true;
|
|
@@ -398,7 +300,7 @@ async function startAnalysis(endpoint) {
|
|
|
398
300
|
} else if (data.type === "done") {
|
|
399
301
|
setStatus("done", "✅ 분석 완료!");
|
|
400
302
|
setAriaState("done");
|
|
401
|
-
document.getElementById("copy-btn").style.display = "inline-flex";
|
|
303
|
+
document.getElementById("copy-btn").style.display = "inline-flex";
|
|
402
304
|
} else if (data.type === "error") {
|
|
403
305
|
setStatus("error", `오류: ${data.message}`);
|
|
404
306
|
setAriaState("error");
|
|
@@ -440,6 +342,99 @@ function onCopy() {
|
|
|
440
342
|
});
|
|
441
343
|
}
|
|
442
344
|
|
|
345
|
+
// ── Auto Analysis via SSE (+ polling fallback) ──
|
|
346
|
+
let autoAnalysisPollTimer = null;
|
|
347
|
+
let autoAnalysisShownFilename = null;
|
|
348
|
+
|
|
349
|
+
function connectAutoAnalysisEvents() {
|
|
350
|
+
const evtSource = new EventSource("/api/events");
|
|
351
|
+
|
|
352
|
+
evtSource.onmessage = (e) => {
|
|
353
|
+
try {
|
|
354
|
+
const data = JSON.parse(e.data);
|
|
355
|
+
handleAutoAnalysisEvent(data);
|
|
356
|
+
} catch {}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
evtSource.onerror = () => {
|
|
360
|
+
// EventSource 자동 재연결됨 — 별도 처리 불필요
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// SSE와 관계없이 5초마다 상태 폴링 (SSE 놓쳐도 반드시 동작)
|
|
364
|
+
startAutoAnalysisPoll();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function handleAutoAnalysisEvent(data) {
|
|
368
|
+
if (isAnalyzing) return;
|
|
369
|
+
if (data.type === "analysis-started") {
|
|
370
|
+
showAutoAnalysisStarted(data.projectName);
|
|
371
|
+
startAutoAnalysisPoll();
|
|
372
|
+
} else if (data.type === "analysis-done") {
|
|
373
|
+
stopAutoAnalysisPoll();
|
|
374
|
+
if (data.filename !== autoAnalysisShownFilename) {
|
|
375
|
+
autoAnalysisShownFilename = data.filename;
|
|
376
|
+
showAutoAnalysisDone(data);
|
|
377
|
+
}
|
|
378
|
+
} else if (data.type === "analysis-error") {
|
|
379
|
+
stopAutoAnalysisPoll();
|
|
380
|
+
setStatus("error", `자동 분석 오류: ${data.message}`);
|
|
381
|
+
setAriaState("error");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function startAutoAnalysisPoll() {
|
|
386
|
+
stopAutoAnalysisPoll();
|
|
387
|
+
autoAnalysisPollTimer = setInterval(async () => {
|
|
388
|
+
if (isAnalyzing) return;
|
|
389
|
+
try {
|
|
390
|
+
const res = await fetch("/api/auto-analysis/state");
|
|
391
|
+
const state = await res.json();
|
|
392
|
+
if (state.status === "analyzing" && autoAnalysisShownFilename !== "__loading__") {
|
|
393
|
+
autoAnalysisShownFilename = "__loading__";
|
|
394
|
+
showAutoAnalysisStarted(state.projectName);
|
|
395
|
+
} else if (state.status === "done" && state.filename !== autoAnalysisShownFilename) {
|
|
396
|
+
autoAnalysisShownFilename = state.filename;
|
|
397
|
+
showAutoAnalysisDone(state);
|
|
398
|
+
}
|
|
399
|
+
} catch {}
|
|
400
|
+
}, 5000);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function stopAutoAnalysisPoll() {
|
|
404
|
+
if (autoAnalysisPollTimer) {
|
|
405
|
+
clearInterval(autoAnalysisPollTimer);
|
|
406
|
+
autoAnalysisPollTimer = null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function showAutoAnalysisStarted(pName) {
|
|
411
|
+
const resultCard = document.getElementById("result-card");
|
|
412
|
+
const analysisBody = document.getElementById("analysis-body");
|
|
413
|
+
const reportSaved = document.getElementById("report-saved");
|
|
414
|
+
resultCard.style.display = "block";
|
|
415
|
+
analysisBody.innerHTML = "";
|
|
416
|
+
reportSaved.textContent = "";
|
|
417
|
+
document.getElementById("copy-btn").style.display = "none";
|
|
418
|
+
setStatus("loading", `${pName} 커밋 자동 분석 중...`);
|
|
419
|
+
setAriaState("thinking");
|
|
420
|
+
resultCard.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function showAutoAnalysisDone({ filename, content }) {
|
|
424
|
+
const resultCard = document.getElementById("result-card");
|
|
425
|
+
const analysisBody = document.getElementById("analysis-body");
|
|
426
|
+
const reportSaved = document.getElementById("report-saved");
|
|
427
|
+
const copyBtn = document.getElementById("copy-btn");
|
|
428
|
+
resultCard.style.display = "block";
|
|
429
|
+
analysisBody.innerHTML = marked.parse(content);
|
|
430
|
+
reportSaved.textContent = `✓ 저장됨: ${filename}`;
|
|
431
|
+
setStatus("done", "✅ 자동 분석 완료!");
|
|
432
|
+
setAriaState("done");
|
|
433
|
+
copyBtn.style.display = "inline-flex";
|
|
434
|
+
copyBtn._text = content;
|
|
435
|
+
resultCard.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
436
|
+
}
|
|
437
|
+
|
|
443
438
|
// ── Reports Tab ──
|
|
444
439
|
async function loadReports() {
|
|
445
440
|
const listEl = document.getElementById("reports-list");
|
|
@@ -506,10 +501,85 @@ function setupTabs() {
|
|
|
506
501
|
btn.classList.add("active");
|
|
507
502
|
document.getElementById("tab-" + tab).classList.add("active");
|
|
508
503
|
if (tab === "reports") loadReports();
|
|
504
|
+
if (tab === "hooks") loadHookStatus();
|
|
509
505
|
});
|
|
510
506
|
});
|
|
511
507
|
}
|
|
512
508
|
|
|
509
|
+
// ── Hooks Tab ──
|
|
510
|
+
async function loadHookStatus() {
|
|
511
|
+
const listEl = document.getElementById("hook-projects-list");
|
|
512
|
+
listEl.innerHTML = '<p class="empty-state">불러오는 중...</p>';
|
|
513
|
+
try {
|
|
514
|
+
const res = await fetch("/api/hooks/status");
|
|
515
|
+
const { projects } = await res.json();
|
|
516
|
+
if (!projects || projects.length === 0) {
|
|
517
|
+
listEl.innerHTML =
|
|
518
|
+
'<p class="empty-state">git 프로젝트를 찾을 수 없습니다.</p>';
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
listEl.innerHTML = projects
|
|
522
|
+
.map((p) => {
|
|
523
|
+
const pcInstalled = p.postCommit?.installed;
|
|
524
|
+
const ppInstalled = p.prePush?.installed;
|
|
525
|
+
const allInstalled = pcInstalled && ppInstalled;
|
|
526
|
+
const noneInstalled = !pcInstalled && !ppInstalled;
|
|
527
|
+
const statusBadge = allInstalled
|
|
528
|
+
? '<span class="hook-badge installed">설치됨</span>'
|
|
529
|
+
: noneInstalled
|
|
530
|
+
? '<span class="hook-badge not-installed">미설치</span>'
|
|
531
|
+
: '<span class="hook-badge partial">일부 설치</span>';
|
|
532
|
+
|
|
533
|
+
const displayName = p.displayName || p.name;
|
|
534
|
+
return `<div class="hook-project-row" data-name="${escHtml(p.name)}">
|
|
535
|
+
<div class="hook-project-info">
|
|
536
|
+
<span class="hook-project-name">${escHtml(displayName)}</span>
|
|
537
|
+
${statusBadge}
|
|
538
|
+
<span class="hook-detail">post-commit: ${pcInstalled ? "✅" : "❌"} pre-push: ${ppInstalled ? "✅" : "❌"}</span>
|
|
539
|
+
</div>
|
|
540
|
+
<div class="hook-project-actions">
|
|
541
|
+
${
|
|
542
|
+
allInstalled
|
|
543
|
+
? `<button class="btn-ghost hook-remove-btn" data-name="${escHtml(p.name)}">제거</button>`
|
|
544
|
+
: `<button class="btn-secondary hook-install-btn" data-name="${escHtml(p.name)}">설치</button>`
|
|
545
|
+
}
|
|
546
|
+
</div>
|
|
547
|
+
</div>`;
|
|
548
|
+
})
|
|
549
|
+
.join("");
|
|
550
|
+
|
|
551
|
+
listEl.querySelectorAll(".hook-install-btn").forEach((btn) => {
|
|
552
|
+
btn.addEventListener("click", () => handleHookAction("install", btn.dataset.name, btn));
|
|
553
|
+
});
|
|
554
|
+
listEl.querySelectorAll(".hook-remove-btn").forEach((btn) => {
|
|
555
|
+
btn.addEventListener("click", () => handleHookAction("remove", btn.dataset.name, btn));
|
|
556
|
+
});
|
|
557
|
+
} catch {
|
|
558
|
+
listEl.innerHTML =
|
|
559
|
+
'<p class="empty-state" style="color:var(--danger)">훅 상태를 불러오지 못했습니다.</p>';
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function handleHookAction(action, pName, btn) {
|
|
564
|
+
const original = btn.textContent;
|
|
565
|
+
btn.disabled = true;
|
|
566
|
+
btn.textContent = action === "install" ? "설치 중..." : "제거 중...";
|
|
567
|
+
try {
|
|
568
|
+
const res = await fetch(`/api/hooks/${action}`, {
|
|
569
|
+
method: "POST",
|
|
570
|
+
headers: { "Content-Type": "application/json" },
|
|
571
|
+
body: JSON.stringify({ projectName: pName }),
|
|
572
|
+
});
|
|
573
|
+
const data = await res.json();
|
|
574
|
+
if (!res.ok) throw new Error(data.error || "요청 실패");
|
|
575
|
+
await loadHookStatus();
|
|
576
|
+
} catch (err) {
|
|
577
|
+
btn.disabled = false;
|
|
578
|
+
btn.textContent = original;
|
|
579
|
+
alert(`오류: ${err.message}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
513
583
|
// ── Helpers ──
|
|
514
584
|
function togglePre(preId, btnId) {
|
|
515
585
|
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"
|
|
@@ -158,7 +156,7 @@
|
|
|
158
156
|
<div class="aria-bubble-wrap">
|
|
159
157
|
<div class="aria-bubble">
|
|
160
158
|
<span class="aria-bubble-text" id="aria-bubble-text"
|
|
161
|
-
|
|
159
|
+
>분석을 시작할까요?</span
|
|
162
160
|
>
|
|
163
161
|
<span
|
|
164
162
|
class="aria-typing-dots"
|
|
@@ -179,14 +177,8 @@
|
|
|
179
177
|
키를 입력해 주세요.
|
|
180
178
|
</div>
|
|
181
179
|
|
|
182
|
-
<!--
|
|
180
|
+
<!-- Selector Card -->
|
|
183
181
|
<div class="card selector-card">
|
|
184
|
-
<div class="card-header">
|
|
185
|
-
<h2 class="card-title">🗂 프로젝트 선택</h2>
|
|
186
|
-
<button class="refresh-btn" id="refresh-projects" title="새로고침">
|
|
187
|
-
↻
|
|
188
|
-
</button>
|
|
189
|
-
</div>
|
|
190
182
|
<!-- Mode Toggle -->
|
|
191
183
|
<div class="mode-toggle">
|
|
192
184
|
<button class="mode-btn active" id="mode-commit" data-mode="commit">
|
|
@@ -196,18 +188,8 @@
|
|
|
196
188
|
🔍 현재 변경사항 분석
|
|
197
189
|
</button>
|
|
198
190
|
</div>
|
|
199
|
-
<div class="project-grid" id="project-grid">
|
|
200
|
-
<div class="skeleton-grid">
|
|
201
|
-
<div class="skeleton"></div>
|
|
202
|
-
<div class="skeleton"></div>
|
|
203
|
-
<div class="skeleton"></div>
|
|
204
|
-
</div>
|
|
205
|
-
</div>
|
|
206
191
|
<div class="card-footer">
|
|
207
|
-
<
|
|
208
|
-
>프로젝트를 선택해 주세요</span
|
|
209
|
-
>
|
|
210
|
-
<button class="btn-primary" id="analyze-btn" disabled>
|
|
192
|
+
<button class="btn-primary" id="analyze-btn">
|
|
211
193
|
<span class="btn-icon">🤖</span>
|
|
212
194
|
<span id="analyze-btn-text">Hanni에게 분석 요청</span>
|
|
213
195
|
</button>
|
|
@@ -265,6 +247,71 @@
|
|
|
265
247
|
</div>
|
|
266
248
|
</section>
|
|
267
249
|
|
|
250
|
+
<!-- ── Tab: Hooks ── -->
|
|
251
|
+
<section class="tab-content" id="tab-hooks">
|
|
252
|
+
<div class="card">
|
|
253
|
+
<div class="card-header">
|
|
254
|
+
<h2 class="card-title">🪝 Git Hook 관리</h2>
|
|
255
|
+
<button class="btn-ghost" id="refresh-hooks" title="새로고침">
|
|
256
|
+
↻
|
|
257
|
+
</button>
|
|
258
|
+
</div>
|
|
259
|
+
<p class="hook-desc">
|
|
260
|
+
훅을 설치하면 <strong>커밋할 때마다 자동으로 AI 분석</strong>이
|
|
261
|
+
실행되고, <strong>push 전에 secret 유출을 자동 차단</strong>합니다.
|
|
262
|
+
</p>
|
|
263
|
+
|
|
264
|
+
<div class="hook-section">
|
|
265
|
+
<div class="hook-section-title">
|
|
266
|
+
<span class="hook-icon">⚡</span>
|
|
267
|
+
post-commit — 자동 커밋 분석
|
|
268
|
+
</div>
|
|
269
|
+
<p class="hook-section-desc">
|
|
270
|
+
커밋 직후 Gemini AI가 백그라운드에서 분석 리포트를 자동
|
|
271
|
+
생성합니다.
|
|
272
|
+
</p>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<div class="hook-section">
|
|
276
|
+
<div class="hook-section-title">
|
|
277
|
+
<span class="hook-icon">⛔</span>
|
|
278
|
+
pre-push — Secret 유출 탐지
|
|
279
|
+
</div>
|
|
280
|
+
<p class="hook-section-desc">
|
|
281
|
+
push 전에 API 키, 비밀번호, private key 등의 유출을 탐지하고
|
|
282
|
+
차단합니다.
|
|
283
|
+
</p>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<div id="hook-projects-list" class="hook-projects-list">
|
|
287
|
+
<p class="empty-state">불러오는 중...</p>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div class="card hook-cli-card">
|
|
292
|
+
<h2 class="card-title">💻 CLI 사용법</h2>
|
|
293
|
+
<div class="hook-cli-block">
|
|
294
|
+
<div class="hook-cli-item">
|
|
295
|
+
<code>commit-ai-agent hook install</code>
|
|
296
|
+
<span>현재 디렉토리에 훅 설치</span>
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
<div class="hook-cli-item">
|
|
300
|
+
<code>commit-ai-agent hook remove</code>
|
|
301
|
+
<span>훅 제거</span>
|
|
302
|
+
</div>
|
|
303
|
+
<div class="hook-cli-item">
|
|
304
|
+
<code>commit-ai-agent hook status</code>
|
|
305
|
+
<span>설치 상태 확인</span>
|
|
306
|
+
</div>
|
|
307
|
+
<div class="hook-cli-item">
|
|
308
|
+
<code>SKIP_SECRET_SCAN=1 git push</code>
|
|
309
|
+
<span>Secret 스캔 일시 스킵 (오탐 시)</span>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
</section>
|
|
314
|
+
|
|
268
315
|
<!-- ── Tab: Reports ── -->
|
|
269
316
|
<section class="tab-content" id="tab-reports">
|
|
270
317
|
<div class="card">
|