@blum84/smart-commit 0.2.0 → 0.2.2
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/README.md +240 -29
- package/dist/chunk-LYR3U55E.js +496 -0
- package/dist/chunk-LYR3U55E.js.map +1 -0
- package/dist/{chunk-ZS27WQDW.js → chunk-MYMEBX2Q.js} +27 -3
- package/dist/chunk-MYMEBX2Q.js.map +1 -0
- package/dist/{classifier-TTZQUM7N.js → classifier-AINQPFLU.js} +4 -2
- package/dist/index.js +36 -459
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.js +239 -0
- package/dist/mcp-server.js.map +1 -0
- package/package.json +8 -4
- package/dist/chunk-ZS27WQDW.js.map +0 -1
- /package/dist/{classifier-TTZQUM7N.js.map → classifier-AINQPFLU.js.map} +0 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
import { cosmiconfig } from "cosmiconfig";
|
|
5
|
+
var DEFAULT_CONFIG = {
|
|
6
|
+
ai: {
|
|
7
|
+
primary: "gemini",
|
|
8
|
+
fallback: "claude",
|
|
9
|
+
timeout: 30
|
|
10
|
+
},
|
|
11
|
+
safety: {
|
|
12
|
+
maxFileSize: "10MB",
|
|
13
|
+
blockedPatterns: [
|
|
14
|
+
"*.env",
|
|
15
|
+
".env.*",
|
|
16
|
+
"*.pem",
|
|
17
|
+
"*.key",
|
|
18
|
+
"credentials*",
|
|
19
|
+
"*.sqlite",
|
|
20
|
+
"*.sqlite3"
|
|
21
|
+
],
|
|
22
|
+
warnPatterns: [
|
|
23
|
+
"*.log",
|
|
24
|
+
"*.csv",
|
|
25
|
+
"package-lock.json",
|
|
26
|
+
"yarn.lock",
|
|
27
|
+
"pnpm-lock.yaml"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
commit: {
|
|
31
|
+
style: "conventional",
|
|
32
|
+
language: "ko",
|
|
33
|
+
maxDiffSize: 1e4
|
|
34
|
+
},
|
|
35
|
+
grouping: {
|
|
36
|
+
strategy: "smart"
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
async function loadConfig(cliOptions = {}) {
|
|
40
|
+
const explorer = cosmiconfig("smart-commit", {
|
|
41
|
+
searchPlaces: [
|
|
42
|
+
".smart-commitrc",
|
|
43
|
+
".smart-commitrc.yaml",
|
|
44
|
+
".smart-commitrc.yml",
|
|
45
|
+
".smart-commitrc.json",
|
|
46
|
+
"smart-commit.config.js",
|
|
47
|
+
"package.json"
|
|
48
|
+
]
|
|
49
|
+
});
|
|
50
|
+
const result = await explorer.search();
|
|
51
|
+
const fileConfig = result?.config ?? {};
|
|
52
|
+
const config = deepMerge(
|
|
53
|
+
DEFAULT_CONFIG,
|
|
54
|
+
fileConfig
|
|
55
|
+
);
|
|
56
|
+
if (cliOptions.ai && typeof cliOptions.ai === "string") {
|
|
57
|
+
config.ai.primary = cliOptions.ai;
|
|
58
|
+
}
|
|
59
|
+
if (cliOptions.group && typeof cliOptions.group === "string") {
|
|
60
|
+
config.grouping.strategy = cliOptions.group;
|
|
61
|
+
}
|
|
62
|
+
return config;
|
|
63
|
+
}
|
|
64
|
+
function deepMerge(target, source) {
|
|
65
|
+
const result = { ...target };
|
|
66
|
+
for (const key of Object.keys(source)) {
|
|
67
|
+
if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key]) && target[key] && typeof target[key] === "object" && !Array.isArray(target[key])) {
|
|
68
|
+
result[key] = deepMerge(
|
|
69
|
+
target[key],
|
|
70
|
+
source[key]
|
|
71
|
+
);
|
|
72
|
+
} else {
|
|
73
|
+
result[key] = source[key];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/scanner.ts
|
|
80
|
+
import { simpleGit } from "simple-git";
|
|
81
|
+
import { readdir, stat, access } from "fs/promises";
|
|
82
|
+
import { join } from "path";
|
|
83
|
+
async function scanRepositories(baseDir, ui, logger) {
|
|
84
|
+
const gitDirs = await findGitDirs(baseDir);
|
|
85
|
+
const repos = [];
|
|
86
|
+
ui.showProgress("Scanning repositories...", 0, gitDirs.length);
|
|
87
|
+
for (let i = 0; i < gitDirs.length; i++) {
|
|
88
|
+
const dir = gitDirs[i];
|
|
89
|
+
ui.showProgress(`Scanning: ${dir}`, i + 1, gitDirs.length);
|
|
90
|
+
try {
|
|
91
|
+
const repo = await inspectRepo(dir, logger);
|
|
92
|
+
repos.push(repo);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
logger.warn({ dir, err }, "Failed to inspect repository");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return repos;
|
|
98
|
+
}
|
|
99
|
+
async function findGitDirs(baseDir) {
|
|
100
|
+
const dirs = [];
|
|
101
|
+
const entries = await readdir(baseDir, { withFileTypes: true });
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (!entry.isDirectory()) continue;
|
|
104
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
105
|
+
const fullPath = join(baseDir, entry.name);
|
|
106
|
+
const gitPath = join(fullPath, ".git");
|
|
107
|
+
try {
|
|
108
|
+
await access(gitPath);
|
|
109
|
+
dirs.push(fullPath);
|
|
110
|
+
} catch {
|
|
111
|
+
const subDirs = await findGitDirs(fullPath);
|
|
112
|
+
dirs.push(...subDirs);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const selfGit = join(baseDir, ".git");
|
|
117
|
+
await access(selfGit);
|
|
118
|
+
if (!dirs.some((d) => d === baseDir)) {
|
|
119
|
+
dirs.unshift(baseDir);
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
return dirs;
|
|
124
|
+
}
|
|
125
|
+
async function inspectRepo(dir, logger) {
|
|
126
|
+
const git = simpleGit(dir);
|
|
127
|
+
const gitStatus = await detectGitStatus(dir, git);
|
|
128
|
+
if (gitStatus === "locked") {
|
|
129
|
+
logger.warn({ dir }, "Git index locked \u2014 skipping");
|
|
130
|
+
return { path: dir, branch: "", status: "locked", files: [], unpushedCommits: 0 };
|
|
131
|
+
}
|
|
132
|
+
if (gitStatus === "detached") {
|
|
133
|
+
logger.warn({ dir }, "Detached HEAD \u2014 skipping");
|
|
134
|
+
return { path: dir, branch: "HEAD (detached)", status: "detached", files: [], unpushedCommits: 0 };
|
|
135
|
+
}
|
|
136
|
+
if (gitStatus === "rebasing") {
|
|
137
|
+
logger.warn({ dir }, "Rebase in progress \u2014 skipping");
|
|
138
|
+
return { path: dir, branch: "", status: "rebasing", files: [], unpushedCommits: 0 };
|
|
139
|
+
}
|
|
140
|
+
const statusResult = await git.status();
|
|
141
|
+
const branch = statusResult.current ?? "unknown";
|
|
142
|
+
const files = [];
|
|
143
|
+
for (const f of statusResult.files) {
|
|
144
|
+
const filePath = join(dir, f.path);
|
|
145
|
+
let size = 0;
|
|
146
|
+
try {
|
|
147
|
+
const s = await stat(filePath);
|
|
148
|
+
size = s.size;
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
files.push({
|
|
152
|
+
path: f.path,
|
|
153
|
+
status: mapGitStatus(f.working_dir, f.index),
|
|
154
|
+
size,
|
|
155
|
+
isBinary: false
|
|
156
|
+
// will be checked by classifier
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
let unpushedCommits = 0;
|
|
160
|
+
try {
|
|
161
|
+
const log = await git.log(["@{u}..HEAD"]);
|
|
162
|
+
unpushedCommits = log.total;
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
const repoStatus = gitStatus === "merging" ? "merging" : files.length > 0 ? "dirty" : "clean";
|
|
166
|
+
return { path: dir, branch, status: repoStatus, files, unpushedCommits };
|
|
167
|
+
}
|
|
168
|
+
async function detectGitStatus(dir, git) {
|
|
169
|
+
try {
|
|
170
|
+
await access(join(dir, ".git", "index.lock"));
|
|
171
|
+
return "locked";
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
await access(join(dir, ".git", "rebase-merge"));
|
|
176
|
+
return "rebasing";
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
await access(join(dir, ".git", "rebase-apply"));
|
|
181
|
+
return "rebasing";
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
await access(join(dir, ".git", "MERGE_HEAD"));
|
|
186
|
+
return "merging";
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
await git.raw(["symbolic-ref", "HEAD"]);
|
|
191
|
+
} catch {
|
|
192
|
+
return "detached";
|
|
193
|
+
}
|
|
194
|
+
return "clean";
|
|
195
|
+
}
|
|
196
|
+
function mapGitStatus(workingDir, index) {
|
|
197
|
+
if (index === "?" || workingDir === "?") return "untracked";
|
|
198
|
+
if (index === "A" || workingDir === "A") return "added";
|
|
199
|
+
if (index === "D" || workingDir === "D") return "deleted";
|
|
200
|
+
if (index === "R" || workingDir === "R") return "renamed";
|
|
201
|
+
return "modified";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/ai-client.ts
|
|
205
|
+
import { execa } from "execa";
|
|
206
|
+
var CONVENTIONAL_PREFIXES = [
|
|
207
|
+
"feat",
|
|
208
|
+
"fix",
|
|
209
|
+
"refactor",
|
|
210
|
+
"docs",
|
|
211
|
+
"style",
|
|
212
|
+
"test",
|
|
213
|
+
"chore",
|
|
214
|
+
"perf",
|
|
215
|
+
"ci",
|
|
216
|
+
"build",
|
|
217
|
+
"revert"
|
|
218
|
+
];
|
|
219
|
+
var CONVENTIONAL_RE = new RegExp(`^(${CONVENTIONAL_PREFIXES.join("|")})(\\(.+\\))?!?:\\s.+`);
|
|
220
|
+
var OFFLINE_TEMPLATES = CONVENTIONAL_PREFIXES.map((prefix) => `${prefix}: `);
|
|
221
|
+
function getOfflineTemplates() {
|
|
222
|
+
return OFFLINE_TEMPLATES;
|
|
223
|
+
}
|
|
224
|
+
async function isAiAvailable(tool) {
|
|
225
|
+
try {
|
|
226
|
+
const cmd = tool === "gpt" ? "openai" : tool;
|
|
227
|
+
await execa("which", [cmd], { timeout: 3e3 });
|
|
228
|
+
return true;
|
|
229
|
+
} catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function createAiClient(config, logger) {
|
|
234
|
+
async function callWithFallback(prompt) {
|
|
235
|
+
let result = await callAi(config.ai.primary, prompt, config.ai.timeout, logger, config);
|
|
236
|
+
if (!result && config.ai.fallback !== config.ai.primary) {
|
|
237
|
+
logger.warn({ fallback: config.ai.fallback }, "Primary AI failed, trying fallback");
|
|
238
|
+
result = await callAi(config.ai.fallback, prompt, config.ai.timeout, logger, config);
|
|
239
|
+
}
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
async generateCommitMessage(diff, language) {
|
|
244
|
+
const summarized = await this.summarizeDiff(diff);
|
|
245
|
+
const prompt = buildCommitPrompt(summarized, language, config.commit.style);
|
|
246
|
+
logger.info({ tool: config.ai.primary, diffLength: summarized.length }, "Requesting commit message");
|
|
247
|
+
let result = await callWithFallback(prompt);
|
|
248
|
+
if (result) {
|
|
249
|
+
if (config.commit.style === "conventional" && !validateConventionalCommit(result)) {
|
|
250
|
+
logger.warn({ message: result.split("\n")[0] }, "Invalid conventional commit, retrying");
|
|
251
|
+
const retryPrompt = buildRetryPrompt(result, language);
|
|
252
|
+
const retried = await callWithFallback(retryPrompt);
|
|
253
|
+
if (retried && validateConventionalCommit(retried)) {
|
|
254
|
+
result = retried;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
result = stripCodeBlocks(result);
|
|
258
|
+
logger.info({ messageLength: result.length }, "Commit message generated");
|
|
259
|
+
}
|
|
260
|
+
return result;
|
|
261
|
+
},
|
|
262
|
+
async resolveConflict(localContent, remoteContent) {
|
|
263
|
+
const prompt = buildConflictPrompt(localContent, remoteContent);
|
|
264
|
+
return callWithFallback(prompt);
|
|
265
|
+
},
|
|
266
|
+
async groupFiles(fileList) {
|
|
267
|
+
const { buildGroupingPrompt } = await import("./classifier-AINQPFLU.js");
|
|
268
|
+
const prompt = buildGroupingPrompt(fileList);
|
|
269
|
+
return callWithFallback(prompt);
|
|
270
|
+
},
|
|
271
|
+
async summarizeDiff(diff) {
|
|
272
|
+
if (diff.length <= config.commit.maxDiffSize) {
|
|
273
|
+
return diff;
|
|
274
|
+
}
|
|
275
|
+
const statSection = extractDiffStat(diff);
|
|
276
|
+
const hunks = extractKeyHunks(diff, config.commit.maxDiffSize - statSection.length - 200);
|
|
277
|
+
const truncated = `${statSection}
|
|
278
|
+
|
|
279
|
+
[\uC8FC\uC694 \uBCC0\uACBD \uB0B4\uC6A9 (\uC804\uCCB4 ${diff.length}\uC790 \uC911 \uD575\uC2EC\uBD80\uB9CC \uCD94\uCD9C)]
|
|
280
|
+
${hunks}`;
|
|
281
|
+
if (truncated.length > config.commit.maxDiffSize * 1.5) {
|
|
282
|
+
logger.info("Diff too large, requesting AI summary");
|
|
283
|
+
const summaryPrompt = buildDiffSummaryPrompt(truncated.slice(0, config.commit.maxDiffSize));
|
|
284
|
+
const summary = await callWithFallback(summaryPrompt);
|
|
285
|
+
return summary ?? truncated.slice(0, config.commit.maxDiffSize);
|
|
286
|
+
}
|
|
287
|
+
return truncated;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function validateConventionalCommit(message) {
|
|
292
|
+
const firstLine = message.split("\n")[0].trim();
|
|
293
|
+
return CONVENTIONAL_RE.test(firstLine);
|
|
294
|
+
}
|
|
295
|
+
function stripCodeBlocks(text) {
|
|
296
|
+
return text.replace(/^```[\w]*\n?/gm, "").replace(/^```\s*$/gm, "").trim();
|
|
297
|
+
}
|
|
298
|
+
function extractDiffStat(diff) {
|
|
299
|
+
const lines = diff.split("\n");
|
|
300
|
+
const statLines = [];
|
|
301
|
+
for (const line of lines) {
|
|
302
|
+
if (line.startsWith("diff --git")) {
|
|
303
|
+
statLines.push(line);
|
|
304
|
+
} else if (line.startsWith("--- ") || line.startsWith("+++ ")) {
|
|
305
|
+
statLines.push(line);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return statLines.join("\n");
|
|
309
|
+
}
|
|
310
|
+
function extractKeyHunks(diff, maxLength) {
|
|
311
|
+
const hunks = [];
|
|
312
|
+
let currentHunk = "";
|
|
313
|
+
let totalLength = 0;
|
|
314
|
+
for (const line of diff.split("\n")) {
|
|
315
|
+
if (line.startsWith("@@")) {
|
|
316
|
+
if (currentHunk && totalLength + currentHunk.length <= maxLength) {
|
|
317
|
+
hunks.push(currentHunk);
|
|
318
|
+
totalLength += currentHunk.length;
|
|
319
|
+
}
|
|
320
|
+
currentHunk = line + "\n";
|
|
321
|
+
} else if (line.startsWith("+") || line.startsWith("-")) {
|
|
322
|
+
currentHunk += line + "\n";
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (currentHunk && totalLength + currentHunk.length <= maxLength) {
|
|
326
|
+
hunks.push(currentHunk);
|
|
327
|
+
}
|
|
328
|
+
return hunks.join("\n");
|
|
329
|
+
}
|
|
330
|
+
async function callAi(tool, prompt, timeout, logger, config) {
|
|
331
|
+
try {
|
|
332
|
+
const { command, args } = buildAiCommand(tool, prompt, config);
|
|
333
|
+
const { stdout } = await execa(command, args, {
|
|
334
|
+
timeout: timeout * 1e3,
|
|
335
|
+
stdin: "ignore"
|
|
336
|
+
});
|
|
337
|
+
const trimmed = stdout.trim();
|
|
338
|
+
return trimmed || null;
|
|
339
|
+
} catch (err) {
|
|
340
|
+
logger.error({ tool, err }, "AI call failed");
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function buildAiCommand(tool, prompt, config) {
|
|
345
|
+
switch (tool) {
|
|
346
|
+
case "gemini":
|
|
347
|
+
return { command: "gemini", args: [prompt] };
|
|
348
|
+
case "claude":
|
|
349
|
+
return { command: "claude", args: ["-p", prompt] };
|
|
350
|
+
case "gpt":
|
|
351
|
+
return { command: "openai", args: ["api", "chat.completions.create", "-m", "gpt-4o", "-g", "user", prompt] };
|
|
352
|
+
case "ollama": {
|
|
353
|
+
const model = config?.ai?.ollama?.model ?? "llama3";
|
|
354
|
+
return { command: "ollama", args: ["run", model, prompt] };
|
|
355
|
+
}
|
|
356
|
+
default:
|
|
357
|
+
return { command: tool, args: [prompt] };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function buildCommitPrompt(diff, language, style) {
|
|
361
|
+
const langLabel = language === "ko" ? "\uD55C\uAD6D\uC5B4" : "English";
|
|
362
|
+
const conventionalGuide = style === "conventional" ? `
|
|
363
|
+
[Conventional Commits \uADDC\uCE59]
|
|
364
|
+
\uBC18\uB4DC\uC2DC \uC544\uB798 \uAD6C\uC870\uB97C \uB530\uB974\uC138\uC694:
|
|
365
|
+
|
|
366
|
+
<type>(<scope>): <subject>
|
|
367
|
+
|
|
368
|
+
<body>
|
|
369
|
+
|
|
370
|
+
\uAD6C\uC870 \uC124\uBA85:
|
|
371
|
+
- type (\uD544\uC218): \uB2E4\uC74C \uC911 \uD558\uB098 \u2014 ${CONVENTIONAL_PREFIXES.join(", ")}
|
|
372
|
+
- scope (\uC120\uD0DD): \uC601\uD5A5 \uBC94\uC704\uB97C \uAD04\uD638 \uC548\uC5D0 \uD45C\uAE30 (\uC608: auth, api, ui)
|
|
373
|
+
- subject (\uD544\uC218): 50\uC790 \uC774\uB0B4, \uBA85\uB839\uC870, \uD575\uC2EC \uC694\uC57D
|
|
374
|
+
- body (\uC120\uD0DD): 72\uC790/\uC904 \uC81C\uD55C, "\uC65C" \uBCC0\uACBD\uD588\uB294\uC9C0 bullet(-) \uBAA9\uB85D\uC73C\uB85C \uC124\uBA85
|
|
375
|
+
|
|
376
|
+
[Type \uC120\uD0DD \uAE30\uC900]
|
|
377
|
+
- feat: \uC0C8\uB85C\uC6B4 \uAE30\uB2A5 \uCD94\uAC00
|
|
378
|
+
- fix: \uBC84\uADF8 \uC218\uC815
|
|
379
|
+
- refactor: \uAE30\uB2A5 \uBCC0\uACBD \uC5C6\uB294 \uCF54\uB4DC \uAC1C\uC120
|
|
380
|
+
- docs: \uBB38\uC11C \uBCC0\uACBD
|
|
381
|
+
- style: \uD3EC\uB9F7\uD305, \uC138\uBBF8\uCF5C\uB860 \uB4F1 (\uB85C\uC9C1 \uBCC0\uACBD \uC5C6\uC74C)
|
|
382
|
+
- test: \uD14C\uC2A4\uD2B8 \uCD94\uAC00/\uC218\uC815
|
|
383
|
+
- chore: \uBE4C\uB4DC, \uC124\uC815, \uC758\uC874\uC131 \uBCC0\uACBD
|
|
384
|
+
- perf: \uC131\uB2A5 \uAC1C\uC120
|
|
385
|
+
- ci: CI/CD \uC124\uC815
|
|
386
|
+
- build: \uBE4C\uB4DC \uC2DC\uC2A4\uD15C \uBCC0\uACBD
|
|
387
|
+
- revert: \uC774\uC804 \uCEE4\uBC0B \uB418\uB3CC\uB9BC` : "";
|
|
388
|
+
return `\uC544\uB798\uC758 [Git Diff]\uB97C \uBD84\uC11D\uD558\uC5EC Git Commit Message\uB97C \uC791\uC131\uD558\uB77C.
|
|
389
|
+
|
|
390
|
+
[CRITICAL INSTRUCTION]
|
|
391
|
+
**\uACB0\uACFC\uB294 \uBB34\uC870\uAC74 '${langLabel}'\uB85C \uC791\uC131\uB418\uC5B4\uC57C \uD55C\uB2E4.**
|
|
392
|
+
${conventionalGuide}
|
|
393
|
+
|
|
394
|
+
[\uC791\uC131 \uC6D0\uCE59]
|
|
395
|
+
1. subject\uB294 "\uBB34\uC5C7\uC744" \uD588\uB294\uC9C0 \u2014 \uBA85\uB839\uC870 \uC0AC\uC6A9 ("\uCD94\uAC00", "\uC218\uC815", "\uC81C\uAC70")
|
|
396
|
+
2. body\uB294 "\uC65C" \uBCC0\uACBD\uD588\uB294\uC9C0 \u2014 \uAD6C\uCCB4\uC801 \uBCC0\uACBD \uB0B4\uC6A9\uC744 bullet(-)\uB85C \uB098\uC5F4
|
|
397
|
+
3. \uD558\uB098\uC758 \uBA54\uC2DC\uC9C0\uB294 \uD558\uB098\uC758 \uB17C\uB9AC\uC801 \uBCC0\uACBD\uB9CC \uC124\uBA85
|
|
398
|
+
4. \uBD80\uC218 \uD6A8\uACFC\uAC00 \uC788\uC73C\uBA74 body\uC5D0 \uBA85\uC2DC
|
|
399
|
+
|
|
400
|
+
[\uC88B\uC740 \uC608\uC2DC]
|
|
401
|
+
feat(auth): JWT \uAE30\uBC18 \uC778\uC99D \uBBF8\uB4E4\uC6E8\uC5B4 \uAD6C\uD604
|
|
402
|
+
|
|
403
|
+
- Access/Refresh \uD1A0\uD070 \uBC1C\uAE09 \uB85C\uC9C1 \uCD94\uAC00
|
|
404
|
+
- \uD1A0\uD070 \uB9CC\uB8CC \uC2DC \uC790\uB3D9 \uAC31\uC2E0 \uCC98\uB9AC
|
|
405
|
+
- \uC778\uC99D \uC2E4\uD328 \uC2DC 401 \uC751\uB2F5 \uD1B5\uC77C
|
|
406
|
+
|
|
407
|
+
[\uB098\uC05C \uC608\uC2DC \u2014 \uC808\uB300 \uC774\uB807\uAC8C \uC791\uC131\uD558\uC9C0 \uB9D0 \uAC83]
|
|
408
|
+
- "\uC218\uC815\uD568" (type \uC5C6\uC74C, \uBB34\uC5C7\uC744 \uC218\uC815\uD588\uB294\uC9C0 \uBD88\uBA85)
|
|
409
|
+
- "fix: \uBC84\uADF8 \uC218\uC815" (\uC5B4\uB5A4 \uBC84\uADF8\uC778\uC9C0 \uBD88\uBA85)
|
|
410
|
+
- "\uC5EC\uB7EC\uAC00\uC9C0 \uC218\uC815 \uBC0F \uAE30\uB2A5 \uCD94\uAC00" (\uD558\uB098\uC758 \uCEE4\uBC0B\uC5D0 \uC5EC\uB7EC \uBCC0\uACBD \uD63C\uD569)
|
|
411
|
+
|
|
412
|
+
[\uCD9C\uB825 \uADDC\uCE59]
|
|
413
|
+
- \uB9C8\uD06C\uB2E4\uC6B4 \uCF54\uB4DC \uBE14\uB85D(\`\`\`)\uC774\uB098 \uBD80\uAC00 \uC124\uBA85 \uC5C6\uC774, \uC624\uC9C1 \uCEE4\uBC0B \uBA54\uC2DC\uC9C0 \uD14D\uC2A4\uD2B8\uB9CC \uCD9C\uB825
|
|
414
|
+
- \uC5B4\uB5A0\uD55C \uB3C4\uAD6C(Functions/Tools)\uB3C4 \uC0AC\uC6A9\uD558\uC9C0 \uB9D0 \uAC83
|
|
415
|
+
|
|
416
|
+
[Git Diff]
|
|
417
|
+
${diff}`;
|
|
418
|
+
}
|
|
419
|
+
function buildRetryPrompt(invalidMessage, language) {
|
|
420
|
+
const langLabel = language === "ko" ? "\uD55C\uAD6D\uC5B4" : "English";
|
|
421
|
+
return `\uC544\uB798 \uCEE4\uBC0B \uBA54\uC2DC\uC9C0\uAC00 Conventional Commits \uD615\uC2DD\uC5D0 \uB9DE\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC218\uC815\uD574\uC8FC\uC138\uC694.
|
|
422
|
+
|
|
423
|
+
[\uD604\uC7AC \uBA54\uC2DC\uC9C0]
|
|
424
|
+
${invalidMessage}
|
|
425
|
+
|
|
426
|
+
[\uD544\uC218 \uAD6C\uC870]
|
|
427
|
+
<type>(<scope>): <subject>
|
|
428
|
+
|
|
429
|
+
<body>
|
|
430
|
+
|
|
431
|
+
[\uADDC\uCE59]
|
|
432
|
+
- type\uC740 \uBC18\uB4DC\uC2DC \uB2E4\uC74C \uC911 \uD558\uB098: ${CONVENTIONAL_PREFIXES.join(", ")}
|
|
433
|
+
- scope\uB294 \uC120\uD0DD\uC0AC\uD56D (\uAD04\uD638 \uC548\uC5D0 \uC601\uD5A5 \uBC94\uC704)
|
|
434
|
+
- subject\uB294 50\uC790 \uC774\uB0B4, \uBA85\uB839\uC870 \uC0AC\uC6A9
|
|
435
|
+
- body\uB294 72\uC790/\uC904 \uC81C\uD55C, bullet(-) \uBAA9\uB85D
|
|
436
|
+
- ${langLabel}\uB85C \uC791\uC131
|
|
437
|
+
- \uC218\uC815\uB41C \uCEE4\uBC0B \uBA54\uC2DC\uC9C0\uB9CC \uCD9C\uB825 (\uB2E4\uB978 \uC124\uBA85 \uC5C6\uC774)`;
|
|
438
|
+
}
|
|
439
|
+
function buildConflictPrompt(localContent, remoteContent) {
|
|
440
|
+
return `\uC544\uB798\uC5D0 Git \uCDA9\uB3CC\uC774 \uBC1C\uC0DD\uD55C \uD30C\uC77C\uC758 [\uB85C\uCEEC \uBC84\uC804]\uACFC [\uC6D0\uACA9 \uBC84\uC804]\uC774 \uC788\uC2B5\uB2C8\uB2E4.
|
|
441
|
+
\uB450 \uBC84\uC804\uC744 \uBD84\uC11D\uD558\uC5EC **\uC62C\uBC14\uB974\uAC8C \uBCD1\uD569\uB41C \uCD5C\uC885 \uD30C\uC77C \uB0B4\uC6A9**\uC744 \uC0DD\uC131\uD574\uC8FC\uC138\uC694.
|
|
442
|
+
|
|
443
|
+
[\uD544\uC218 \uADDC\uCE59]
|
|
444
|
+
1. \uB450 \uBC84\uC804\uC758 \uBCC0\uACBD \uC0AC\uD56D\uC744 \uBAA8\uB450 \uD3EC\uD568\uD558\uC5EC \uBCD1\uD569\uD560 \uAC83
|
|
445
|
+
2. \uCDA9\uB3CC \uB9C8\uCEE4(<<<<<<, ======, >>>>>>)\uB294 \uC808\uB300 \uD3EC\uD568\uD558\uC9C0 \uB9D0 \uAC83
|
|
446
|
+
3. \uCF54\uB4DC\uC758 \uB17C\uB9AC\uC801 \uC77C\uAD00\uC131\uC744 \uC720\uC9C0\uD560 \uAC83
|
|
447
|
+
4. \uCD9C\uB825\uC740 **\uC624\uC9C1 \uBCD1\uD569\uB41C \uD30C\uC77C \uB0B4\uC6A9\uB9CC** \uCD9C\uB825\uD560 \uAC83
|
|
448
|
+
|
|
449
|
+
[\uB85C\uCEEC \uBC84\uC804]
|
|
450
|
+
${localContent}
|
|
451
|
+
|
|
452
|
+
[\uC6D0\uACA9 \uBC84\uC804]
|
|
453
|
+
${remoteContent}`;
|
|
454
|
+
}
|
|
455
|
+
function buildDiffSummaryPrompt(diff) {
|
|
456
|
+
return `\uC544\uB798 Git Diff\uAC00 \uB108\uBB34 \uD07D\uB2C8\uB2E4. \uD575\uC2EC \uBCC0\uACBD \uC0AC\uD56D\uB9CC \uC694\uC57D\uD574\uC8FC\uC138\uC694.
|
|
457
|
+
|
|
458
|
+
[\uADDC\uCE59]
|
|
459
|
+
1. \uC5B4\uB5A4 \uD30C\uC77C\uC5D0\uC11C \uBB34\uC5C7\uC774 \uBCC0\uACBD\uB418\uC5C8\uB294\uC9C0 \uC694\uC57D
|
|
460
|
+
2. \uCD94\uAC00/\uC218\uC815/\uC0AD\uC81C\uB41C \uC8FC\uC694 \uD568\uC218/\uD074\uB798\uC2A4/\uBCC0\uC218 \uB098\uC5F4
|
|
461
|
+
3. diff \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825 (+ / - \uC811\uB450\uC0AC \uC0AC\uC6A9)
|
|
462
|
+
4. 500\uC790 \uC774\uB0B4\uB85C \uC694\uC57D
|
|
463
|
+
|
|
464
|
+
[Diff]
|
|
465
|
+
${diff}`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/logger.ts
|
|
469
|
+
import pino from "pino";
|
|
470
|
+
import { join as join2 } from "path";
|
|
471
|
+
import { mkdirSync } from "fs";
|
|
472
|
+
import { homedir } from "os";
|
|
473
|
+
function createLogger() {
|
|
474
|
+
const logDir = join2(homedir(), ".smart-commit", "logs");
|
|
475
|
+
try {
|
|
476
|
+
mkdirSync(logDir, { recursive: true });
|
|
477
|
+
} catch {
|
|
478
|
+
return pino({ level: "info" });
|
|
479
|
+
}
|
|
480
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
481
|
+
const logFile = join2(logDir, `${today}.log`);
|
|
482
|
+
return pino(
|
|
483
|
+
{ level: "info" },
|
|
484
|
+
pino.destination({ dest: logFile, append: true, sync: false })
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export {
|
|
489
|
+
loadConfig,
|
|
490
|
+
scanRepositories,
|
|
491
|
+
getOfflineTemplates,
|
|
492
|
+
isAiAvailable,
|
|
493
|
+
createAiClient,
|
|
494
|
+
createLogger
|
|
495
|
+
};
|
|
496
|
+
//# sourceMappingURL=chunk-LYR3U55E.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/scanner.ts","../src/ai-client.ts","../src/logger.ts"],"sourcesContent":["import { cosmiconfig } from \"cosmiconfig\";\nimport type { SmartCommitConfig } from \"./types.js\";\n\nconst DEFAULT_CONFIG: SmartCommitConfig = {\n ai: {\n primary: \"gemini\",\n fallback: \"claude\",\n timeout: 30,\n },\n safety: {\n maxFileSize: \"10MB\",\n blockedPatterns: [\n \"*.env\",\n \".env.*\",\n \"*.pem\",\n \"*.key\",\n \"credentials*\",\n \"*.sqlite\",\n \"*.sqlite3\",\n ],\n warnPatterns: [\n \"*.log\",\n \"*.csv\",\n \"package-lock.json\",\n \"yarn.lock\",\n \"pnpm-lock.yaml\",\n ],\n },\n commit: {\n style: \"conventional\",\n language: \"ko\",\n maxDiffSize: 10000,\n },\n grouping: {\n strategy: \"smart\",\n },\n};\n\nexport async function loadConfig(\n cliOptions: Record<string, unknown> = {},\n): Promise<SmartCommitConfig> {\n const explorer = cosmiconfig(\"smart-commit\", {\n searchPlaces: [\n \".smart-commitrc\",\n \".smart-commitrc.yaml\",\n \".smart-commitrc.yml\",\n \".smart-commitrc.json\",\n \"smart-commit.config.js\",\n \"package.json\",\n ],\n });\n\n const result = await explorer.search();\n const fileConfig = result?.config ?? {};\n\n const config = deepMerge(\n DEFAULT_CONFIG as unknown as Record<string, unknown>,\n fileConfig as Record<string, unknown>,\n ) as unknown as SmartCommitConfig;\n\n if (cliOptions.ai && typeof cliOptions.ai === \"string\") {\n config.ai.primary = cliOptions.ai as string;\n }\n if (cliOptions.group && typeof cliOptions.group === \"string\") {\n config.grouping.strategy = cliOptions.group as \"smart\" | \"single\" | \"manual\";\n }\n\n return config;\n}\n\nfunction deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {\n const result = { ...target };\n for (const key of Object.keys(source)) {\n if (\n source[key] &&\n typeof source[key] === \"object\" &&\n !Array.isArray(source[key]) &&\n target[key] &&\n typeof target[key] === \"object\" &&\n !Array.isArray(target[key])\n ) {\n result[key] = deepMerge(\n target[key] as Record<string, unknown>,\n source[key] as Record<string, unknown>,\n );\n } else {\n result[key] = source[key];\n }\n }\n return result;\n}\n","import { simpleGit, type SimpleGit } from \"simple-git\";\nimport { readdir, stat, access } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport type { RepoState, RepoGitStatus, FileChange } from \"./types.js\";\nimport type { UI } from \"./ui.js\";\nimport type { Logger } from \"pino\";\n\nexport async function scanRepositories(\n baseDir: string,\n ui: UI,\n logger: Logger,\n): Promise<RepoState[]> {\n const gitDirs = await findGitDirs(baseDir);\n const repos: RepoState[] = [];\n\n ui.showProgress(\"Scanning repositories...\", 0, gitDirs.length);\n\n for (let i = 0; i < gitDirs.length; i++) {\n const dir = gitDirs[i];\n ui.showProgress(`Scanning: ${dir}`, i + 1, gitDirs.length);\n\n try {\n const repo = await inspectRepo(dir, logger);\n repos.push(repo);\n } catch (err) {\n logger.warn({ dir, err }, \"Failed to inspect repository\");\n }\n }\n\n return repos;\n}\n\nasync function findGitDirs(baseDir: string): Promise<string[]> {\n const dirs: string[] = [];\n const entries = await readdir(baseDir, { withFileTypes: true });\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n if (entry.name === \"node_modules\" || entry.name.startsWith(\".\")) continue;\n\n const fullPath = join(baseDir, entry.name);\n const gitPath = join(fullPath, \".git\");\n\n try {\n await access(gitPath);\n dirs.push(fullPath);\n } catch {\n // not a git repo, check subdirectories\n const subDirs = await findGitDirs(fullPath);\n dirs.push(...subDirs);\n }\n }\n\n // also check if baseDir itself is a git repo\n try {\n const selfGit = join(baseDir, \".git\");\n await access(selfGit);\n if (!dirs.some((d) => d === baseDir)) {\n dirs.unshift(baseDir);\n }\n } catch {\n // baseDir is not a git repo\n }\n\n return dirs;\n}\n\nasync function inspectRepo(dir: string, logger: Logger): Promise<RepoState> {\n const git: SimpleGit = simpleGit(dir);\n\n const gitStatus = await detectGitStatus(dir, git);\n\n if (gitStatus === \"locked\") {\n logger.warn({ dir }, \"Git index locked — skipping\");\n return { path: dir, branch: \"\", status: \"locked\", files: [], unpushedCommits: 0 };\n }\n if (gitStatus === \"detached\") {\n logger.warn({ dir }, \"Detached HEAD — skipping\");\n return { path: dir, branch: \"HEAD (detached)\", status: \"detached\", files: [], unpushedCommits: 0 };\n }\n if (gitStatus === \"rebasing\") {\n logger.warn({ dir }, \"Rebase in progress — skipping\");\n return { path: dir, branch: \"\", status: \"rebasing\", files: [], unpushedCommits: 0 };\n }\n\n const statusResult = await git.status();\n const branch = statusResult.current ?? \"unknown\";\n\n const files: FileChange[] = [];\n for (const f of statusResult.files) {\n const filePath = join(dir, f.path);\n let size = 0;\n try {\n const s = await stat(filePath);\n size = s.size;\n } catch {\n // file might have been deleted\n }\n\n files.push({\n path: f.path,\n status: mapGitStatus(f.working_dir, f.index),\n size,\n isBinary: false, // will be checked by classifier\n });\n }\n\n let unpushedCommits = 0;\n try {\n const log = await git.log([\"@{u}..HEAD\"]);\n unpushedCommits = log.total;\n } catch {\n // no upstream set\n }\n\n const repoStatus: RepoGitStatus =\n gitStatus === \"merging\"\n ? \"merging\"\n : files.length > 0\n ? \"dirty\"\n : \"clean\";\n\n return { path: dir, branch, status: repoStatus, files, unpushedCommits };\n}\n\nasync function detectGitStatus(dir: string, git: SimpleGit): Promise<RepoGitStatus> {\n // Check lock file\n try {\n await access(join(dir, \".git\", \"index.lock\"));\n return \"locked\";\n } catch {\n // no lock\n }\n\n // Check rebase\n try {\n await access(join(dir, \".git\", \"rebase-merge\"));\n return \"rebasing\";\n } catch {\n // not rebasing\n }\n try {\n await access(join(dir, \".git\", \"rebase-apply\"));\n return \"rebasing\";\n } catch {\n // not rebasing\n }\n\n // Check merge\n try {\n await access(join(dir, \".git\", \"MERGE_HEAD\"));\n return \"merging\";\n } catch {\n // not merging\n }\n\n // Check detached HEAD\n try {\n await git.raw([\"symbolic-ref\", \"HEAD\"]);\n } catch {\n return \"detached\";\n }\n\n return \"clean\";\n}\n\nfunction mapGitStatus(workingDir: string, index: string): FileChange[\"status\"] {\n if (index === \"?\" || workingDir === \"?\") return \"untracked\";\n if (index === \"A\" || workingDir === \"A\") return \"added\";\n if (index === \"D\" || workingDir === \"D\") return \"deleted\";\n if (index === \"R\" || workingDir === \"R\") return \"renamed\";\n return \"modified\";\n}\n","import { execa } from \"execa\";\nimport type { SmartCommitConfig, AiTool } from \"./types.js\";\nimport type { Logger } from \"pino\";\n\nconst CONVENTIONAL_PREFIXES = [\n \"feat\", \"fix\", \"refactor\", \"docs\", \"style\", \"test\", \"chore\", \"perf\", \"ci\", \"build\", \"revert\",\n];\nconst CONVENTIONAL_RE = new RegExp(`^(${CONVENTIONAL_PREFIXES.join(\"|\")})(\\\\(.+\\\\))?!?:\\\\s.+`);\n\n// ─── Offline templates ───\n\nconst OFFLINE_TEMPLATES = CONVENTIONAL_PREFIXES.map((prefix) => `${prefix}: `);\n\nexport function getOfflineTemplates(): string[] {\n return OFFLINE_TEMPLATES;\n}\n\nexport async function isAiAvailable(tool: AiTool): Promise<boolean> {\n try {\n const cmd = tool === \"gpt\" ? \"openai\" : tool;\n await execa(\"which\", [cmd], { timeout: 3000 });\n return true;\n } catch {\n return false;\n }\n}\n\nexport interface AiClient {\n generateCommitMessage(diff: string, language: string): Promise<string | null>;\n resolveConflict(localContent: string, remoteContent: string): Promise<string | null>;\n groupFiles(fileList: string): Promise<string | null>;\n summarizeDiff(diff: string): Promise<string>;\n}\n\nexport function createAiClient(config: SmartCommitConfig, logger: Logger): AiClient {\n async function callWithFallback(prompt: string): Promise<string | null> {\n let result = await callAi(config.ai.primary, prompt, config.ai.timeout, logger, config);\n if (!result && config.ai.fallback !== config.ai.primary) {\n logger.warn({ fallback: config.ai.fallback }, \"Primary AI failed, trying fallback\");\n result = await callAi(config.ai.fallback, prompt, config.ai.timeout, logger, config);\n }\n return result;\n }\n\n return {\n async generateCommitMessage(diff, language) {\n const summarized = await this.summarizeDiff(diff);\n const prompt = buildCommitPrompt(summarized, language, config.commit.style);\n\n logger.info({ tool: config.ai.primary, diffLength: summarized.length }, \"Requesting commit message\");\n\n let result = await callWithFallback(prompt);\n\n if (result) {\n // Conventional commit validation + retry\n if (config.commit.style === \"conventional\" && !validateConventionalCommit(result)) {\n logger.warn({ message: result.split(\"\\n\")[0] }, \"Invalid conventional commit, retrying\");\n const retryPrompt = buildRetryPrompt(result, language);\n const retried = await callWithFallback(retryPrompt);\n if (retried && validateConventionalCommit(retried)) {\n result = retried;\n }\n // use original if retry also fails — better than nothing\n }\n\n // Strip markdown code blocks if AI wrapped it\n result = stripCodeBlocks(result);\n\n logger.info({ messageLength: result.length }, \"Commit message generated\");\n }\n\n return result;\n },\n\n async resolveConflict(localContent, remoteContent) {\n const prompt = buildConflictPrompt(localContent, remoteContent);\n return callWithFallback(prompt);\n },\n\n async groupFiles(fileList) {\n const { buildGroupingPrompt } = await import(\"./classifier.js\");\n const prompt = buildGroupingPrompt(fileList);\n return callWithFallback(prompt);\n },\n\n async summarizeDiff(diff) {\n if (diff.length <= config.commit.maxDiffSize) {\n return diff;\n }\n\n // Smart truncation: stat header + most important hunks\n const statSection = extractDiffStat(diff);\n const hunks = extractKeyHunks(diff, config.commit.maxDiffSize - statSection.length - 200);\n\n const truncated = `${statSection}\\n\\n[주요 변경 내용 (전체 ${diff.length}자 중 핵심부만 추출)]\\n${hunks}`;\n\n // If still too large, ask AI to summarize\n if (truncated.length > config.commit.maxDiffSize * 1.5) {\n logger.info(\"Diff too large, requesting AI summary\");\n const summaryPrompt = buildDiffSummaryPrompt(truncated.slice(0, config.commit.maxDiffSize));\n const summary = await callWithFallback(summaryPrompt);\n return summary ?? truncated.slice(0, config.commit.maxDiffSize);\n }\n\n return truncated;\n },\n };\n}\n\n// ─── Conventional commit validation ───\n\nexport function validateConventionalCommit(message: string): boolean {\n const firstLine = message.split(\"\\n\")[0].trim();\n return CONVENTIONAL_RE.test(firstLine);\n}\n\nfunction stripCodeBlocks(text: string): string {\n return text\n .replace(/^```[\\w]*\\n?/gm, \"\")\n .replace(/^```\\s*$/gm, \"\")\n .trim();\n}\n\n// ─── Diff summarization ───\n\nfunction extractDiffStat(diff: string): string {\n const lines = diff.split(\"\\n\");\n const statLines: string[] = [];\n\n for (const line of lines) {\n if (line.startsWith(\"diff --git\")) {\n statLines.push(line);\n } else if (line.startsWith(\"--- \") || line.startsWith(\"+++ \")) {\n statLines.push(line);\n }\n }\n\n return statLines.join(\"\\n\");\n}\n\nfunction extractKeyHunks(diff: string, maxLength: number): string {\n const hunks: string[] = [];\n let currentHunk = \"\";\n let totalLength = 0;\n\n for (const line of diff.split(\"\\n\")) {\n if (line.startsWith(\"@@\")) {\n if (currentHunk && totalLength + currentHunk.length <= maxLength) {\n hunks.push(currentHunk);\n totalLength += currentHunk.length;\n }\n currentHunk = line + \"\\n\";\n } else if (line.startsWith(\"+\") || line.startsWith(\"-\")) {\n // Prioritize actual changes over context\n currentHunk += line + \"\\n\";\n }\n }\n\n // Don't forget the last hunk\n if (currentHunk && totalLength + currentHunk.length <= maxLength) {\n hunks.push(currentHunk);\n }\n\n return hunks.join(\"\\n\");\n}\n\n// ─── AI call ───\n\nasync function callAi(\n tool: AiTool,\n prompt: string,\n timeout: number,\n logger: Logger,\n config?: SmartCommitConfig,\n): Promise<string | null> {\n try {\n const { command, args } = buildAiCommand(tool, prompt, config);\n\n const { stdout } = await execa(command, args, {\n timeout: timeout * 1000,\n stdin: \"ignore\",\n });\n\n const trimmed = stdout.trim();\n return trimmed || null;\n } catch (err) {\n logger.error({ tool, err }, \"AI call failed\");\n return null;\n }\n}\n\nfunction buildAiCommand(\n tool: AiTool,\n prompt: string,\n config?: SmartCommitConfig,\n): { command: string; args: string[] } {\n switch (tool) {\n case \"gemini\":\n return { command: \"gemini\", args: [prompt] };\n case \"claude\":\n return { command: \"claude\", args: [\"-p\", prompt] };\n case \"gpt\":\n // OpenAI CLI: https://platform.openai.com/docs/guides/command-line\n return { command: \"openai\", args: [\"api\", \"chat.completions.create\", \"-m\", \"gpt-4o\", \"-g\", \"user\", prompt] };\n case \"ollama\": {\n const model = config?.ai?.ollama?.model ?? \"llama3\";\n return { command: \"ollama\", args: [\"run\", model, prompt] };\n }\n default:\n // Generic: treat tool name as command, pass prompt as first arg\n return { command: tool, args: [prompt] };\n }\n}\n\n// ─── Prompt builders ───\n\nfunction buildCommitPrompt(diff: string, language: string, style: string): string {\n const langLabel = language === \"ko\" ? \"한국어\" : \"English\";\n\n const conventionalGuide = style === \"conventional\" ? `\n[Conventional Commits 규칙]\n반드시 아래 구조를 따르세요:\n\n<type>(<scope>): <subject>\n\n<body>\n\n구조 설명:\n- type (필수): 다음 중 하나 — ${CONVENTIONAL_PREFIXES.join(\", \")}\n- scope (선택): 영향 범위를 괄호 안에 표기 (예: auth, api, ui)\n- subject (필수): 50자 이내, 명령조, 핵심 요약\n- body (선택): 72자/줄 제한, \"왜\" 변경했는지 bullet(-) 목록으로 설명\n\n[Type 선택 기준]\n- feat: 새로운 기능 추가\n- fix: 버그 수정\n- refactor: 기능 변경 없는 코드 개선\n- docs: 문서 변경\n- style: 포맷팅, 세미콜론 등 (로직 변경 없음)\n- test: 테스트 추가/수정\n- chore: 빌드, 설정, 의존성 변경\n- perf: 성능 개선\n- ci: CI/CD 설정\n- build: 빌드 시스템 변경\n- revert: 이전 커밋 되돌림` : \"\";\n\n return `아래의 [Git Diff]를 분석하여 Git Commit Message를 작성하라.\n\n[CRITICAL INSTRUCTION]\n**결과는 무조건 '${langLabel}'로 작성되어야 한다.**\n${conventionalGuide}\n\n[작성 원칙]\n1. subject는 \"무엇을\" 했는지 — 명령조 사용 (\"추가\", \"수정\", \"제거\")\n2. body는 \"왜\" 변경했는지 — 구체적 변경 내용을 bullet(-)로 나열\n3. 하나의 메시지는 하나의 논리적 변경만 설명\n4. 부수 효과가 있으면 body에 명시\n\n[좋은 예시]\nfeat(auth): JWT 기반 인증 미들웨어 구현\n\n- Access/Refresh 토큰 발급 로직 추가\n- 토큰 만료 시 자동 갱신 처리\n- 인증 실패 시 401 응답 통일\n\n[나쁜 예시 — 절대 이렇게 작성하지 말 것]\n- \"수정함\" (type 없음, 무엇을 수정했는지 불명)\n- \"fix: 버그 수정\" (어떤 버그인지 불명)\n- \"여러가지 수정 및 기능 추가\" (하나의 커밋에 여러 변경 혼합)\n\n[출력 규칙]\n- 마크다운 코드 블록(\\`\\`\\`)이나 부가 설명 없이, 오직 커밋 메시지 텍스트만 출력\n- 어떠한 도구(Functions/Tools)도 사용하지 말 것\n\n[Git Diff]\n${diff}`;\n}\n\nfunction buildRetryPrompt(invalidMessage: string, language: string): string {\n const langLabel = language === \"ko\" ? \"한국어\" : \"English\";\n return `아래 커밋 메시지가 Conventional Commits 형식에 맞지 않습니다. 수정해주세요.\n\n[현재 메시지]\n${invalidMessage}\n\n[필수 구조]\n<type>(<scope>): <subject>\n\n<body>\n\n[규칙]\n- type은 반드시 다음 중 하나: ${CONVENTIONAL_PREFIXES.join(\", \")}\n- scope는 선택사항 (괄호 안에 영향 범위)\n- subject는 50자 이내, 명령조 사용\n- body는 72자/줄 제한, bullet(-) 목록\n- ${langLabel}로 작성\n- 수정된 커밋 메시지만 출력 (다른 설명 없이)`;\n}\n\nfunction buildConflictPrompt(localContent: string, remoteContent: string): string {\n return `아래에 Git 충돌이 발생한 파일의 [로컬 버전]과 [원격 버전]이 있습니다.\n두 버전을 분석하여 **올바르게 병합된 최종 파일 내용**을 생성해주세요.\n\n[필수 규칙]\n1. 두 버전의 변경 사항을 모두 포함하여 병합할 것\n2. 충돌 마커(<<<<<<, ======, >>>>>>)는 절대 포함하지 말 것\n3. 코드의 논리적 일관성을 유지할 것\n4. 출력은 **오직 병합된 파일 내용만** 출력할 것\n\n[로컬 버전]\n${localContent}\n\n[원격 버전]\n${remoteContent}`;\n}\n\nfunction buildDiffSummaryPrompt(diff: string): string {\n return `아래 Git Diff가 너무 큽니다. 핵심 변경 사항만 요약해주세요.\n\n[규칙]\n1. 어떤 파일에서 무엇이 변경되었는지 요약\n2. 추가/수정/삭제된 주요 함수/클래스/변수 나열\n3. diff 형식으로 출력 (+ / - 접두사 사용)\n4. 500자 이내로 요약\n\n[Diff]\n${diff}`;\n}\n","import pino from \"pino\";\nimport { join } from \"node:path\";\nimport { mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\n\nexport function createLogger(): pino.Logger {\n const logDir = join(homedir(), \".smart-commit\", \"logs\");\n\n try {\n mkdirSync(logDir, { recursive: true });\n } catch {\n // fallback: log to stderr only\n return pino({ level: \"info\" });\n }\n\n const today = new Date().toISOString().slice(0, 10);\n const logFile = join(logDir, `${today}.log`);\n\n return pino(\n { level: \"info\" },\n pino.destination({ dest: logFile, append: true, sync: false }),\n );\n}\n"],"mappings":"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","names":["join"]}
|
|
@@ -3,19 +3,42 @@
|
|
|
3
3
|
// src/classifier.ts
|
|
4
4
|
import { minimatch } from "minimatch";
|
|
5
5
|
import { dirname, extname } from "path";
|
|
6
|
+
import { readFile } from "fs/promises";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { execFileSync } from "child_process";
|
|
6
10
|
var SIZE_UNITS = {
|
|
7
11
|
B: 1,
|
|
8
12
|
KB: 1024,
|
|
9
13
|
MB: 1024 * 1024,
|
|
10
14
|
GB: 1024 * 1024 * 1024
|
|
11
15
|
};
|
|
12
|
-
|
|
16
|
+
var globalIgnorePatterns = null;
|
|
17
|
+
async function loadGlobalGitignore() {
|
|
18
|
+
if (globalIgnorePatterns !== null) return globalIgnorePatterns;
|
|
19
|
+
globalIgnorePatterns = [];
|
|
20
|
+
try {
|
|
21
|
+
const gitignorePath = execFileSync("git", ["config", "--global", "core.excludesFile"], {
|
|
22
|
+
timeout: 3e3
|
|
23
|
+
}).toString().trim();
|
|
24
|
+
if (gitignorePath) {
|
|
25
|
+
const resolved = gitignorePath.startsWith("~") ? join(homedir(), gitignorePath.slice(1)) : gitignorePath;
|
|
26
|
+
const content = await readFile(resolved, "utf-8");
|
|
27
|
+
globalIgnorePatterns = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
return globalIgnorePatterns;
|
|
32
|
+
}
|
|
33
|
+
async function classifyFiles(files, config) {
|
|
13
34
|
const maxBytes = parseSize(config.safety.maxFileSize);
|
|
35
|
+
const globalPatterns = await loadGlobalGitignore();
|
|
36
|
+
const allBlockedPatterns = [...config.safety.blockedPatterns, ...globalPatterns];
|
|
14
37
|
const blocked = [];
|
|
15
38
|
const warned = [];
|
|
16
39
|
const safe = [];
|
|
17
40
|
for (const file of files) {
|
|
18
|
-
if (isBlocked(file,
|
|
41
|
+
if (isBlocked(file, allBlockedPatterns, maxBytes)) {
|
|
19
42
|
blocked.push(file);
|
|
20
43
|
} else if (isWarned(file, config.safety.warnPatterns)) {
|
|
21
44
|
warned.push(file);
|
|
@@ -154,9 +177,10 @@ function parseSize(sizeStr) {
|
|
|
154
177
|
}
|
|
155
178
|
|
|
156
179
|
export {
|
|
180
|
+
loadGlobalGitignore,
|
|
157
181
|
classifyFiles,
|
|
158
182
|
groupFiles,
|
|
159
183
|
ruleBasedGrouping,
|
|
160
184
|
buildGroupingPrompt
|
|
161
185
|
};
|
|
162
|
-
//# sourceMappingURL=chunk-
|
|
186
|
+
//# sourceMappingURL=chunk-MYMEBX2Q.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/classifier.ts"],"sourcesContent":["import { minimatch } from \"minimatch\";\nimport { dirname, extname } from \"node:path\";\nimport { readFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { execFileSync } from \"node:child_process\";\nimport type {\n FileChange,\n SmartCommitConfig,\n SafetyResult,\n CommitGroup,\n AiGroupingResult,\n} from \"./types.js\";\nimport type { Logger } from \"pino\";\n\nconst SIZE_UNITS: Record<string, number> = {\n B: 1,\n KB: 1024,\n MB: 1024 * 1024,\n GB: 1024 * 1024 * 1024,\n};\n\n// ─── Global gitignore cache ───\n\nlet globalIgnorePatterns: string[] | null = null;\n\nexport async function loadGlobalGitignore(): Promise<string[]> {\n if (globalIgnorePatterns !== null) return globalIgnorePatterns;\n\n globalIgnorePatterns = [];\n\n try {\n // Get global gitignore path from git config\n const gitignorePath = execFileSync(\"git\", [\"config\", \"--global\", \"core.excludesFile\"], {\n timeout: 3000,\n }).toString().trim();\n\n if (gitignorePath) {\n const resolved = gitignorePath.startsWith(\"~\")\n ? join(homedir(), gitignorePath.slice(1))\n : gitignorePath;\n\n const content = await readFile(resolved, \"utf-8\");\n globalIgnorePatterns = content\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter((line) => line && !line.startsWith(\"#\"));\n }\n } catch {\n // No global gitignore configured — that's fine\n }\n\n return globalIgnorePatterns;\n}\n\n// ─── Safety classification ───\n\nexport async function classifyFiles(\n files: FileChange[],\n config: SmartCommitConfig,\n): Promise<SafetyResult> {\n const maxBytes = parseSize(config.safety.maxFileSize);\n const globalPatterns = await loadGlobalGitignore();\n\n // Merge config blocked patterns with global gitignore\n const allBlockedPatterns = [...config.safety.blockedPatterns, ...globalPatterns];\n\n const blocked: FileChange[] = [];\n const warned: FileChange[] = [];\n const safe: FileChange[] = [];\n\n for (const file of files) {\n if (isBlocked(file, allBlockedPatterns, maxBytes)) {\n blocked.push(file);\n } else if (isWarned(file, config.safety.warnPatterns)) {\n warned.push(file);\n } else {\n safe.push(file);\n }\n }\n\n return { blocked, warned, safe };\n}\n\n// ─── Grouping ───\n\nexport async function groupFiles(\n files: FileChange[],\n strategy: SmartCommitConfig[\"grouping\"][\"strategy\"],\n callAiForGrouping: ((fileList: string) => Promise<string | null>) | null,\n logger: Logger,\n): Promise<CommitGroup[]> {\n if (files.length === 0) return [];\n\n if (strategy === \"single\") {\n return [{ label: \"all\", files, reason: \"single strategy\" }];\n }\n\n if (strategy === \"smart\" && callAiForGrouping) {\n const aiGroups = await tryAiGrouping(files, callAiForGrouping, logger);\n if (aiGroups) return aiGroups;\n logger.warn(\"AI grouping failed, falling back to rule-based grouping\");\n }\n\n return ruleBasedGrouping(files);\n}\n\nasync function tryAiGrouping(\n files: FileChange[],\n callAi: (fileList: string) => Promise<string | null>,\n logger: Logger,\n): Promise<CommitGroup[] | null> {\n const fileList = files\n .map((f) => `${f.status.charAt(0).toUpperCase()} ${f.path}`)\n .join(\"\\n\");\n\n try {\n const response = await callAi(fileList);\n if (!response) return null;\n\n const parsed = parseAiGroupingResponse(response, files);\n if (parsed.length === 0) return null;\n\n logger.info({ groupCount: parsed.length }, \"AI grouping succeeded\");\n return parsed;\n } catch (err) {\n logger.error({ err }, \"AI grouping parse error\");\n return null;\n }\n}\n\nfunction parseAiGroupingResponse(\n response: string,\n allFiles: FileChange[],\n): CommitGroup[] {\n // Try JSON parse first\n try {\n const cleaned = response.replace(/```json?\\s*/g, \"\").replace(/```\\s*/g, \"\").trim();\n const parsed: AiGroupingResult = JSON.parse(cleaned);\n\n if (!parsed.groups || !Array.isArray(parsed.groups)) return [];\n\n const fileMap = new Map(allFiles.map((f) => [f.path, f]));\n const usedFiles = new Set<string>();\n const groups: CommitGroup[] = [];\n\n for (const g of parsed.groups) {\n const matchedFiles = g.files\n .map((path) => fileMap.get(path))\n .filter((f): f is FileChange => f !== undefined && !usedFiles.has(f.path));\n\n for (const f of matchedFiles) usedFiles.add(f.path);\n\n if (matchedFiles.length > 0) {\n groups.push({\n label: g.label,\n files: matchedFiles,\n reason: g.reason,\n });\n }\n }\n\n // remaining files that AI didn't assign\n const remaining = allFiles.filter((f) => !usedFiles.has(f.path));\n if (remaining.length > 0) {\n groups.push({\n label: \"other\",\n files: remaining,\n reason: \"AI가 분류하지 않은 나머지 파일\",\n });\n }\n\n return groups;\n } catch {\n return [];\n }\n}\n\n// ─── Rule-based fallback grouping ───\n\nexport function ruleBasedGrouping(files: FileChange[]): CommitGroup[] {\n const dirMap = new Map<string, FileChange[]>();\n\n for (const file of files) {\n const dir = dirname(file.path);\n const ext = extname(file.path);\n\n // Group by top-level directory + extension category\n const topDir = dir.split(\"/\")[0] || \".\";\n const category = getCategory(ext);\n const key = `${topDir}/${category}`;\n\n if (!dirMap.has(key)) dirMap.set(key, []);\n dirMap.get(key)!.push(file);\n }\n\n const groups: CommitGroup[] = [];\n for (const [key, groupFiles] of dirMap) {\n groups.push({\n label: key,\n files: groupFiles,\n reason: `디렉토리/유형 기반 그룹핑: ${key}`,\n });\n }\n\n return groups;\n}\n\nfunction getCategory(ext: string): string {\n const categories: Record<string, string[]> = {\n source: [\".ts\", \".tsx\", \".js\", \".jsx\", \".py\", \".go\", \".rs\", \".java\", \".kt\", \".swift\", \".c\", \".cpp\", \".h\"],\n style: [\".css\", \".scss\", \".sass\", \".less\", \".styl\"],\n markup: [\".html\", \".xml\", \".svg\", \".vue\", \".svelte\"],\n config: [\".json\", \".yaml\", \".yml\", \".toml\", \".ini\", \".env\", \".conf\"],\n docs: [\".md\", \".txt\", \".rst\", \".adoc\"],\n test: [\".test.ts\", \".test.js\", \".spec.ts\", \".spec.js\", \".test.py\"],\n };\n\n for (const [category, exts] of Object.entries(categories)) {\n if (exts.includes(ext)) return category;\n }\n return \"other\";\n}\n\n// ─── AI grouping prompt builder ───\n\nexport function buildGroupingPrompt(fileList: string): string {\n return `아래 Git 변경 파일 목록을 분석하여 의미 있는 커밋 단위로 그룹핑해주세요.\n\n[규칙]\n1. 관련된 파일끼리 묶어 하나의 커밋 그룹으로 만들어주세요.\n2. 각 그룹에 짧은 라벨(한국어)과 이유를 붙여주세요.\n3. 반드시 아래 JSON 형식으로만 출력하세요. 다른 텍스트 없이 JSON만 출력하세요.\n\n[출력 형식]\n{\"groups\":[{\"label\":\"그룹명\",\"files\":[\"파일경로1\",\"파일경로2\"],\"reason\":\"그룹핑 이유\"}]}\n\n[변경 파일 목록]\n${fileList}`;\n}\n\n// ─── Utilities ───\n\nfunction isBlocked(\n file: FileChange,\n patterns: string[],\n maxBytes: number,\n): boolean {\n if (file.size > maxBytes) return true;\n if (file.isBinary) return true;\n return matchesAny(file.path, patterns);\n}\n\nfunction isWarned(file: FileChange, patterns: string[]): boolean {\n return matchesAny(file.path, patterns);\n}\n\nfunction matchesAny(filePath: string, patterns: string[]): boolean {\n return patterns.some((pattern) =>\n minimatch(filePath, pattern, { dot: true, matchBase: true }),\n );\n}\n\nfunction parseSize(sizeStr: string): number {\n const match = sizeStr.match(/^(\\d+(?:\\.\\d+)?)\\s*(B|KB|MB|GB)$/i);\n if (!match) return 10 * 1024 * 1024; // default 10MB\n const value = parseFloat(match[1]);\n const unit = match[2].toUpperCase();\n return value * (SIZE_UNITS[unit] ?? 1);\n}\n"],"mappings":"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","names":["groupFiles"]}
|
|
@@ -3,12 +3,14 @@ import {
|
|
|
3
3
|
buildGroupingPrompt,
|
|
4
4
|
classifyFiles,
|
|
5
5
|
groupFiles,
|
|
6
|
+
loadGlobalGitignore,
|
|
6
7
|
ruleBasedGrouping
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-MYMEBX2Q.js";
|
|
8
9
|
export {
|
|
9
10
|
buildGroupingPrompt,
|
|
10
11
|
classifyFiles,
|
|
11
12
|
groupFiles,
|
|
13
|
+
loadGlobalGitignore,
|
|
12
14
|
ruleBasedGrouping
|
|
13
15
|
};
|
|
14
|
-
//# sourceMappingURL=classifier-
|
|
16
|
+
//# sourceMappingURL=classifier-AINQPFLU.js.map
|