@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/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-ZS27WQDW.js";
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 as simpleGit2 } from "simple-git";
19
+ import { simpleGit } from "simple-git";
439
20
  async function commitAndPush(repo, files, message, action, ui, logger) {
440
- const git = simpleGit2(repo.path);
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: simpleGit3 } = await import("simple-git");
819
- const git = simpleGit3(repo.path);
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;