@blum84/smart-commit 0.2.0 → 0.2.1
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 +27 -455
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.js +236 -0
- package/dist/mcp-server.js.map +1 -0
- package/package.json +6 -3
- package/dist/chunk-ZS27WQDW.js.map +0 -1
- /package/dist/{classifier-TTZQUM7N.js.map → classifier-AINQPFLU.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,443 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createAiClient,
|
|
4
|
+
createLogger,
|
|
5
|
+
getOfflineTemplates,
|
|
6
|
+
isAiAvailable,
|
|
7
|
+
loadConfig,
|
|
8
|
+
scanRepositories
|
|
9
|
+
} from "./chunk-LYR3U55E.js";
|
|
2
10
|
import {
|
|
3
11
|
classifyFiles,
|
|
4
12
|
groupFiles
|
|
5
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-MYMEBX2Q.js";
|
|
6
14
|
|
|
7
15
|
// src/index.ts
|
|
8
16
|
import { Command } from "commander";
|
|
9
17
|
|
|
10
|
-
// src/config.ts
|
|
11
|
-
import { cosmiconfig } from "cosmiconfig";
|
|
12
|
-
var DEFAULT_CONFIG = {
|
|
13
|
-
ai: {
|
|
14
|
-
primary: "gemini",
|
|
15
|
-
fallback: "claude",
|
|
16
|
-
timeout: 30
|
|
17
|
-
},
|
|
18
|
-
safety: {
|
|
19
|
-
maxFileSize: "10MB",
|
|
20
|
-
blockedPatterns: [
|
|
21
|
-
"*.env",
|
|
22
|
-
".env.*",
|
|
23
|
-
"*.pem",
|
|
24
|
-
"*.key",
|
|
25
|
-
"credentials*",
|
|
26
|
-
"*.sqlite",
|
|
27
|
-
"*.sqlite3"
|
|
28
|
-
],
|
|
29
|
-
warnPatterns: [
|
|
30
|
-
"*.log",
|
|
31
|
-
"*.csv",
|
|
32
|
-
"package-lock.json",
|
|
33
|
-
"yarn.lock",
|
|
34
|
-
"pnpm-lock.yaml"
|
|
35
|
-
]
|
|
36
|
-
},
|
|
37
|
-
commit: {
|
|
38
|
-
style: "conventional",
|
|
39
|
-
language: "ko",
|
|
40
|
-
maxDiffSize: 1e4
|
|
41
|
-
},
|
|
42
|
-
grouping: {
|
|
43
|
-
strategy: "smart"
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
async function loadConfig(cliOptions = {}) {
|
|
47
|
-
const explorer = cosmiconfig("smart-commit", {
|
|
48
|
-
searchPlaces: [
|
|
49
|
-
".smart-commitrc",
|
|
50
|
-
".smart-commitrc.yaml",
|
|
51
|
-
".smart-commitrc.yml",
|
|
52
|
-
".smart-commitrc.json",
|
|
53
|
-
"smart-commit.config.js",
|
|
54
|
-
"package.json"
|
|
55
|
-
]
|
|
56
|
-
});
|
|
57
|
-
const result = await explorer.search();
|
|
58
|
-
const fileConfig = result?.config ?? {};
|
|
59
|
-
const config = deepMerge(
|
|
60
|
-
DEFAULT_CONFIG,
|
|
61
|
-
fileConfig
|
|
62
|
-
);
|
|
63
|
-
if (cliOptions.ai && typeof cliOptions.ai === "string") {
|
|
64
|
-
config.ai.primary = cliOptions.ai;
|
|
65
|
-
}
|
|
66
|
-
if (cliOptions.group && typeof cliOptions.group === "string") {
|
|
67
|
-
config.grouping.strategy = cliOptions.group;
|
|
68
|
-
}
|
|
69
|
-
return config;
|
|
70
|
-
}
|
|
71
|
-
function deepMerge(target, source) {
|
|
72
|
-
const result = { ...target };
|
|
73
|
-
for (const key of Object.keys(source)) {
|
|
74
|
-
if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key]) && target[key] && typeof target[key] === "object" && !Array.isArray(target[key])) {
|
|
75
|
-
result[key] = deepMerge(
|
|
76
|
-
target[key],
|
|
77
|
-
source[key]
|
|
78
|
-
);
|
|
79
|
-
} else {
|
|
80
|
-
result[key] = source[key];
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return result;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// src/scanner.ts
|
|
87
|
-
import { simpleGit } from "simple-git";
|
|
88
|
-
import { readdir, stat, access } from "fs/promises";
|
|
89
|
-
import { join } from "path";
|
|
90
|
-
async function scanRepositories(baseDir, ui, logger) {
|
|
91
|
-
const gitDirs = await findGitDirs(baseDir);
|
|
92
|
-
const repos = [];
|
|
93
|
-
ui.showProgress("Scanning repositories...", 0, gitDirs.length);
|
|
94
|
-
for (let i = 0; i < gitDirs.length; i++) {
|
|
95
|
-
const dir = gitDirs[i];
|
|
96
|
-
ui.showProgress(`Scanning: ${dir}`, i + 1, gitDirs.length);
|
|
97
|
-
try {
|
|
98
|
-
const repo = await inspectRepo(dir, logger);
|
|
99
|
-
repos.push(repo);
|
|
100
|
-
} catch (err) {
|
|
101
|
-
logger.warn({ dir, err }, "Failed to inspect repository");
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return repos;
|
|
105
|
-
}
|
|
106
|
-
async function findGitDirs(baseDir) {
|
|
107
|
-
const dirs = [];
|
|
108
|
-
const entries = await readdir(baseDir, { withFileTypes: true });
|
|
109
|
-
for (const entry of entries) {
|
|
110
|
-
if (!entry.isDirectory()) continue;
|
|
111
|
-
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
112
|
-
const fullPath = join(baseDir, entry.name);
|
|
113
|
-
const gitPath = join(fullPath, ".git");
|
|
114
|
-
try {
|
|
115
|
-
await access(gitPath);
|
|
116
|
-
dirs.push(fullPath);
|
|
117
|
-
} catch {
|
|
118
|
-
const subDirs = await findGitDirs(fullPath);
|
|
119
|
-
dirs.push(...subDirs);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
try {
|
|
123
|
-
const selfGit = join(baseDir, ".git");
|
|
124
|
-
await access(selfGit);
|
|
125
|
-
if (!dirs.some((d) => d === baseDir)) {
|
|
126
|
-
dirs.unshift(baseDir);
|
|
127
|
-
}
|
|
128
|
-
} catch {
|
|
129
|
-
}
|
|
130
|
-
return dirs;
|
|
131
|
-
}
|
|
132
|
-
async function inspectRepo(dir, logger) {
|
|
133
|
-
const git = simpleGit(dir);
|
|
134
|
-
const gitStatus = await detectGitStatus(dir, git);
|
|
135
|
-
if (gitStatus === "locked") {
|
|
136
|
-
logger.warn({ dir }, "Git index locked \u2014 skipping");
|
|
137
|
-
return { path: dir, branch: "", status: "locked", files: [], unpushedCommits: 0 };
|
|
138
|
-
}
|
|
139
|
-
if (gitStatus === "detached") {
|
|
140
|
-
logger.warn({ dir }, "Detached HEAD \u2014 skipping");
|
|
141
|
-
return { path: dir, branch: "HEAD (detached)", status: "detached", files: [], unpushedCommits: 0 };
|
|
142
|
-
}
|
|
143
|
-
if (gitStatus === "rebasing") {
|
|
144
|
-
logger.warn({ dir }, "Rebase in progress \u2014 skipping");
|
|
145
|
-
return { path: dir, branch: "", status: "rebasing", files: [], unpushedCommits: 0 };
|
|
146
|
-
}
|
|
147
|
-
const statusResult = await git.status();
|
|
148
|
-
const branch = statusResult.current ?? "unknown";
|
|
149
|
-
const files = [];
|
|
150
|
-
for (const f of statusResult.files) {
|
|
151
|
-
const filePath = join(dir, f.path);
|
|
152
|
-
let size = 0;
|
|
153
|
-
try {
|
|
154
|
-
const s = await stat(filePath);
|
|
155
|
-
size = s.size;
|
|
156
|
-
} catch {
|
|
157
|
-
}
|
|
158
|
-
files.push({
|
|
159
|
-
path: f.path,
|
|
160
|
-
status: mapGitStatus(f.working_dir, f.index),
|
|
161
|
-
size,
|
|
162
|
-
isBinary: false
|
|
163
|
-
// will be checked by classifier
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
let unpushedCommits = 0;
|
|
167
|
-
try {
|
|
168
|
-
const log = await git.log(["@{u}..HEAD"]);
|
|
169
|
-
unpushedCommits = log.total;
|
|
170
|
-
} catch {
|
|
171
|
-
}
|
|
172
|
-
const repoStatus = gitStatus === "merging" ? "merging" : files.length > 0 ? "dirty" : "clean";
|
|
173
|
-
return { path: dir, branch, status: repoStatus, files, unpushedCommits };
|
|
174
|
-
}
|
|
175
|
-
async function detectGitStatus(dir, git) {
|
|
176
|
-
try {
|
|
177
|
-
await access(join(dir, ".git", "index.lock"));
|
|
178
|
-
return "locked";
|
|
179
|
-
} catch {
|
|
180
|
-
}
|
|
181
|
-
try {
|
|
182
|
-
await access(join(dir, ".git", "rebase-merge"));
|
|
183
|
-
return "rebasing";
|
|
184
|
-
} catch {
|
|
185
|
-
}
|
|
186
|
-
try {
|
|
187
|
-
await access(join(dir, ".git", "rebase-apply"));
|
|
188
|
-
return "rebasing";
|
|
189
|
-
} catch {
|
|
190
|
-
}
|
|
191
|
-
try {
|
|
192
|
-
await access(join(dir, ".git", "MERGE_HEAD"));
|
|
193
|
-
return "merging";
|
|
194
|
-
} catch {
|
|
195
|
-
}
|
|
196
|
-
try {
|
|
197
|
-
await git.raw(["symbolic-ref", "HEAD"]);
|
|
198
|
-
} catch {
|
|
199
|
-
return "detached";
|
|
200
|
-
}
|
|
201
|
-
return "clean";
|
|
202
|
-
}
|
|
203
|
-
function mapGitStatus(workingDir, index) {
|
|
204
|
-
if (index === "?" || workingDir === "?") return "untracked";
|
|
205
|
-
if (index === "A" || workingDir === "A") return "added";
|
|
206
|
-
if (index === "D" || workingDir === "D") return "deleted";
|
|
207
|
-
if (index === "R" || workingDir === "R") return "renamed";
|
|
208
|
-
return "modified";
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// src/ai-client.ts
|
|
212
|
-
import { execa } from "execa";
|
|
213
|
-
var CONVENTIONAL_PREFIXES = [
|
|
214
|
-
"feat",
|
|
215
|
-
"fix",
|
|
216
|
-
"refactor",
|
|
217
|
-
"docs",
|
|
218
|
-
"style",
|
|
219
|
-
"test",
|
|
220
|
-
"chore",
|
|
221
|
-
"perf",
|
|
222
|
-
"ci",
|
|
223
|
-
"build",
|
|
224
|
-
"revert"
|
|
225
|
-
];
|
|
226
|
-
var CONVENTIONAL_RE = new RegExp(`^(${CONVENTIONAL_PREFIXES.join("|")})(\\(.+\\))?!?:\\s.+`);
|
|
227
|
-
var OFFLINE_TEMPLATES = CONVENTIONAL_PREFIXES.map((prefix) => `${prefix}: `);
|
|
228
|
-
function getOfflineTemplates() {
|
|
229
|
-
return OFFLINE_TEMPLATES;
|
|
230
|
-
}
|
|
231
|
-
async function isAiAvailable(tool) {
|
|
232
|
-
try {
|
|
233
|
-
const cmd = tool === "gpt" ? "openai" : tool;
|
|
234
|
-
await execa("which", [cmd], { timeout: 3e3 });
|
|
235
|
-
return true;
|
|
236
|
-
} catch {
|
|
237
|
-
return false;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
function createAiClient(config, logger) {
|
|
241
|
-
async function callWithFallback(prompt) {
|
|
242
|
-
let result = await callAi(config.ai.primary, prompt, config.ai.timeout, logger, config);
|
|
243
|
-
if (!result && config.ai.fallback !== config.ai.primary) {
|
|
244
|
-
logger.warn({ fallback: config.ai.fallback }, "Primary AI failed, trying fallback");
|
|
245
|
-
result = await callAi(config.ai.fallback, prompt, config.ai.timeout, logger, config);
|
|
246
|
-
}
|
|
247
|
-
return result;
|
|
248
|
-
}
|
|
249
|
-
return {
|
|
250
|
-
async generateCommitMessage(diff, language) {
|
|
251
|
-
const summarized = await this.summarizeDiff(diff);
|
|
252
|
-
const prompt = buildCommitPrompt(summarized, language, config.commit.style);
|
|
253
|
-
logger.info({ tool: config.ai.primary, diffLength: summarized.length }, "Requesting commit message");
|
|
254
|
-
let result = await callWithFallback(prompt);
|
|
255
|
-
if (result) {
|
|
256
|
-
if (config.commit.style === "conventional" && !validateConventionalCommit(result)) {
|
|
257
|
-
logger.warn({ message: result.split("\n")[0] }, "Invalid conventional commit, retrying");
|
|
258
|
-
const retryPrompt = buildRetryPrompt(result, language);
|
|
259
|
-
const retried = await callWithFallback(retryPrompt);
|
|
260
|
-
if (retried && validateConventionalCommit(retried)) {
|
|
261
|
-
result = retried;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
result = stripCodeBlocks(result);
|
|
265
|
-
logger.info({ messageLength: result.length }, "Commit message generated");
|
|
266
|
-
}
|
|
267
|
-
return result;
|
|
268
|
-
},
|
|
269
|
-
async resolveConflict(localContent, remoteContent) {
|
|
270
|
-
const prompt = buildConflictPrompt(localContent, remoteContent);
|
|
271
|
-
return callWithFallback(prompt);
|
|
272
|
-
},
|
|
273
|
-
async groupFiles(fileList) {
|
|
274
|
-
const { buildGroupingPrompt } = await import("./classifier-TTZQUM7N.js");
|
|
275
|
-
const prompt = buildGroupingPrompt(fileList);
|
|
276
|
-
return callWithFallback(prompt);
|
|
277
|
-
},
|
|
278
|
-
async summarizeDiff(diff) {
|
|
279
|
-
if (diff.length <= config.commit.maxDiffSize) {
|
|
280
|
-
return diff;
|
|
281
|
-
}
|
|
282
|
-
const statSection = extractDiffStat(diff);
|
|
283
|
-
const hunks = extractKeyHunks(diff, config.commit.maxDiffSize - statSection.length - 200);
|
|
284
|
-
const truncated = `${statSection}
|
|
285
|
-
|
|
286
|
-
[\uC8FC\uC694 \uBCC0\uACBD \uB0B4\uC6A9 (\uC804\uCCB4 ${diff.length}\uC790 \uC911 \uD575\uC2EC\uBD80\uB9CC \uCD94\uCD9C)]
|
|
287
|
-
${hunks}`;
|
|
288
|
-
if (truncated.length > config.commit.maxDiffSize * 1.5) {
|
|
289
|
-
logger.info("Diff too large, requesting AI summary");
|
|
290
|
-
const summaryPrompt = buildDiffSummaryPrompt(truncated.slice(0, config.commit.maxDiffSize));
|
|
291
|
-
const summary = await callWithFallback(summaryPrompt);
|
|
292
|
-
return summary ?? truncated.slice(0, config.commit.maxDiffSize);
|
|
293
|
-
}
|
|
294
|
-
return truncated;
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
function validateConventionalCommit(message) {
|
|
299
|
-
const firstLine = message.split("\n")[0].trim();
|
|
300
|
-
return CONVENTIONAL_RE.test(firstLine);
|
|
301
|
-
}
|
|
302
|
-
function stripCodeBlocks(text) {
|
|
303
|
-
return text.replace(/^```[\w]*\n?/gm, "").replace(/^```\s*$/gm, "").trim();
|
|
304
|
-
}
|
|
305
|
-
function extractDiffStat(diff) {
|
|
306
|
-
const lines = diff.split("\n");
|
|
307
|
-
const statLines = [];
|
|
308
|
-
for (const line of lines) {
|
|
309
|
-
if (line.startsWith("diff --git")) {
|
|
310
|
-
statLines.push(line);
|
|
311
|
-
} else if (line.startsWith("--- ") || line.startsWith("+++ ")) {
|
|
312
|
-
statLines.push(line);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return statLines.join("\n");
|
|
316
|
-
}
|
|
317
|
-
function extractKeyHunks(diff, maxLength) {
|
|
318
|
-
const hunks = [];
|
|
319
|
-
let currentHunk = "";
|
|
320
|
-
let totalLength = 0;
|
|
321
|
-
for (const line of diff.split("\n")) {
|
|
322
|
-
if (line.startsWith("@@")) {
|
|
323
|
-
if (currentHunk && totalLength + currentHunk.length <= maxLength) {
|
|
324
|
-
hunks.push(currentHunk);
|
|
325
|
-
totalLength += currentHunk.length;
|
|
326
|
-
}
|
|
327
|
-
currentHunk = line + "\n";
|
|
328
|
-
} else if (line.startsWith("+") || line.startsWith("-")) {
|
|
329
|
-
currentHunk += line + "\n";
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
if (currentHunk && totalLength + currentHunk.length <= maxLength) {
|
|
333
|
-
hunks.push(currentHunk);
|
|
334
|
-
}
|
|
335
|
-
return hunks.join("\n");
|
|
336
|
-
}
|
|
337
|
-
async function callAi(tool, prompt, timeout, logger, config) {
|
|
338
|
-
try {
|
|
339
|
-
const { command, args } = buildAiCommand(tool, prompt, config);
|
|
340
|
-
const { stdout } = await execa(command, args, {
|
|
341
|
-
timeout: timeout * 1e3,
|
|
342
|
-
stdin: "ignore"
|
|
343
|
-
});
|
|
344
|
-
const trimmed = stdout.trim();
|
|
345
|
-
return trimmed || null;
|
|
346
|
-
} catch (err) {
|
|
347
|
-
logger.error({ tool, err }, "AI call failed");
|
|
348
|
-
return null;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
function buildAiCommand(tool, prompt, config) {
|
|
352
|
-
switch (tool) {
|
|
353
|
-
case "gemini":
|
|
354
|
-
return { command: "gemini", args: [prompt] };
|
|
355
|
-
case "claude":
|
|
356
|
-
return { command: "claude", args: ["-p", prompt] };
|
|
357
|
-
case "gpt":
|
|
358
|
-
return { command: "openai", args: ["api", "chat.completions.create", "-m", "gpt-4o", "-g", "user", prompt] };
|
|
359
|
-
case "ollama": {
|
|
360
|
-
const model = config?.ai?.ollama?.model ?? "llama3";
|
|
361
|
-
return { command: "ollama", args: ["run", model, prompt] };
|
|
362
|
-
}
|
|
363
|
-
default:
|
|
364
|
-
return { command: tool, args: [prompt] };
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
function buildCommitPrompt(diff, language, style) {
|
|
368
|
-
const langLabel = language === "ko" ? "\uD55C\uAD6D\uC5B4" : "English";
|
|
369
|
-
const styleGuide = style === "conventional" ? `Conventional Commits \uD615\uC2DD\uC744 \uBC18\uB4DC\uC2DC \uB530\uB974\uC138\uC694.
|
|
370
|
-
\uC811\uB450\uC0AC\uB294 \uB2E4\uC74C \uC911 \uC120\uD0DD: ${CONVENTIONAL_PREFIXES.join(", ")}
|
|
371
|
-
\uD615\uC2DD: <\uC811\uB450\uC0AC>(<\uBC94\uC704>): <\uC124\uBA85> (\uBC94\uC704\uB294 \uC120\uD0DD\uC0AC\uD56D)` : "";
|
|
372
|
-
return `\uC544\uB798\uC758 [Git Diff] \uB0B4\uC6A9\uC744 \uBD84\uC11D\uD558\uC5EC Git Commit Message\uB97C \uC791\uC131\uD574\uC918.
|
|
373
|
-
|
|
374
|
-
[CRITICAL INSTRUCTION]
|
|
375
|
-
**\uACB0\uACFC\uB294 \uBB34\uC870\uAC74 '${langLabel}'\uB85C \uC791\uC131\uB418\uC5B4\uC57C \uD569\uB2C8\uB2E4.**
|
|
376
|
-
${styleGuide}
|
|
377
|
-
|
|
378
|
-
[\uC791\uC131 \uC608\uC2DC]
|
|
379
|
-
feat(auth): \uC0AC\uC6A9\uC790 \uB85C\uADF8\uC778 API \uAD6C\uD604
|
|
380
|
-
|
|
381
|
-
- \uB85C\uADF8\uC778 \uC694\uCCAD \uCC98\uB9AC\uB97C \uC704\uD55C \uCEE8\uD2B8\uB864\uB7EC \uBA54\uC11C\uB4DC \uCD94\uAC00
|
|
382
|
-
- JWT \uD1A0\uD070 \uBC1C\uAE09 \uB85C\uC9C1 \uAD6C\uD604
|
|
383
|
-
|
|
384
|
-
[\uD544\uC218 \uADDC\uCE59]
|
|
385
|
-
1. \uC5B8\uC5B4: **100% ${langLabel}**\uB85C \uC791\uC131\uD560 \uAC83.
|
|
386
|
-
2. \uD615\uC2DD:
|
|
387
|
-
- \uCCAB \uC904: \uBCC0\uACBD \uC0AC\uD56D\uC744 50\uC790 \uC774\uB0B4\uB85C \uC694\uC57D (\uC81C\uBAA9)
|
|
388
|
-
- \uB450 \uBC88\uC9F8 \uC904: \uBE48 \uC904
|
|
389
|
-
- \uC138 \uBC88\uC9F8 \uC904\uBD80\uD130: \uBCC0\uACBD\uB41C \uC0C1\uC138 \uB0B4\uC6A9\uC744 \uBD88\uB9BF \uD3EC\uC778\uD2B8(-)\uB85C \uC815\uB9AC
|
|
390
|
-
3. \uCD9C\uB825: \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\uD560 \uAC83.
|
|
391
|
-
4. \uC81C\uD55C: \uC5B4\uB5A0\uD55C \uB3C4\uAD6C(Functions/Tools)\uB3C4 \uC0AC\uC6A9\uD558\uC9C0 \uB9D0 \uAC83. \uC624\uC9C1 \uD14D\uC2A4\uD2B8\uB9CC \uC0DD\uC131\uD558\uB77C.
|
|
392
|
-
|
|
393
|
-
[Git Diff]
|
|
394
|
-
${diff}`;
|
|
395
|
-
}
|
|
396
|
-
function buildRetryPrompt(invalidMessage, language) {
|
|
397
|
-
const langLabel = language === "ko" ? "\uD55C\uAD6D\uC5B4" : "English";
|
|
398
|
-
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.
|
|
399
|
-
|
|
400
|
-
[\uD604\uC7AC \uBA54\uC2DC\uC9C0]
|
|
401
|
-
${invalidMessage}
|
|
402
|
-
|
|
403
|
-
[\uADDC\uCE59]
|
|
404
|
-
- \uCCAB \uC904\uC740 \uBC18\uB4DC\uC2DC "${CONVENTIONAL_PREFIXES.join("|")}(<\uBC94\uC704>): <\uC124\uBA85>" \uD615\uC2DD\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4.
|
|
405
|
-
- ${langLabel}\uB85C \uC791\uC131\uD558\uC138\uC694.
|
|
406
|
-
- \uC218\uC815\uB41C \uCEE4\uBC0B \uBA54\uC2DC\uC9C0\uB9CC \uCD9C\uB825\uD558\uC138\uC694.`;
|
|
407
|
-
}
|
|
408
|
-
function buildConflictPrompt(localContent, remoteContent) {
|
|
409
|
-
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.
|
|
410
|
-
\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.
|
|
411
|
-
|
|
412
|
-
[\uD544\uC218 \uADDC\uCE59]
|
|
413
|
-
1. \uB450 \uBC84\uC804\uC758 \uBCC0\uACBD \uC0AC\uD56D\uC744 \uBAA8\uB450 \uD3EC\uD568\uD558\uC5EC \uBCD1\uD569\uD560 \uAC83
|
|
414
|
-
2. \uCDA9\uB3CC \uB9C8\uCEE4(<<<<<<, ======, >>>>>>)\uB294 \uC808\uB300 \uD3EC\uD568\uD558\uC9C0 \uB9D0 \uAC83
|
|
415
|
-
3. \uCF54\uB4DC\uC758 \uB17C\uB9AC\uC801 \uC77C\uAD00\uC131\uC744 \uC720\uC9C0\uD560 \uAC83
|
|
416
|
-
4. \uCD9C\uB825\uC740 **\uC624\uC9C1 \uBCD1\uD569\uB41C \uD30C\uC77C \uB0B4\uC6A9\uB9CC** \uCD9C\uB825\uD560 \uAC83
|
|
417
|
-
|
|
418
|
-
[\uB85C\uCEEC \uBC84\uC804]
|
|
419
|
-
${localContent}
|
|
420
|
-
|
|
421
|
-
[\uC6D0\uACA9 \uBC84\uC804]
|
|
422
|
-
${remoteContent}`;
|
|
423
|
-
}
|
|
424
|
-
function buildDiffSummaryPrompt(diff) {
|
|
425
|
-
return `\uC544\uB798 Git Diff\uAC00 \uB108\uBB34 \uD07D\uB2C8\uB2E4. \uD575\uC2EC \uBCC0\uACBD \uC0AC\uD56D\uB9CC \uC694\uC57D\uD574\uC8FC\uC138\uC694.
|
|
426
|
-
|
|
427
|
-
[\uADDC\uCE59]
|
|
428
|
-
1. \uC5B4\uB5A4 \uD30C\uC77C\uC5D0\uC11C \uBB34\uC5C7\uC774 \uBCC0\uACBD\uB418\uC5C8\uB294\uC9C0 \uC694\uC57D
|
|
429
|
-
2. \uCD94\uAC00/\uC218\uC815/\uC0AD\uC81C\uB41C \uC8FC\uC694 \uD568\uC218/\uD074\uB798\uC2A4/\uBCC0\uC218 \uB098\uC5F4
|
|
430
|
-
3. diff \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825 (+ / - \uC811\uB450\uC0AC \uC0AC\uC6A9)
|
|
431
|
-
4. 500\uC790 \uC774\uB0B4\uB85C \uC694\uC57D
|
|
432
|
-
|
|
433
|
-
[Diff]
|
|
434
|
-
${diff}`;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
18
|
// src/committer.ts
|
|
438
|
-
import { simpleGit
|
|
19
|
+
import { simpleGit } from "simple-git";
|
|
439
20
|
async function commitAndPush(repo, files, message, action, ui, logger) {
|
|
440
|
-
const git =
|
|
21
|
+
const git = simpleGit(repo.path);
|
|
441
22
|
if (action === "cancel") {
|
|
442
23
|
ui.showMessage(`${repo.path}: \uAC74\uB108\uB701\uB2C8\uB2E4.`, "info");
|
|
443
24
|
return;
|
|
@@ -591,12 +172,14 @@ function createUI() {
|
|
|
591
172
|
const items = [
|
|
592
173
|
"Push (\uD478\uC2DC \uC2E4\uD589)",
|
|
593
174
|
"Skip (\uB85C\uCEEC \uCEE4\uBC0B \uC720\uC9C0)",
|
|
594
|
-
"Cancel (\uCEE4\uBC0B \uCDE8\uC18C)"
|
|
175
|
+
"Cancel (\uCEE4\uBC0B \uCDE8\uC18C)",
|
|
176
|
+
"Skip repo (\uC774 \uC800\uC7A5\uC18C \uAC74\uB108\uB6F0\uAE30)",
|
|
177
|
+
"Exit (\uC885\uB8CC)"
|
|
595
178
|
];
|
|
596
179
|
term(" \u25B6 Select action:\n");
|
|
597
180
|
const response = await term.singleColumnMenu(items).promise;
|
|
598
181
|
term("\n");
|
|
599
|
-
const map = ["push", "skip", "cancel"];
|
|
182
|
+
const map = ["push", "skip", "cancel", "skip-repo", "exit"];
|
|
600
183
|
return map[response.selectedIndex] ?? "skip";
|
|
601
184
|
},
|
|
602
185
|
async promptOfflineTemplate(templates) {
|
|
@@ -669,26 +252,6 @@ function padEnd(str, len) {
|
|
|
669
252
|
return str + " ".repeat(diff);
|
|
670
253
|
}
|
|
671
254
|
|
|
672
|
-
// src/logger.ts
|
|
673
|
-
import pino from "pino";
|
|
674
|
-
import { join as join2 } from "path";
|
|
675
|
-
import { mkdirSync } from "fs";
|
|
676
|
-
import { homedir } from "os";
|
|
677
|
-
function createLogger() {
|
|
678
|
-
const logDir = join2(homedir(), ".smart-commit", "logs");
|
|
679
|
-
try {
|
|
680
|
-
mkdirSync(logDir, { recursive: true });
|
|
681
|
-
} catch {
|
|
682
|
-
return pino({ level: "info" });
|
|
683
|
-
}
|
|
684
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
685
|
-
const logFile = join2(logDir, `${today}.log`);
|
|
686
|
-
return pino(
|
|
687
|
-
{ level: "info" },
|
|
688
|
-
pino.destination({ dest: logFile, append: true, sync: false })
|
|
689
|
-
);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
255
|
// src/index.ts
|
|
693
256
|
var program = new Command();
|
|
694
257
|
program.name("smart-commit").description("AI-powered intelligent Git auto-commit & push CLI tool").version("0.1.0").option("-d, --dry-run", "Preview without committing or pushing").option("-g, --group <strategy>", "Grouping strategy: smart | single | manual").option("-a, --ai <tool>", "AI tool: gemini | claude | gpt | ollama").option("--no-interactive", "Headless mode (no prompts)").option("--offline", "Offline mode (use templates instead of AI)").action(async (options) => {
|
|
@@ -731,7 +294,7 @@ program.name("smart-commit").description("AI-powered intelligent Git auto-commit
|
|
|
731
294
|
}
|
|
732
295
|
continue;
|
|
733
296
|
}
|
|
734
|
-
const safety = classifyFiles(repo.files, config);
|
|
297
|
+
const safety = await classifyFiles(repo.files, config);
|
|
735
298
|
if (safety.blocked.length > 0) {
|
|
736
299
|
ui.showBlocked(repo, safety.blocked);
|
|
737
300
|
}
|
|
@@ -787,6 +350,15 @@ program.name("smart-commit").description("AI-powered intelligent Git auto-commit
|
|
|
787
350
|
continue;
|
|
788
351
|
}
|
|
789
352
|
const action = isHeadless ? "push" : await ui.promptAction();
|
|
353
|
+
if (action === "exit") {
|
|
354
|
+
ui.showMessage("\uC885\uB8CC\uD569\uB2C8\uB2E4.", "info");
|
|
355
|
+
ui.cleanup();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (action === "skip-repo") {
|
|
359
|
+
ui.showMessage(`${repo.path}: \uC800\uC7A5\uC18C \uAC74\uB108\uB6F0\uAE30`, "info");
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
790
362
|
await commitAndPush(repo, group.files, commitMsg, action, ui, logger);
|
|
791
363
|
}
|
|
792
364
|
}
|
|
@@ -815,8 +387,8 @@ program.command("hook").description("Install or uninstall Git hooks").option("--
|
|
|
815
387
|
ui.cleanup();
|
|
816
388
|
});
|
|
817
389
|
async function getDiff(repo, filePaths) {
|
|
818
|
-
const { simpleGit:
|
|
819
|
-
const git =
|
|
390
|
+
const { simpleGit: simpleGit2 } = await import("simple-git");
|
|
391
|
+
const git = simpleGit2(repo.path);
|
|
820
392
|
await git.add(filePaths);
|
|
821
393
|
const diff = await git.diff(["--cached", "--", ...filePaths]);
|
|
822
394
|
return diff;
|