commit-ai-agent 1.0.6 → 1.0.7
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 +2 -1
- package/README.md +17 -12
- package/bin/cli.js +6 -7
- package/package.json +1 -1
- package/public/app.js +283 -188
- package/public/index.html +102 -30
- package/public/style.css +742 -223
- package/src/config.js +32 -0
- package/src/server.js +106 -18
package/src/config.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
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
|
+
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
|
+
let stat;
|
|
23
|
+
try {
|
|
24
|
+
stat = fs.statSync(targetPath);
|
|
25
|
+
} catch {
|
|
26
|
+
throw new Error(`${sourceLabel} 경로를 찾을 수 없습니다: ${targetPath}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!stat.isDirectory()) {
|
|
30
|
+
throw new Error(`${sourceLabel} 경로가 디렉토리가 아닙니다: ${targetPath}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/server.js
CHANGED
|
@@ -5,12 +5,14 @@ import { fileURLToPath } from "url";
|
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import { listGitProjects, getLatestCommit, getWorkingStatus } from "./git.js";
|
|
7
7
|
import { analyzeCommit, analyzeWorkingStatus } from "./analyzer.js";
|
|
8
|
+
import { resolveDevRoot } from "./config.js";
|
|
8
9
|
|
|
9
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const app = express();
|
|
11
12
|
|
|
12
|
-
const PORT = process.env.PORT ||
|
|
13
|
-
const DEV_ROOT =
|
|
13
|
+
const PORT = process.env.PORT || 50324;
|
|
14
|
+
const { devRoot: DEV_ROOT, source: DEV_ROOT_SOURCE } = resolveDevRoot();
|
|
15
|
+
const BAD_REQUEST_PREFIX = "[BAD_REQUEST]";
|
|
14
16
|
|
|
15
17
|
// npx/global install 시: COMMIT_ANALYZER_ROOT = bin/cli.js가 설정한 패키지 루트
|
|
16
18
|
// 로컬 dev 시: __dirname/../ 사용
|
|
@@ -28,6 +30,55 @@ if (!fs.existsSync(REPORTS_DIR)) {
|
|
|
28
30
|
app.use(express.json());
|
|
29
31
|
app.use(express.static(path.join(PACKAGE_ROOT, "public")));
|
|
30
32
|
|
|
33
|
+
function createBadRequestError(message) {
|
|
34
|
+
return new Error(`${BAD_REQUEST_PREFIX} ${message}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isBadRequestError(err) {
|
|
38
|
+
return (
|
|
39
|
+
typeof err?.message === "string" &&
|
|
40
|
+
err.message.startsWith(BAD_REQUEST_PREFIX)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveProjectPath(projectName) {
|
|
45
|
+
if (!projectName || typeof projectName !== "string") {
|
|
46
|
+
throw createBadRequestError("프로젝트명이 필요합니다.");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const trimmedName = projectName.trim();
|
|
50
|
+
if (!trimmedName) {
|
|
51
|
+
throw createBadRequestError("프로젝트명이 비어 있습니다.");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Single-project mode: "__self__" = DEV_ROOT 자체 (DEV_ROOT가 git 저장소인 경우)
|
|
55
|
+
if (trimmedName === "__self__") {
|
|
56
|
+
if (!fs.existsSync(path.join(DEV_ROOT, ".git"))) {
|
|
57
|
+
throw createBadRequestError("현재 디렉토리가 Git 저장소가 아닙니다.");
|
|
58
|
+
}
|
|
59
|
+
return DEV_ROOT;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const projectPath = path.resolve(DEV_ROOT, trimmedName);
|
|
63
|
+
const relativePath = path.relative(DEV_ROOT, projectPath);
|
|
64
|
+
const isOutsideRoot =
|
|
65
|
+
relativePath.startsWith("..") || path.isAbsolute(relativePath);
|
|
66
|
+
|
|
67
|
+
if (isOutsideRoot) {
|
|
68
|
+
throw createBadRequestError("유효하지 않은 프로젝트 경로입니다.");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) {
|
|
72
|
+
throw createBadRequestError("프로젝트를 찾을 수 없습니다.");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!fs.existsSync(path.join(projectPath, ".git"))) {
|
|
76
|
+
throw createBadRequestError("Git 저장소가 아닌 프로젝트입니다.");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return projectPath;
|
|
80
|
+
}
|
|
81
|
+
|
|
31
82
|
// ──────────────────────────────────────────────
|
|
32
83
|
// PWA 아이콘 (SVG를 PNG MIME으로 서빙)
|
|
33
84
|
// ──────────────────────────────────────────────
|
|
@@ -50,7 +101,15 @@ app.get("/api/config", (req, res) => {
|
|
|
50
101
|
process.env.GEMINI_API_KEY &&
|
|
51
102
|
process.env.GEMINI_API_KEY !== "your_gemini_api_key_here"
|
|
52
103
|
);
|
|
53
|
-
|
|
104
|
+
const isSingleProject =
|
|
105
|
+
DEV_ROOT_SOURCE === "cwd" && fs.existsSync(path.join(DEV_ROOT, ".git"));
|
|
106
|
+
res.json({
|
|
107
|
+
hasKey,
|
|
108
|
+
devRoot: DEV_ROOT,
|
|
109
|
+
devRootSource: DEV_ROOT_SOURCE,
|
|
110
|
+
isSingleProject,
|
|
111
|
+
singleProjectName: isSingleProject ? path.basename(DEV_ROOT) : null,
|
|
112
|
+
});
|
|
54
113
|
});
|
|
55
114
|
|
|
56
115
|
// ──────────────────────────────────────────────
|
|
@@ -70,10 +129,15 @@ app.get("/api/projects", async (req, res) => {
|
|
|
70
129
|
// ──────────────────────────────────────────────
|
|
71
130
|
app.get("/api/projects/:name/commit", async (req, res) => {
|
|
72
131
|
try {
|
|
73
|
-
const projectPath =
|
|
132
|
+
const projectPath = resolveProjectPath(req.params.name);
|
|
74
133
|
const commit = await getLatestCommit(projectPath);
|
|
75
134
|
res.json({ commit });
|
|
76
135
|
} catch (err) {
|
|
136
|
+
if (isBadRequestError(err)) {
|
|
137
|
+
return res
|
|
138
|
+
.status(400)
|
|
139
|
+
.json({ error: err.message.replace(`${BAD_REQUEST_PREFIX} `, "") });
|
|
140
|
+
}
|
|
77
141
|
res.status(500).json({ error: err.message });
|
|
78
142
|
}
|
|
79
143
|
});
|
|
@@ -83,10 +147,15 @@ app.get("/api/projects/:name/commit", async (req, res) => {
|
|
|
83
147
|
// ──────────────────────────────────────────────
|
|
84
148
|
app.get("/api/projects/:name/status", async (req, res) => {
|
|
85
149
|
try {
|
|
86
|
-
const projectPath =
|
|
150
|
+
const projectPath = resolveProjectPath(req.params.name);
|
|
87
151
|
const status = await getWorkingStatus(projectPath);
|
|
88
152
|
res.json({ status });
|
|
89
153
|
} catch (err) {
|
|
154
|
+
if (isBadRequestError(err)) {
|
|
155
|
+
return res
|
|
156
|
+
.status(400)
|
|
157
|
+
.json({ error: err.message.replace(`${BAD_REQUEST_PREFIX} `, "") });
|
|
158
|
+
}
|
|
90
159
|
res.status(500).json({ error: err.message });
|
|
91
160
|
}
|
|
92
161
|
});
|
|
@@ -97,6 +166,7 @@ app.get("/api/projects/:name/status", async (req, res) => {
|
|
|
97
166
|
app.post("/api/analyze", async (req, res) => {
|
|
98
167
|
const { projectName } = req.body;
|
|
99
168
|
const apiKey = process.env.GEMINI_API_KEY;
|
|
169
|
+
let projectPath;
|
|
100
170
|
|
|
101
171
|
if (!apiKey || apiKey === "your_gemini_api_key_here") {
|
|
102
172
|
return res
|
|
@@ -104,8 +174,15 @@ app.post("/api/analyze", async (req, res) => {
|
|
|
104
174
|
.json({ error: "GEMINI_API_KEY가 .env 파일에 설정되지 않았습니다." });
|
|
105
175
|
}
|
|
106
176
|
|
|
107
|
-
|
|
108
|
-
|
|
177
|
+
try {
|
|
178
|
+
projectPath = resolveProjectPath(projectName);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
if (isBadRequestError(err)) {
|
|
181
|
+
return res
|
|
182
|
+
.status(400)
|
|
183
|
+
.json({ error: err.message.replace(`${BAD_REQUEST_PREFIX} `, "") });
|
|
184
|
+
}
|
|
185
|
+
return res.status(500).json({ error: err.message });
|
|
109
186
|
}
|
|
110
187
|
|
|
111
188
|
// Server-Sent Events 설정
|
|
@@ -120,22 +197,23 @@ app.post("/api/analyze", async (req, res) => {
|
|
|
120
197
|
|
|
121
198
|
try {
|
|
122
199
|
send({ type: "status", message: "커밋 정보를 가져오는 중..." });
|
|
123
|
-
const projectPath = path.join(DEV_ROOT, projectName);
|
|
124
200
|
const commit = await getLatestCommit(projectPath);
|
|
125
201
|
|
|
126
202
|
send({ type: "commit", commit });
|
|
127
203
|
send({ type: "status", message: "AI 분석 중... (30초~1분 소요)" });
|
|
128
204
|
|
|
129
|
-
const
|
|
205
|
+
const displayName =
|
|
206
|
+
projectName === "__self__" ? path.basename(DEV_ROOT) : projectName;
|
|
207
|
+
const analysis = await analyzeCommit(commit, displayName, apiKey);
|
|
130
208
|
|
|
131
209
|
// 리포트 저장
|
|
132
210
|
const timestamp = new Date()
|
|
133
211
|
.toISOString()
|
|
134
212
|
.replace(/[:.]/g, "-")
|
|
135
213
|
.slice(0, 19);
|
|
136
|
-
const reportFilename = `${
|
|
214
|
+
const reportFilename = `${displayName}-${timestamp}.md`;
|
|
137
215
|
const reportPath = path.join(REPORTS_DIR, reportFilename);
|
|
138
|
-
const fullReport = buildMarkdownReport(
|
|
216
|
+
const fullReport = buildMarkdownReport(displayName, commit, analysis);
|
|
139
217
|
fs.writeFileSync(reportPath, fullReport, "utf-8");
|
|
140
218
|
|
|
141
219
|
send({ type: "analysis", analysis, reportFilename });
|
|
@@ -153,6 +231,7 @@ app.post("/api/analyze", async (req, res) => {
|
|
|
153
231
|
app.post("/api/analyze-status", async (req, res) => {
|
|
154
232
|
const { projectName } = req.body;
|
|
155
233
|
const apiKey = process.env.GEMINI_API_KEY;
|
|
234
|
+
let projectPath;
|
|
156
235
|
|
|
157
236
|
if (!apiKey || apiKey === "your_gemini_api_key_here") {
|
|
158
237
|
return res
|
|
@@ -160,8 +239,15 @@ app.post("/api/analyze-status", async (req, res) => {
|
|
|
160
239
|
.json({ error: "GEMINI_API_KEY가 .env 파일에 설정되지 않았습니다." });
|
|
161
240
|
}
|
|
162
241
|
|
|
163
|
-
|
|
164
|
-
|
|
242
|
+
try {
|
|
243
|
+
projectPath = resolveProjectPath(projectName);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
if (isBadRequestError(err)) {
|
|
246
|
+
return res
|
|
247
|
+
.status(400)
|
|
248
|
+
.json({ error: err.message.replace(`${BAD_REQUEST_PREFIX} `, "") });
|
|
249
|
+
}
|
|
250
|
+
return res.status(500).json({ error: err.message });
|
|
165
251
|
}
|
|
166
252
|
|
|
167
253
|
res.setHeader("Content-Type", "text/event-stream");
|
|
@@ -175,7 +261,6 @@ app.post("/api/analyze-status", async (req, res) => {
|
|
|
175
261
|
|
|
176
262
|
try {
|
|
177
263
|
send({ type: "status", message: "변경사항을 가져오는 중..." });
|
|
178
|
-
const projectPath = path.join(DEV_ROOT, projectName);
|
|
179
264
|
const workingStatus = await getWorkingStatus(projectPath);
|
|
180
265
|
|
|
181
266
|
if (!workingStatus) {
|
|
@@ -190,9 +275,11 @@ app.post("/api/analyze-status", async (req, res) => {
|
|
|
190
275
|
send({ type: "working-status", workingStatus });
|
|
191
276
|
send({ type: "status", message: "AI 분석 중... (30초~1분 소요)" });
|
|
192
277
|
|
|
278
|
+
const displayName =
|
|
279
|
+
projectName === "__self__" ? path.basename(DEV_ROOT) : projectName;
|
|
193
280
|
const analysis = await analyzeWorkingStatus(
|
|
194
281
|
workingStatus,
|
|
195
|
-
|
|
282
|
+
displayName,
|
|
196
283
|
apiKey,
|
|
197
284
|
);
|
|
198
285
|
|
|
@@ -201,9 +288,9 @@ app.post("/api/analyze-status", async (req, res) => {
|
|
|
201
288
|
.toISOString()
|
|
202
289
|
.replace(/[:.]/g, "-")
|
|
203
290
|
.slice(0, 19);
|
|
204
|
-
const reportFilename = `${
|
|
291
|
+
const reportFilename = `${displayName}-status-${timestamp}.md`;
|
|
205
292
|
const reportPath = path.join(REPORTS_DIR, reportFilename);
|
|
206
|
-
const fullReport = buildStatusReport(
|
|
293
|
+
const fullReport = buildStatusReport(displayName, workingStatus, analysis);
|
|
207
294
|
fs.writeFileSync(reportPath, fullReport, "utf-8");
|
|
208
295
|
|
|
209
296
|
send({ type: "analysis", analysis, reportFilename });
|
|
@@ -293,5 +380,6 @@ ${analysis}
|
|
|
293
380
|
app.listen(PORT, () => {
|
|
294
381
|
console.log(`\n🚀 Commit Ai Agent 실행 중`);
|
|
295
382
|
console.log(` 브라우저: http://localhost:${PORT}`);
|
|
296
|
-
console.log(` 분석 대상: ${DEV_ROOT}
|
|
383
|
+
console.log(` 분석 대상: ${DEV_ROOT}`);
|
|
384
|
+
console.log(` DEV_ROOT source: ${DEV_ROOT_SOURCE}\n`);
|
|
297
385
|
});
|