pmatrix-smart-commit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/chunk-ZS27WQDW.js +162 -0
- package/dist/chunk-ZS27WQDW.js.map +1 -0
- package/dist/classifier-TTZQUM7N.js +14 -0
- package/dist/classifier-TTZQUM7N.js.map +1 -0
- package/dist/index.js +815 -0
- package/dist/index.js.map +1 -0
- package/dist/install-XOLZLFAI.js +84 -0
- package/dist/install-XOLZLFAI.js.map +1 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
classifyFiles,
|
|
4
|
+
groupFiles
|
|
5
|
+
} from "./chunk-ZS27WQDW.js";
|
|
6
|
+
|
|
7
|
+
// src/index.ts
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
|
|
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
|
+
// src/committer.ts
|
|
438
|
+
import { simpleGit as simpleGit2 } from "simple-git";
|
|
439
|
+
async function commitAndPush(repo, files, message, action, ui, logger) {
|
|
440
|
+
const git = simpleGit2(repo.path);
|
|
441
|
+
if (action === "cancel") {
|
|
442
|
+
ui.showMessage(`${repo.path}: \uAC74\uB108\uB701\uB2C8\uB2E4.`, "info");
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (action === "edit") {
|
|
446
|
+
ui.showMessage("\uBA54\uC2DC\uC9C0 \uD3B8\uC9D1\uC740 Phase 2\uC5D0\uC11C \uC9C0\uC6D0\uB429\uB2C8\uB2E4.", "info");
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const filePaths = files.map((f) => f.path);
|
|
450
|
+
await git.add(filePaths);
|
|
451
|
+
logger.info({ repo: repo.path, files: filePaths }, "Files staged");
|
|
452
|
+
try {
|
|
453
|
+
await git.commit(message);
|
|
454
|
+
ui.showMessage(`${repo.path}: \uCEE4\uBC0B \uC644\uB8CC`, "success");
|
|
455
|
+
logger.info({ repo: repo.path, message }, "Committed");
|
|
456
|
+
} catch (err) {
|
|
457
|
+
logger.error({ repo: repo.path, err }, "Commit failed");
|
|
458
|
+
ui.showMessage(`${repo.path}: \uCEE4\uBC0B \uC2E4\uD328 \u2014 ${err}`, "error");
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (action === "skip") {
|
|
462
|
+
ui.showMessage(`${repo.path}: \uB85C\uCEEC \uCEE4\uBC0B \uC720\uC9C0, \uD478\uC2DC \uAC74\uB108\uB700`, "info");
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (action === "push") {
|
|
466
|
+
await pushWithRetry(repo, git, ui, logger);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
async function pushWithRetry(repo, git, ui, logger) {
|
|
470
|
+
ui.showMessage(`${repo.path}: \uD478\uC2DC \uC911...`, "info");
|
|
471
|
+
try {
|
|
472
|
+
await git.push();
|
|
473
|
+
ui.showMessage(`${repo.path}: \uD478\uC2DC \uC131\uACF5!`, "success");
|
|
474
|
+
logger.info({ repo: repo.path }, "Pushed");
|
|
475
|
+
} catch {
|
|
476
|
+
ui.showMessage(`${repo.path}: \uD478\uC2DC \uC2E4\uD328, pull \uD6C4 \uC7AC\uC2DC\uB3C4...`, "warn");
|
|
477
|
+
logger.warn({ repo: repo.path }, "Push failed, attempting pull");
|
|
478
|
+
try {
|
|
479
|
+
await git.pull();
|
|
480
|
+
await git.push();
|
|
481
|
+
ui.showMessage(`${repo.path}: pull \uD6C4 \uD478\uC2DC \uC131\uACF5!`, "success");
|
|
482
|
+
logger.info({ repo: repo.path }, "Push succeeded after pull");
|
|
483
|
+
} catch (pullErr) {
|
|
484
|
+
ui.showMessage(`${repo.path}: pull/push \uC2E4\uD328 \u2014 \uC218\uB3D9 \uD655\uC778 \uD544\uC694`, "error");
|
|
485
|
+
logger.error({ repo: repo.path, err: pullErr }, "Pull+push failed");
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/ui.ts
|
|
491
|
+
import termkit from "terminal-kit";
|
|
492
|
+
var term = termkit.terminal;
|
|
493
|
+
function createUI() {
|
|
494
|
+
let progressBar = null;
|
|
495
|
+
return {
|
|
496
|
+
showHeader(config) {
|
|
497
|
+
term.clear();
|
|
498
|
+
term.bold.cyan("\n Smart Commit v0.1.0\n");
|
|
499
|
+
term.gray(` AI: ${config.ai.primary} (fallback: ${config.ai.fallback})
|
|
500
|
+
`);
|
|
501
|
+
term.gray(` Style: ${config.commit.style} | Language: ${config.commit.language}
|
|
502
|
+
`);
|
|
503
|
+
term("\n");
|
|
504
|
+
},
|
|
505
|
+
showProgress(label, current, total) {
|
|
506
|
+
if (!progressBar) {
|
|
507
|
+
term(" ");
|
|
508
|
+
progressBar = term.progressBar({
|
|
509
|
+
width: 50,
|
|
510
|
+
title: label,
|
|
511
|
+
percent: true
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
progressBar.update({ progress: current / total, title: label });
|
|
515
|
+
if (current >= total) {
|
|
516
|
+
term("\n");
|
|
517
|
+
progressBar = null;
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
showRepoTable(repos) {
|
|
521
|
+
term("\n");
|
|
522
|
+
const tableData = [
|
|
523
|
+
[" #", "Repository", "Branch", "Changes", "Status"]
|
|
524
|
+
];
|
|
525
|
+
repos.forEach((repo, i) => {
|
|
526
|
+
const shortPath = repo.path.split("/").slice(-2).join("/");
|
|
527
|
+
const changes = repo.files.length > 0 ? `${repo.files.length} files` : repo.unpushedCommits > 0 ? `${repo.unpushedCommits} unpushed` : "-";
|
|
528
|
+
const status = statusIcon(repo.status);
|
|
529
|
+
tableData.push([` ${i + 1}`, shortPath, repo.branch, changes, status]);
|
|
530
|
+
});
|
|
531
|
+
term.table(tableData, {
|
|
532
|
+
hasBorder: false,
|
|
533
|
+
contentHasMarkup: true,
|
|
534
|
+
width: 80,
|
|
535
|
+
fit: true
|
|
536
|
+
});
|
|
537
|
+
term("\n");
|
|
538
|
+
},
|
|
539
|
+
showBlocked(repo, files) {
|
|
540
|
+
const shortPath = repo.path.split("/").slice(-1)[0];
|
|
541
|
+
term.red(` \u2716 ${shortPath}: \uCC28\uB2E8\uB41C \uD30C\uC77C (\uCEE4\uBC0B \uC81C\uC678)
|
|
542
|
+
`);
|
|
543
|
+
for (const f of files) {
|
|
544
|
+
term.red(` - ${f.path}
|
|
545
|
+
`);
|
|
546
|
+
}
|
|
547
|
+
term("\n");
|
|
548
|
+
},
|
|
549
|
+
async confirmWarned(repo, files) {
|
|
550
|
+
const shortPath = repo.path.split("/").slice(-1)[0];
|
|
551
|
+
term.yellow(` \u26A0 ${shortPath}: \uC8FC\uC758 \uD544\uC694\uD55C \uD30C\uC77C
|
|
552
|
+
`);
|
|
553
|
+
for (const f of files) {
|
|
554
|
+
term.yellow(` - ${f.path}
|
|
555
|
+
`);
|
|
556
|
+
}
|
|
557
|
+
term("\n \uD3EC\uD568\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? ");
|
|
558
|
+
const result = await term.yesOrNo({ yes: ["y", "ENTER"], no: ["n"] }).promise;
|
|
559
|
+
term("\n");
|
|
560
|
+
return result ?? false;
|
|
561
|
+
},
|
|
562
|
+
showCommitPreview(repo, message, files) {
|
|
563
|
+
const shortPath = repo.path.split("/").slice(-2).join("/");
|
|
564
|
+
term.bold(`
|
|
565
|
+
\u{1F4C2} ${shortPath}
|
|
566
|
+
`);
|
|
567
|
+
term(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\uFFFD\uFFFD\uFFFD\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
568
|
+
term.green(` ${message.split("\n")[0]}
|
|
569
|
+
`);
|
|
570
|
+
const body = message.split("\n").slice(1).join("\n").trim();
|
|
571
|
+
if (body) {
|
|
572
|
+
term.gray(` ${body.replace(/\n/g, "\n ")}
|
|
573
|
+
`);
|
|
574
|
+
}
|
|
575
|
+
term(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
576
|
+
term.gray(` Files (${files.length}):
|
|
577
|
+
`);
|
|
578
|
+
for (const f of files.slice(0, 10)) {
|
|
579
|
+
const icon = f.status === "added" ? "A" : f.status === "deleted" ? "D" : "M";
|
|
580
|
+
term.gray(` ${icon} ${f.path}
|
|
581
|
+
`);
|
|
582
|
+
}
|
|
583
|
+
if (files.length > 10) {
|
|
584
|
+
term.gray(` ... and ${files.length - 10} more
|
|
585
|
+
`);
|
|
586
|
+
}
|
|
587
|
+
term("\n");
|
|
588
|
+
},
|
|
589
|
+
async promptAction() {
|
|
590
|
+
const items = [
|
|
591
|
+
"Push (\uD478\uC2DC \uC2E4\uD589)",
|
|
592
|
+
"Skip (\uB85C\uCEEC \uCEE4\uBC0B \uC720\uC9C0)",
|
|
593
|
+
"Cancel (\uCEE4\uBC0B \uCDE8\uC18C)"
|
|
594
|
+
];
|
|
595
|
+
term(" \u25B6 Select action:\n");
|
|
596
|
+
const response = await term.singleColumnMenu(items).promise;
|
|
597
|
+
term("\n");
|
|
598
|
+
const map = ["push", "skip", "cancel"];
|
|
599
|
+
return map[response.selectedIndex] ?? "skip";
|
|
600
|
+
},
|
|
601
|
+
async promptOfflineTemplate(templates) {
|
|
602
|
+
term.yellow(" \u26A0 AI \uC0AC\uC6A9 \uBD88\uAC00 \u2014 \uC624\uD504\uB77C\uC778 \uD15C\uD50C\uB9BF\uC744 \uC120\uD0DD\uD558\uC138\uC694:\n");
|
|
603
|
+
const response = await term.singleColumnMenu(templates).promise;
|
|
604
|
+
term("\n");
|
|
605
|
+
const selected = templates[response.selectedIndex];
|
|
606
|
+
term(" \uCEE4\uBC0B \uBA54\uC2DC\uC9C0\uB97C \uC785\uB825\uD558\uC138\uC694 (\uC811\uB450\uC0AC \uD3EC\uD568): ");
|
|
607
|
+
const input = await term.inputField({ default: selected }).promise;
|
|
608
|
+
term("\n");
|
|
609
|
+
return input ?? selected;
|
|
610
|
+
},
|
|
611
|
+
async promptInput(label) {
|
|
612
|
+
term(` ${label}: `);
|
|
613
|
+
const input = await term.inputField().promise;
|
|
614
|
+
term("\n");
|
|
615
|
+
return input ?? "";
|
|
616
|
+
},
|
|
617
|
+
showMessage(msg, level) {
|
|
618
|
+
const icon = { info: "\u2139", success: "\u2705", warn: "\u26A0\uFE0F", error: "\u2716" };
|
|
619
|
+
const text = ` ${icon[level]} ${msg}
|
|
620
|
+
`;
|
|
621
|
+
switch (level) {
|
|
622
|
+
case "info":
|
|
623
|
+
term.cyan(text);
|
|
624
|
+
break;
|
|
625
|
+
case "success":
|
|
626
|
+
term.green(text);
|
|
627
|
+
break;
|
|
628
|
+
case "warn":
|
|
629
|
+
term.yellow(text);
|
|
630
|
+
break;
|
|
631
|
+
case "error":
|
|
632
|
+
term.red(text);
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
showComplete() {
|
|
637
|
+
term("\n");
|
|
638
|
+
term.bold.green(" \u{1F389} \uBAA8\uB4E0 \uC800\uC7A5\uC18C \uC791\uC5C5 \uC644\uB8CC!\n\n");
|
|
639
|
+
},
|
|
640
|
+
cleanup() {
|
|
641
|
+
term.processExit(0);
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
function statusIcon(status) {
|
|
646
|
+
switch (status) {
|
|
647
|
+
case "dirty":
|
|
648
|
+
return "\u{1F4DD} \uBCC0\uACBD\uB428";
|
|
649
|
+
case "clean":
|
|
650
|
+
return "\u2705 Clean";
|
|
651
|
+
case "detached":
|
|
652
|
+
return "\u26A0\uFE0F Detached";
|
|
653
|
+
case "rebasing":
|
|
654
|
+
return "\u{1F504} Rebasing";
|
|
655
|
+
case "merging":
|
|
656
|
+
return "\u{1F500} Merging";
|
|
657
|
+
case "locked":
|
|
658
|
+
return "\u{1F512} Locked";
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/logger.ts
|
|
663
|
+
import pino from "pino";
|
|
664
|
+
import { join as join2 } from "path";
|
|
665
|
+
import { mkdirSync } from "fs";
|
|
666
|
+
import { homedir } from "os";
|
|
667
|
+
function createLogger() {
|
|
668
|
+
const logDir = join2(homedir(), ".smart-commit", "logs");
|
|
669
|
+
try {
|
|
670
|
+
mkdirSync(logDir, { recursive: true });
|
|
671
|
+
} catch {
|
|
672
|
+
return pino({ level: "info" });
|
|
673
|
+
}
|
|
674
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
675
|
+
const logFile = join2(logDir, `${today}.log`);
|
|
676
|
+
return pino(
|
|
677
|
+
{ level: "info" },
|
|
678
|
+
pino.destination({ dest: logFile, append: true, sync: false })
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/index.ts
|
|
683
|
+
var program = new Command();
|
|
684
|
+
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) => {
|
|
685
|
+
const config = await loadConfig(options);
|
|
686
|
+
const logger = createLogger();
|
|
687
|
+
const ui = createUI();
|
|
688
|
+
const ai = createAiClient(config, logger);
|
|
689
|
+
const isHeadless = options.interactive === false;
|
|
690
|
+
logger.info({ options }, "smart-commit started");
|
|
691
|
+
ui.showHeader(config);
|
|
692
|
+
let offlineMode = options.offline ?? false;
|
|
693
|
+
if (!offlineMode) {
|
|
694
|
+
const primaryAvail = await isAiAvailable(config.ai.primary);
|
|
695
|
+
const fallbackAvail = await isAiAvailable(config.ai.fallback);
|
|
696
|
+
if (!primaryAvail && !fallbackAvail) {
|
|
697
|
+
ui.showMessage("AI \uB3C4\uAD6C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC624\uD504\uB77C\uC778 \uBAA8\uB4DC\uB85C \uC804\uD658\uD569\uB2C8\uB2E4.", "warn");
|
|
698
|
+
offlineMode = true;
|
|
699
|
+
logger.warn("No AI tools available, switching to offline mode");
|
|
700
|
+
} else if (!primaryAvail) {
|
|
701
|
+
ui.showMessage(`${config.ai.primary}\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. ${config.ai.fallback}\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.`, "warn");
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
const repos = await scanRepositories(process.cwd(), ui, logger);
|
|
705
|
+
if (repos.length === 0) {
|
|
706
|
+
ui.showMessage("\uBCC0\uACBD \uC0AC\uD56D\uC774 \uC788\uB294 \uC800\uC7A5\uC18C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", "info");
|
|
707
|
+
ui.cleanup();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
ui.showRepoTable(repos);
|
|
711
|
+
for (const repo of repos) {
|
|
712
|
+
if (repo.status !== "dirty") {
|
|
713
|
+
if (repo.status === "clean" && repo.unpushedCommits > 0) {
|
|
714
|
+
ui.showMessage(`${repo.path}: \uD478\uC2DC\uB418\uC9C0 \uC54A\uC740 \uCEE4\uBC0B ${repo.unpushedCommits}\uAC1C`, "info");
|
|
715
|
+
if (!isHeadless) {
|
|
716
|
+
const action = await ui.promptAction();
|
|
717
|
+
if (action === "push") {
|
|
718
|
+
await commitAndPush(repo, [], "", "push", ui, logger);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const safety = classifyFiles(repo.files, config);
|
|
725
|
+
if (safety.blocked.length > 0) {
|
|
726
|
+
ui.showBlocked(repo, safety.blocked);
|
|
727
|
+
}
|
|
728
|
+
if (safety.warned.length > 0) {
|
|
729
|
+
if (isHeadless) {
|
|
730
|
+
ui.showMessage(`${repo.path}: \uACBD\uACE0 \uD30C\uC77C ${safety.warned.length}\uAC1C \u2014 headless \uBAA8\uB4DC\uC5D0\uC11C \uC81C\uC678`, "warn");
|
|
731
|
+
} else {
|
|
732
|
+
const proceed = await ui.confirmWarned(repo, safety.warned);
|
|
733
|
+
if (proceed) {
|
|
734
|
+
safety.safe.push(...safety.warned);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (safety.safe.length === 0) {
|
|
739
|
+
ui.showMessage(`${repo.path}: \uCEE4\uBC0B\uD560 \uC548\uC804\uD55C \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.`, "warn");
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const groups = await groupFiles(
|
|
743
|
+
safety.safe,
|
|
744
|
+
offlineMode ? "single" : config.grouping.strategy,
|
|
745
|
+
!offlineMode && config.grouping.strategy === "smart" ? (fileList) => ai.groupFiles(fileList) : null,
|
|
746
|
+
logger
|
|
747
|
+
);
|
|
748
|
+
for (const group of groups) {
|
|
749
|
+
let commitMsg = null;
|
|
750
|
+
if (offlineMode) {
|
|
751
|
+
if (isHeadless) {
|
|
752
|
+
commitMsg = `chore: auto-commit ${group.files.length} files`;
|
|
753
|
+
} else {
|
|
754
|
+
commitMsg = await ui.promptOfflineTemplate(getOfflineTemplates());
|
|
755
|
+
}
|
|
756
|
+
} else {
|
|
757
|
+
const diff = await getDiff(repo, group.files.map((f) => f.path));
|
|
758
|
+
const summarizedDiff = await ai.summarizeDiff(diff);
|
|
759
|
+
commitMsg = await ai.generateCommitMessage(summarizedDiff, config.commit.language);
|
|
760
|
+
if (!commitMsg) {
|
|
761
|
+
ui.showMessage(`${repo.path} [${group.label}]: AI \uBA54\uC2DC\uC9C0 \uC0DD\uC131 \uC2E4\uD328`, "warn");
|
|
762
|
+
if (!isHeadless) {
|
|
763
|
+
ui.showMessage("\uC624\uD504\uB77C\uC778 \uD15C\uD50C\uB9BF\uC73C\uB85C \uC804\uD658\uD569\uB2C8\uB2E4.", "info");
|
|
764
|
+
commitMsg = await ui.promptOfflineTemplate(getOfflineTemplates());
|
|
765
|
+
} else {
|
|
766
|
+
commitMsg = `chore: auto-commit ${group.files.length} files`;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (!commitMsg) continue;
|
|
771
|
+
ui.showCommitPreview(repo, commitMsg, group.files);
|
|
772
|
+
if (group.reason) {
|
|
773
|
+
ui.showMessage(` \uADF8\uB8F9\uD551 \uC774\uC720: ${group.reason}`, "info");
|
|
774
|
+
}
|
|
775
|
+
if (options.dryRun) {
|
|
776
|
+
ui.showMessage("(dry-run) \uC2E4\uC81C \uCEE4\uBC0B/\uD478\uC2DC\uB97C \uC218\uD589\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.", "info");
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
const action = isHeadless ? "push" : await ui.promptAction();
|
|
780
|
+
await commitAndPush(repo, group.files, commitMsg, action, ui, logger);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
ui.showComplete();
|
|
784
|
+
ui.cleanup();
|
|
785
|
+
});
|
|
786
|
+
program.command("hook").description("Install or uninstall Git hooks").option("--uninstall", "Remove smart-commit hooks").action(async (options) => {
|
|
787
|
+
const { installHooks, uninstallHooks } = await import("./install-XOLZLFAI.js");
|
|
788
|
+
const ui = createUI();
|
|
789
|
+
if (options.uninstall) {
|
|
790
|
+
const removed = await uninstallHooks(process.cwd());
|
|
791
|
+
if (removed.length > 0) {
|
|
792
|
+
ui.showMessage(`\uD6C5 \uC81C\uAC70 \uC644\uB8CC: ${removed.join(", ")}`, "success");
|
|
793
|
+
} else {
|
|
794
|
+
ui.showMessage("\uC81C\uAC70\uD560 smart-commit \uD6C5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.", "info");
|
|
795
|
+
}
|
|
796
|
+
} else {
|
|
797
|
+
const { installed, skipped } = await installHooks(process.cwd());
|
|
798
|
+
if (installed.length > 0) {
|
|
799
|
+
ui.showMessage(`\uD6C5 \uC124\uCE58 \uC644\uB8CC: ${installed.join(", ")}`, "success");
|
|
800
|
+
}
|
|
801
|
+
if (skipped.length > 0) {
|
|
802
|
+
ui.showMessage(`\uAE30\uC874 \uD6C5\uC774 \uC788\uC5B4 \uAC74\uB108\uB700: ${skipped.join(", ")}`, "warn");
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
ui.cleanup();
|
|
806
|
+
});
|
|
807
|
+
async function getDiff(repo, filePaths) {
|
|
808
|
+
const { simpleGit: simpleGit3 } = await import("simple-git");
|
|
809
|
+
const git = simpleGit3(repo.path);
|
|
810
|
+
await git.add(filePaths);
|
|
811
|
+
const diff = await git.diff(["--cached", "--", ...filePaths]);
|
|
812
|
+
return diff;
|
|
813
|
+
}
|
|
814
|
+
program.parse();
|
|
815
|
+
//# sourceMappingURL=index.js.map
|