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/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 || 3000;
13
- const DEV_ROOT = process.env.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
- res.json({ hasKey, devRoot: DEV_ROOT });
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 = path.join(DEV_ROOT, req.params.name);
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 = path.join(DEV_ROOT, req.params.name);
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
- if (!projectName) {
108
- return res.status(400).json({ error: "프로젝트명이 필요합니다." });
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 analysis = await analyzeCommit(commit, projectName, apiKey);
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 = `${projectName}-${timestamp}.md`;
214
+ const reportFilename = `${displayName}-${timestamp}.md`;
137
215
  const reportPath = path.join(REPORTS_DIR, reportFilename);
138
- const fullReport = buildMarkdownReport(projectName, commit, analysis);
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
- if (!projectName) {
164
- return res.status(400).json({ error: "프로젝트명이 필요합니다." });
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
- projectName,
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 = `${projectName}-status-${timestamp}.md`;
291
+ const reportFilename = `${displayName}-status-${timestamp}.md`;
205
292
  const reportPath = path.join(REPORTS_DIR, reportFilename);
206
- const fullReport = buildStatusReport(projectName, workingStatus, analysis);
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}\n`);
383
+ console.log(` 분석 대상: ${DEV_ROOT}`);
384
+ console.log(` DEV_ROOT source: ${DEV_ROOT_SOURCE}\n`);
297
385
  });