@blum84/smart-commit 0.3.2 → 0.4.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/dist/index.js CHANGED
@@ -79,6 +79,7 @@ async function pushWithRetry(repo, git, ui, logger) {
79
79
 
80
80
  // src/ui.ts
81
81
  import termkit from "terminal-kit";
82
+ import stringWidth from "string-width";
82
83
  var term = termkit.terminal;
83
84
  function createUI() {
84
85
  let progressBar = null;
@@ -111,15 +112,21 @@ function createUI() {
111
112
  },
112
113
  showRepoTable(repos) {
113
114
  term("\n");
114
- term.gray(" # Repository Branch Changes Status\n");
115
- term.gray(" \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\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");
115
+ const COL_NUM = 4;
116
+ const COL_REPO = 34;
117
+ const COL_BRANCH = 20;
118
+ const COL_CHANGES = 11;
119
+ term.gray(` ${cwPad("#", COL_NUM)}${cwPad("Repository", COL_REPO)}${cwPad("Branch", COL_BRANCH)}${cwPad("Changes", COL_CHANGES)}Status
120
+ `);
121
+ term.gray(` ${cwPad("\u2500\u2500", COL_NUM)}${cwPad("\u2500".repeat(30), COL_REPO)}${cwPad("\u2500".repeat(18), COL_BRANCH)}${cwPad("\u2500".repeat(9), COL_CHANGES)}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
122
+ `);
116
123
  repos.forEach((repo, i) => {
117
- const shortPath = truncate(repo.path.split("/").slice(-2).join("/"), 30);
118
- const branch = truncate(repo.branch, 18);
124
+ const shortPath = cwTruncate(repo.path.split("/").slice(-2).join("/"), COL_REPO - 2);
125
+ const branch = cwTruncate(repo.branch, COL_BRANCH - 2);
119
126
  const changes = repo.files.length > 0 ? `${repo.files.length} files` : repo.unpushedCommits > 0 ? `${repo.unpushedCommits} unpushed` : "-";
120
127
  const status = statusIcon(repo.status);
121
128
  const num = String(i + 1).padStart(2);
122
- const line = ` ${num} ${padEnd(shortPath, 32)}${padEnd(branch, 20)}${padEnd(changes, 11)}${status}
129
+ const line = ` ${cwPad(num, COL_NUM)}${cwPad(shortPath, COL_REPO)}${cwPad(branch, COL_BRANCH)}${cwPad(changes, COL_CHANGES)}${status}
123
130
  `;
124
131
  if (repo.status === "dirty") {
125
132
  term.yellow(line);
@@ -157,7 +164,7 @@ function createUI() {
157
164
  term.bold(`
158
165
  \u{1F4C2} ${shortPath}
159
166
  `);
160
- 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");
167
+ 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");
161
168
  term.green(` ${message.split("\n")[0]}
162
169
  `);
163
170
  const body = message.split("\n").slice(1).join("\n").trim();
@@ -228,6 +235,24 @@ function createUI() {
228
235
  break;
229
236
  }
230
237
  },
238
+ showSpinner(label) {
239
+ const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
240
+ let idx = 0;
241
+ let running = true;
242
+ const interval = setInterval(() => {
243
+ if (!running) return;
244
+ term.column(1);
245
+ term.eraseLine();
246
+ term.cyan(` ${frames[idx % frames.length]} ${label}`);
247
+ idx++;
248
+ }, 80);
249
+ return () => {
250
+ running = false;
251
+ clearInterval(interval);
252
+ term.column(1);
253
+ term.eraseLine();
254
+ };
255
+ },
231
256
  showComplete() {
232
257
  term("\n");
233
258
  term.bold.green(" \u{1F389} \uBAA8\uB4E0 \uC800\uC7A5\uC18C \uC791\uC5C5 \uC644\uB8CC!\n\n");
@@ -253,14 +278,23 @@ function statusIcon(status) {
253
278
  return "\u{1F512} Locked";
254
279
  }
255
280
  }
256
- function truncate(str, maxLen) {
257
- if (str.length <= maxLen) return str;
258
- return str.slice(0, maxLen - 1) + "\u2026";
281
+ function cwTruncate(str, maxWidth) {
282
+ let width = 0;
283
+ let i = 0;
284
+ for (const char of str) {
285
+ const cw = stringWidth(char);
286
+ if (width + cw > maxWidth - 1) {
287
+ return str.slice(0, i) + "\u2026";
288
+ }
289
+ width += cw;
290
+ i += char.length;
291
+ }
292
+ return str;
259
293
  }
260
- function padEnd(str, len) {
261
- const diff = len - str.length;
262
- if (diff <= 0) return str;
263
- return str + " ".repeat(diff);
294
+ function cwPad(str, targetWidth) {
295
+ const sw = stringWidth(str);
296
+ if (sw >= targetWidth) return str;
297
+ return str + " ".repeat(targetWidth - sw);
264
298
  }
265
299
 
266
300
  // src/index.ts
@@ -347,9 +381,11 @@ program.name("smart-commit").description("AI-powered intelligent Git auto-commit
347
381
  commitMsg = await ui.promptOfflineTemplate(getOfflineTemplates());
348
382
  }
349
383
  } else {
384
+ const stopSpinner = ui.showSpinner("AI \uCEE4\uBC0B \uBA54\uC2DC\uC9C0 \uC0DD\uC131 \uC911...");
350
385
  const diff = await getDiff(repo, group.files.map((f) => f.path));
351
386
  const summarizedDiff = await ai.summarizeDiff(diff);
352
387
  commitMsg = await ai.generateCommitMessage(summarizedDiff, config.commit.language);
388
+ stopSpinner();
353
389
  if (!commitMsg) {
354
390
  ui.showMessage(`${repo.path} [${group.label}]: AI \uBA54\uC2DC\uC9C0 \uC0DD\uC131 \uC2E4\uD328`, "warn");
355
391
  if (!isHeadless) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/committer.ts","../src/ui.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { createRequire } from \"node:module\";\nimport { loadConfig } from \"./config.js\";\n\nconst require = createRequire(import.meta.url);\nconst { version: PKG_VERSION } = require(\"../package.json\");\nimport { scanRepositories } from \"./scanner.js\";\nimport { classifyFiles, groupFiles } from \"./classifier.js\";\nimport { createAiClient, isAiAvailable, getOfflineTemplates } from \"./ai-client.js\";\nimport { commitAndPush } from \"./committer.js\";\nimport { createUI } from \"./ui.js\";\nimport { createLogger } from \"./logger.js\";\nimport type { RepoState, UserAction } from \"./types.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"smart-commit\")\n .description(\"AI-powered intelligent Git auto-commit & push CLI tool\")\n .version(PKG_VERSION)\n .option(\"-d, --dry-run\", \"Preview without committing or pushing\")\n .option(\"-g, --group <strategy>\", \"Grouping strategy: smart | single | manual\")\n .option(\"-a, --ai <tool>\", \"AI tool: gemini | claude | gpt | ollama\")\n .option(\"--no-interactive\", \"Headless mode (no prompts)\")\n .option(\"--offline\", \"Offline mode (use templates instead of AI)\")\n .action(async (options) => {\n const config = await loadConfig(options);\n const logger = createLogger();\n const ui = createUI();\n const ai = createAiClient(config, logger);\n const isHeadless = options.interactive === false;\n\n logger.info({ options }, \"smart-commit started\");\n\n ui.showHeader(config, PKG_VERSION);\n\n // Check AI availability (skip in offline mode)\n let offlineMode = options.offline ?? false;\n if (!offlineMode) {\n const primaryAvail = await isAiAvailable(config.ai.primary);\n const fallbackAvail = await isAiAvailable(config.ai.fallback);\n if (!primaryAvail && !fallbackAvail) {\n ui.showMessage(\"AI 도구를 찾을 수 없습니다. 오프라인 모드로 전환합니다.\", \"warn\");\n offlineMode = true;\n logger.warn(\"No AI tools available, switching to offline mode\");\n } else if (!primaryAvail) {\n ui.showMessage(`${config.ai.primary}를 찾을 수 없습니다. ${config.ai.fallback}를 사용합니다.`, \"warn\");\n }\n }\n\n const repos = await scanRepositories(process.cwd(), ui, logger);\n\n if (repos.length === 0) {\n ui.showMessage(\"변경 사항이 있는 저장소가 없습니다.\", \"info\");\n ui.cleanup();\n return;\n }\n\n ui.showRepoTable(repos);\n\n for (const repo of repos) {\n if (repo.status !== \"dirty\") {\n // Handle unpushed commits\n if (repo.status === \"clean\" && repo.unpushedCommits > 0) {\n ui.showMessage(`${repo.path}: 푸시되지 않은 커밋 ${repo.unpushedCommits}개`, \"info\");\n if (options.dryRun) {\n ui.showMessage(\"(dry-run) 푸시를 수행하지 않습니다.\", \"info\");\n } else if (!isHeadless) {\n const action = await ui.promptAction();\n if (action === \"exit\") {\n ui.showMessage(\"종료합니다.\", \"info\");\n ui.cleanup();\n return;\n }\n if (action === \"push\") {\n await commitAndPush(repo, [], \"\", \"push\", ui, logger);\n }\n }\n }\n continue;\n }\n\n const safety = await classifyFiles(repo.files, config);\n\n if (safety.blocked.length > 0) {\n ui.showBlocked(repo, safety.blocked);\n }\n\n if (safety.warned.length > 0) {\n if (isHeadless) {\n ui.showMessage(`${repo.path}: 경고 파일 ${safety.warned.length}개 — headless 모드에서 제외`, \"warn\");\n } else {\n const proceed = await ui.confirmWarned(repo, safety.warned);\n if (proceed) {\n safety.safe.push(...safety.warned);\n }\n }\n }\n\n if (safety.safe.length === 0) {\n ui.showMessage(`${repo.path}: 커밋할 안전한 파일이 없습니다.`, \"warn\");\n continue;\n }\n\n // Group files (skip AI grouping in offline mode)\n const groups = await groupFiles(\n safety.safe,\n offlineMode ? \"single\" : config.grouping.strategy,\n !offlineMode && config.grouping.strategy === \"smart\"\n ? (fileList) => ai.groupFiles(fileList)\n : null,\n logger,\n );\n\n for (const group of groups) {\n let commitMsg: string | null = null;\n\n if (offlineMode) {\n // Offline mode: use template\n if (isHeadless) {\n commitMsg = `chore: auto-commit ${group.files.length} files`;\n } else {\n commitMsg = await ui.promptOfflineTemplate(getOfflineTemplates());\n }\n } else {\n // AI mode\n const diff = await getDiff(repo, group.files.map((f) => f.path));\n const summarizedDiff = await ai.summarizeDiff(diff);\n commitMsg = await ai.generateCommitMessage(summarizedDiff, config.commit.language);\n\n if (!commitMsg) {\n ui.showMessage(`${repo.path} [${group.label}]: AI 메시지 생성 실패`, \"warn\");\n if (!isHeadless) {\n ui.showMessage(\"오프라인 템플릿으로 전환합니다.\", \"info\");\n commitMsg = await ui.promptOfflineTemplate(getOfflineTemplates());\n } else {\n commitMsg = `chore: auto-commit ${group.files.length} files`;\n }\n }\n }\n\n if (!commitMsg) continue;\n\n ui.showCommitPreview(repo, commitMsg, group.files);\n\n if (group.reason) {\n ui.showMessage(` 그룹핑 이유: ${group.reason}`, \"info\");\n }\n\n if (options.dryRun) {\n ui.showMessage(\"(dry-run) 실제 커밋/푸시를 수행하지 않습니다.\", \"info\");\n continue;\n }\n\n const action: UserAction = isHeadless ? \"push\" : await ui.promptAction();\n\n if (action === \"exit\") {\n ui.showMessage(\"종료합니다.\", \"info\");\n ui.cleanup();\n return;\n }\n\n if (action === \"skip-repo\") {\n ui.showMessage(`${repo.path}: 저장소 건너뛰기`, \"info\");\n break; // break out of groups loop, continue to next repo\n }\n\n await commitAndPush(repo, group.files, commitMsg, action, ui, logger);\n }\n }\n\n ui.showComplete();\n ui.cleanup();\n });\n\n// ─── Hook subcommand ───\n\nprogram\n .command(\"hook\")\n .description(\"Install or uninstall Git hooks\")\n .option(\"--uninstall\", \"Remove smart-commit hooks\")\n .action(async (options) => {\n const { installHooks, uninstallHooks } = await import(\"./hooks/install.js\");\n const ui = createUI();\n\n if (options.uninstall) {\n const removed = await uninstallHooks(process.cwd());\n if (removed.length > 0) {\n ui.showMessage(`훅 제거 완료: ${removed.join(\", \")}`, \"success\");\n } else {\n ui.showMessage(\"제거할 smart-commit 훅이 없습니다.\", \"info\");\n }\n } else {\n const { installed, skipped } = await installHooks(process.cwd());\n if (installed.length > 0) {\n ui.showMessage(`훅 설치 완료: ${installed.join(\", \")}`, \"success\");\n }\n if (skipped.length > 0) {\n ui.showMessage(`기존 훅이 있어 건너뜀: ${skipped.join(\", \")}`, \"warn\");\n }\n }\n\n ui.cleanup();\n });\n\nasync function getDiff(repo: RepoState, filePaths: string[]): Promise<string> {\n const { simpleGit } = await import(\"simple-git\");\n const git = simpleGit(repo.path);\n\n // Stage files one by one, skipping any that fail (e.g. gitignored)\n for (const fp of filePaths) {\n try {\n await git.add(fp);\n } catch {\n // Skip files that can't be staged (gitignored, etc.)\n }\n }\n\n const diff = await git.diff([\"--cached\", \"--\", ...filePaths]);\n return diff;\n}\n\nprogram.parse();\n","import { simpleGit } from \"simple-git\";\nimport type { RepoState, FileChange, UserAction } from \"./types.js\";\nimport type { UI } from \"./ui.js\";\nimport type { Logger } from \"pino\";\n\nexport async function commitAndPush(\n repo: RepoState,\n files: FileChange[],\n message: string,\n action: UserAction,\n ui: UI,\n logger: Logger,\n): Promise<void> {\n const git = simpleGit(repo.path);\n\n if (action === \"cancel\") {\n ui.showMessage(`${repo.path}: 건너뜁니다.`, \"info\");\n return;\n }\n\n if (action === \"edit\") {\n // in future: allow user to edit message\n ui.showMessage(\"메시지 편집은 Phase 2에서 지원됩니다.\", \"info\");\n return;\n }\n\n // Stage only safe files (skip gitignored)\n const filePaths = files.map((f) => f.path);\n const staged: string[] = [];\n for (const fp of filePaths) {\n try {\n await git.add(fp);\n staged.push(fp);\n } catch {\n logger.warn({ repo: repo.path, file: fp }, \"Skipped (gitignored or inaccessible)\");\n }\n }\n logger.info({ repo: repo.path, files: staged }, \"Files staged\");\n\n // Commit\n try {\n await git.commit(message);\n ui.showMessage(`${repo.path}: 커밋 완료`, \"success\");\n logger.info({ repo: repo.path, message }, \"Committed\");\n } catch (err) {\n logger.error({ repo: repo.path, err }, \"Commit failed\");\n ui.showMessage(`${repo.path}: 커밋 실패 — ${err}`, \"error\");\n return;\n }\n\n if (action === \"skip\") {\n ui.showMessage(`${repo.path}: 로컬 커밋 유지, 푸시 건너뜀`, \"info\");\n return;\n }\n\n // Push\n if (action === \"push\") {\n await pushWithRetry(repo, git, ui, logger);\n }\n}\n\nasync function pushWithRetry(\n repo: RepoState,\n git: ReturnType<typeof simpleGit>,\n ui: UI,\n logger: Logger,\n): Promise<void> {\n ui.showMessage(`${repo.path}: 푸시 중...`, \"info\");\n\n try {\n await git.push();\n ui.showMessage(`${repo.path}: 푸시 성공!`, \"success\");\n logger.info({ repo: repo.path }, \"Pushed\");\n } catch {\n ui.showMessage(`${repo.path}: 푸시 실패, pull 후 재시도...`, \"warn\");\n logger.warn({ repo: repo.path }, \"Push failed, attempting pull\");\n\n try {\n await git.pull();\n await git.push();\n ui.showMessage(`${repo.path}: pull 후 푸시 성공!`, \"success\");\n logger.info({ repo: repo.path }, \"Push succeeded after pull\");\n } catch (pullErr) {\n ui.showMessage(`${repo.path}: pull/push 실패 — 수동 확인 필요`, \"error\");\n logger.error({ repo: repo.path, err: pullErr }, \"Pull+push failed\");\n }\n }\n}\n","import termkit from \"terminal-kit\";\nimport type { RepoState, FileChange, SmartCommitConfig, UserAction } from \"./types.js\";\n\nconst term = termkit.terminal;\n\nexport interface UI {\n showHeader(config: SmartCommitConfig, version?: string): void;\n showProgress(label: string, current: number, total: number): void;\n showRepoTable(repos: RepoState[]): void;\n showBlocked(repo: RepoState, files: FileChange[]): void;\n confirmWarned(repo: RepoState, files: FileChange[]): Promise<boolean>;\n showCommitPreview(repo: RepoState, message: string, files: FileChange[]): void;\n promptAction(): Promise<UserAction>;\n promptOfflineTemplate(templates: string[]): Promise<string>;\n promptInput(label: string): Promise<string>;\n showMessage(msg: string, level: \"info\" | \"success\" | \"warn\" | \"error\"): void;\n showComplete(): void;\n cleanup(): void;\n}\n\nexport function createUI(): UI {\n let progressBar: termkit.Terminal.ProgressBarController | null = null;\n\n return {\n showHeader(config, version) {\n term.clear();\n term.bold.cyan(`\\n Smart Commit v${version ?? \"unknown\"}\\n`);\n term.gray(` AI: ${config.ai.primary} (fallback: ${config.ai.fallback})\\n`);\n term.gray(` Style: ${config.commit.style} | Language: ${config.commit.language}\\n`);\n term(\"\\n\");\n },\n\n showProgress(label, current, total) {\n if (!progressBar) {\n term(\" \");\n progressBar = term.progressBar({\n width: 50,\n title: label,\n percent: true,\n });\n }\n progressBar.update({ progress: current / total, title: label });\n\n if (current >= total) {\n term(\"\\n\");\n progressBar = null;\n }\n },\n\n showRepoTable(repos) {\n term(\"\\n\");\n term.gray(\" # Repository Branch Changes Status\\n\");\n term.gray(\" ── ───────────────────────────── ────────────────── ───────── ──────────\\n\");\n\n repos.forEach((repo, i) => {\n const shortPath = truncate(repo.path.split(\"/\").slice(-2).join(\"/\"), 30);\n const branch = truncate(repo.branch, 18);\n const changes =\n repo.files.length > 0\n ? `${repo.files.length} files`\n : repo.unpushedCommits > 0\n ? `${repo.unpushedCommits} unpushed`\n : \"-\";\n const status = statusIcon(repo.status);\n const num = String(i + 1).padStart(2);\n\n const line = ` ${num} ${padEnd(shortPath, 32)}${padEnd(branch, 20)}${padEnd(changes, 11)}${status}\\n`;\n\n if (repo.status === \"dirty\") {\n term.yellow(line);\n } else {\n term(line);\n }\n });\n\n term(\"\\n\");\n },\n\n showBlocked(repo, files) {\n const shortPath = repo.path.split(\"/\").slice(-1)[0];\n term.red(` ✖ ${shortPath}: 차단된 파일 (커밋 제외)\\n`);\n for (const f of files) {\n term.red(` - ${f.path}\\n`);\n }\n term(\"\\n\");\n },\n\n async confirmWarned(repo, files) {\n const shortPath = repo.path.split(\"/\").slice(-1)[0];\n term.yellow(` ⚠ ${shortPath}: 주의 필요한 파일\\n`);\n for (const f of files) {\n term.yellow(` - ${f.path}\\n`);\n }\n\n term(\"\\n 포함하시겠습니까? \");\n const result = await term.yesOrNo({ yes: [\"y\", \"ENTER\"], no: [\"n\"] })\n .promise;\n term(\"\\n\");\n return result ?? false;\n },\n\n showCommitPreview(repo, message, files) {\n const shortPath = repo.path.split(\"/\").slice(-2).join(\"/\");\n term.bold(`\\n 📂 ${shortPath}\\n`);\n term(\" ──────────────────────���──────────────────\\n\");\n term.green(` ${message.split(\"\\n\")[0]}\\n`);\n\n const body = message.split(\"\\n\").slice(1).join(\"\\n\").trim();\n if (body) {\n term.gray(` ${body.replace(/\\n/g, \"\\n \")}\\n`);\n }\n\n term(\" ─────────────────────────────────────────\\n\");\n term.gray(` Files (${files.length}):\\n`);\n for (const f of files.slice(0, 10)) {\n const icon = f.status === \"added\" ? \"A\" : f.status === \"deleted\" ? \"D\" : \"M\";\n term.gray(` ${icon} ${f.path}\\n`);\n }\n if (files.length > 10) {\n term.gray(` ... and ${files.length - 10} more\\n`);\n }\n term(\"\\n\");\n },\n\n async promptAction() {\n const items = [\n \"Push (푸시 실행)\",\n \"Skip (로컬 커밋 유지)\",\n \"Cancel (커밋 취소)\",\n \"Skip repo (이 저장소 건너뛰기)\",\n \"Exit (종료)\",\n ];\n\n term(\" ▶ Select action:\\n\");\n const response = await term.singleColumnMenu(items).promise;\n term(\"\\n\");\n\n const map: UserAction[] = [\"push\", \"skip\", \"cancel\", \"skip-repo\", \"exit\"];\n return map[response.selectedIndex] ?? \"skip\";\n },\n\n async promptOfflineTemplate(templates) {\n term.yellow(\" ⚠ AI 사용 불가 — 오프라인 템플릿을 선택하세요:\\n\");\n const response = await term.singleColumnMenu(templates).promise;\n term(\"\\n\");\n\n const selected = templates[response.selectedIndex];\n term(\" 커밋 메시지를 입력하세요 (접두사 포함): \");\n const input = await term.inputField({ default: selected }).promise;\n term(\"\\n\");\n return input ?? selected;\n },\n\n async promptInput(label) {\n term(` ${label}: `);\n const input = await term.inputField().promise;\n term(\"\\n\");\n return input ?? \"\";\n },\n\n showMessage(msg, level) {\n const icon = { info: \"ℹ\", success: \"✅\", warn: \"⚠️\", error: \"✖\" };\n const text = ` ${icon[level]} ${msg}\\n`;\n switch (level) {\n case \"info\": term.cyan(text); break;\n case \"success\": term.green(text); break;\n case \"warn\": term.yellow(text); break;\n case \"error\": term.red(text); break;\n }\n },\n\n showComplete() {\n term(\"\\n\");\n term.bold.green(\" 🎉 모든 저장소 작업 완료!\\n\\n\");\n },\n\n cleanup() {\n term.processExit(0);\n },\n };\n}\n\nfunction statusIcon(status: RepoState[\"status\"]): string {\n switch (status) {\n case \"dirty\":\n return \"📝 변경됨\";\n case \"clean\":\n return \"✅ Clean\";\n case \"detached\":\n return \"⚠️ Detached\";\n case \"rebasing\":\n return \"🔄 Rebasing\";\n case \"merging\":\n return \"🔀 Merging\";\n case \"locked\":\n return \"🔒 Locked\";\n }\n}\n\nfunction truncate(str: string, maxLen: number): string {\n if (str.length <= maxLen) return str;\n return str.slice(0, maxLen - 1) + \"…\";\n}\n\nfunction padEnd(str: string, len: number): string {\n // Simple padding — works better than term.table for CJK characters\n const diff = len - str.length;\n if (diff <= 0) return str;\n return str + \" \".repeat(diff);\n}\n"],"mappings":"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","names":["require","simpleGit"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/committer.ts","../src/ui.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { createRequire } from \"node:module\";\nimport { loadConfig } from \"./config.js\";\n\nconst require = createRequire(import.meta.url);\nconst { version: PKG_VERSION } = require(\"../package.json\");\nimport { scanRepositories } from \"./scanner.js\";\nimport { classifyFiles, groupFiles } from \"./classifier.js\";\nimport { createAiClient, isAiAvailable, getOfflineTemplates } from \"./ai-client.js\";\nimport { commitAndPush } from \"./committer.js\";\nimport { createUI } from \"./ui.js\";\nimport { createLogger } from \"./logger.js\";\nimport type { RepoState, UserAction } from \"./types.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"smart-commit\")\n .description(\"AI-powered intelligent Git auto-commit & push CLI tool\")\n .version(PKG_VERSION)\n .option(\"-d, --dry-run\", \"Preview without committing or pushing\")\n .option(\"-g, --group <strategy>\", \"Grouping strategy: smart | single | manual\")\n .option(\"-a, --ai <tool>\", \"AI tool: gemini | claude | gpt | ollama\")\n .option(\"--no-interactive\", \"Headless mode (no prompts)\")\n .option(\"--offline\", \"Offline mode (use templates instead of AI)\")\n .action(async (options) => {\n const config = await loadConfig(options);\n const logger = createLogger();\n const ui = createUI();\n const ai = createAiClient(config, logger);\n const isHeadless = options.interactive === false;\n\n logger.info({ options }, \"smart-commit started\");\n\n ui.showHeader(config, PKG_VERSION);\n\n // Check AI availability (skip in offline mode)\n let offlineMode = options.offline ?? false;\n if (!offlineMode) {\n const primaryAvail = await isAiAvailable(config.ai.primary);\n const fallbackAvail = await isAiAvailable(config.ai.fallback);\n if (!primaryAvail && !fallbackAvail) {\n ui.showMessage(\"AI 도구를 찾을 수 없습니다. 오프라인 모드로 전환합니다.\", \"warn\");\n offlineMode = true;\n logger.warn(\"No AI tools available, switching to offline mode\");\n } else if (!primaryAvail) {\n ui.showMessage(`${config.ai.primary}를 찾을 수 없습니다. ${config.ai.fallback}를 사용합니다.`, \"warn\");\n }\n }\n\n const repos = await scanRepositories(process.cwd(), ui, logger);\n\n if (repos.length === 0) {\n ui.showMessage(\"변경 사항이 있는 저장소가 없습니다.\", \"info\");\n ui.cleanup();\n return;\n }\n\n ui.showRepoTable(repos);\n\n for (const repo of repos) {\n if (repo.status !== \"dirty\") {\n // Handle unpushed commits\n if (repo.status === \"clean\" && repo.unpushedCommits > 0) {\n ui.showMessage(`${repo.path}: 푸시되지 않은 커밋 ${repo.unpushedCommits}개`, \"info\");\n if (options.dryRun) {\n ui.showMessage(\"(dry-run) 푸시를 수행하지 않습니다.\", \"info\");\n } else if (!isHeadless) {\n const action = await ui.promptAction();\n if (action === \"exit\") {\n ui.showMessage(\"종료합니다.\", \"info\");\n ui.cleanup();\n return;\n }\n if (action === \"push\") {\n await commitAndPush(repo, [], \"\", \"push\", ui, logger);\n }\n }\n }\n continue;\n }\n\n const safety = await classifyFiles(repo.files, config);\n\n if (safety.blocked.length > 0) {\n ui.showBlocked(repo, safety.blocked);\n }\n\n if (safety.warned.length > 0) {\n if (isHeadless) {\n ui.showMessage(`${repo.path}: 경고 파일 ${safety.warned.length}개 — headless 모드에서 제외`, \"warn\");\n } else {\n const proceed = await ui.confirmWarned(repo, safety.warned);\n if (proceed) {\n safety.safe.push(...safety.warned);\n }\n }\n }\n\n if (safety.safe.length === 0) {\n ui.showMessage(`${repo.path}: 커밋할 안전한 파일이 없습니다.`, \"warn\");\n continue;\n }\n\n // Group files (skip AI grouping in offline mode)\n const groups = await groupFiles(\n safety.safe,\n offlineMode ? \"single\" : config.grouping.strategy,\n !offlineMode && config.grouping.strategy === \"smart\"\n ? (fileList) => ai.groupFiles(fileList)\n : null,\n logger,\n );\n\n for (const group of groups) {\n let commitMsg: string | null = null;\n\n if (offlineMode) {\n // Offline mode: use template\n if (isHeadless) {\n commitMsg = `chore: auto-commit ${group.files.length} files`;\n } else {\n commitMsg = await ui.promptOfflineTemplate(getOfflineTemplates());\n }\n } else {\n // AI mode\n const stopSpinner = ui.showSpinner(\"AI 커밋 메시지 생성 중...\");\n const diff = await getDiff(repo, group.files.map((f) => f.path));\n const summarizedDiff = await ai.summarizeDiff(diff);\n commitMsg = await ai.generateCommitMessage(summarizedDiff, config.commit.language);\n stopSpinner();\n\n if (!commitMsg) {\n ui.showMessage(`${repo.path} [${group.label}]: AI 메시지 생성 실패`, \"warn\");\n if (!isHeadless) {\n ui.showMessage(\"오프라인 템플릿으로 전환합니다.\", \"info\");\n commitMsg = await ui.promptOfflineTemplate(getOfflineTemplates());\n } else {\n commitMsg = `chore: auto-commit ${group.files.length} files`;\n }\n }\n }\n\n if (!commitMsg) continue;\n\n ui.showCommitPreview(repo, commitMsg, group.files);\n\n if (group.reason) {\n ui.showMessage(` 그룹핑 이유: ${group.reason}`, \"info\");\n }\n\n if (options.dryRun) {\n ui.showMessage(\"(dry-run) 실제 커밋/푸시를 수행하지 않습니다.\", \"info\");\n continue;\n }\n\n const action: UserAction = isHeadless ? \"push\" : await ui.promptAction();\n\n if (action === \"exit\") {\n ui.showMessage(\"종료합니다.\", \"info\");\n ui.cleanup();\n return;\n }\n\n if (action === \"skip-repo\") {\n ui.showMessage(`${repo.path}: 저장소 건너뛰기`, \"info\");\n break; // break out of groups loop, continue to next repo\n }\n\n await commitAndPush(repo, group.files, commitMsg, action, ui, logger);\n }\n }\n\n ui.showComplete();\n ui.cleanup();\n });\n\n// ─── Hook subcommand ───\n\nprogram\n .command(\"hook\")\n .description(\"Install or uninstall Git hooks\")\n .option(\"--uninstall\", \"Remove smart-commit hooks\")\n .action(async (options) => {\n const { installHooks, uninstallHooks } = await import(\"./hooks/install.js\");\n const ui = createUI();\n\n if (options.uninstall) {\n const removed = await uninstallHooks(process.cwd());\n if (removed.length > 0) {\n ui.showMessage(`훅 제거 완료: ${removed.join(\", \")}`, \"success\");\n } else {\n ui.showMessage(\"제거할 smart-commit 훅이 없습니다.\", \"info\");\n }\n } else {\n const { installed, skipped } = await installHooks(process.cwd());\n if (installed.length > 0) {\n ui.showMessage(`훅 설치 완료: ${installed.join(\", \")}`, \"success\");\n }\n if (skipped.length > 0) {\n ui.showMessage(`기존 훅이 있어 건너뜀: ${skipped.join(\", \")}`, \"warn\");\n }\n }\n\n ui.cleanup();\n });\n\nasync function getDiff(repo: RepoState, filePaths: string[]): Promise<string> {\n const { simpleGit } = await import(\"simple-git\");\n const git = simpleGit(repo.path);\n\n // Stage files one by one, skipping any that fail (e.g. gitignored)\n for (const fp of filePaths) {\n try {\n await git.add(fp);\n } catch {\n // Skip files that can't be staged (gitignored, etc.)\n }\n }\n\n const diff = await git.diff([\"--cached\", \"--\", ...filePaths]);\n return diff;\n}\n\nprogram.parse();\n","import { simpleGit } from \"simple-git\";\nimport type { RepoState, FileChange, UserAction } from \"./types.js\";\nimport type { UI } from \"./ui.js\";\nimport type { Logger } from \"pino\";\n\nexport async function commitAndPush(\n repo: RepoState,\n files: FileChange[],\n message: string,\n action: UserAction,\n ui: UI,\n logger: Logger,\n): Promise<void> {\n const git = simpleGit(repo.path);\n\n if (action === \"cancel\") {\n ui.showMessage(`${repo.path}: 건너뜁니다.`, \"info\");\n return;\n }\n\n if (action === \"edit\") {\n // in future: allow user to edit message\n ui.showMessage(\"메시지 편집은 Phase 2에서 지원됩니다.\", \"info\");\n return;\n }\n\n // Stage only safe files (skip gitignored)\n const filePaths = files.map((f) => f.path);\n const staged: string[] = [];\n for (const fp of filePaths) {\n try {\n await git.add(fp);\n staged.push(fp);\n } catch {\n logger.warn({ repo: repo.path, file: fp }, \"Skipped (gitignored or inaccessible)\");\n }\n }\n logger.info({ repo: repo.path, files: staged }, \"Files staged\");\n\n // Commit\n try {\n await git.commit(message);\n ui.showMessage(`${repo.path}: 커밋 완료`, \"success\");\n logger.info({ repo: repo.path, message }, \"Committed\");\n } catch (err) {\n logger.error({ repo: repo.path, err }, \"Commit failed\");\n ui.showMessage(`${repo.path}: 커밋 실패 — ${err}`, \"error\");\n return;\n }\n\n if (action === \"skip\") {\n ui.showMessage(`${repo.path}: 로컬 커밋 유지, 푸시 건너뜀`, \"info\");\n return;\n }\n\n // Push\n if (action === \"push\") {\n await pushWithRetry(repo, git, ui, logger);\n }\n}\n\nasync function pushWithRetry(\n repo: RepoState,\n git: ReturnType<typeof simpleGit>,\n ui: UI,\n logger: Logger,\n): Promise<void> {\n ui.showMessage(`${repo.path}: 푸시 중...`, \"info\");\n\n try {\n await git.push();\n ui.showMessage(`${repo.path}: 푸시 성공!`, \"success\");\n logger.info({ repo: repo.path }, \"Pushed\");\n } catch {\n ui.showMessage(`${repo.path}: 푸시 실패, pull 후 재시도...`, \"warn\");\n logger.warn({ repo: repo.path }, \"Push failed, attempting pull\");\n\n try {\n await git.pull();\n await git.push();\n ui.showMessage(`${repo.path}: pull 후 푸시 성공!`, \"success\");\n logger.info({ repo: repo.path }, \"Push succeeded after pull\");\n } catch (pullErr) {\n ui.showMessage(`${repo.path}: pull/push 실패 — 수동 확인 필요`, \"error\");\n logger.error({ repo: repo.path, err: pullErr }, \"Pull+push failed\");\n }\n }\n}\n","import termkit from \"terminal-kit\";\nimport stringWidth from \"string-width\";\nimport type { RepoState, FileChange, SmartCommitConfig, UserAction } from \"./types.js\";\n\nconst term = termkit.terminal;\n\nexport interface UI {\n showHeader(config: SmartCommitConfig, version?: string): void;\n showProgress(label: string, current: number, total: number): void;\n showRepoTable(repos: RepoState[]): void;\n showBlocked(repo: RepoState, files: FileChange[]): void;\n confirmWarned(repo: RepoState, files: FileChange[]): Promise<boolean>;\n showCommitPreview(repo: RepoState, message: string, files: FileChange[]): void;\n promptAction(): Promise<UserAction>;\n promptOfflineTemplate(templates: string[]): Promise<string>;\n promptInput(label: string): Promise<string>;\n showMessage(msg: string, level: \"info\" | \"success\" | \"warn\" | \"error\"): void;\n showSpinner(label: string): () => void;\n showComplete(): void;\n cleanup(): void;\n}\n\nexport function createUI(): UI {\n let progressBar: termkit.Terminal.ProgressBarController | null = null;\n\n return {\n showHeader(config, version) {\n term.clear();\n term.bold.cyan(`\\n Smart Commit v${version ?? \"unknown\"}\\n`);\n term.gray(` AI: ${config.ai.primary} (fallback: ${config.ai.fallback})\\n`);\n term.gray(` Style: ${config.commit.style} | Language: ${config.commit.language}\\n`);\n term(\"\\n\");\n },\n\n showProgress(label, current, total) {\n if (!progressBar) {\n term(\" \");\n progressBar = term.progressBar({\n width: 50,\n title: label,\n percent: true,\n });\n }\n progressBar.update({ progress: current / total, title: label });\n\n if (current >= total) {\n term(\"\\n\");\n progressBar = null;\n }\n },\n\n showRepoTable(repos) {\n term(\"\\n\");\n\n // Column widths\n const COL_NUM = 4;\n const COL_REPO = 34;\n const COL_BRANCH = 20;\n const COL_CHANGES = 11;\n\n term.gray(` ${cwPad(\"#\", COL_NUM)}${cwPad(\"Repository\", COL_REPO)}${cwPad(\"Branch\", COL_BRANCH)}${cwPad(\"Changes\", COL_CHANGES)}Status\\n`);\n term.gray(` ${cwPad(\"──\", COL_NUM)}${cwPad(\"─\".repeat(30), COL_REPO)}${cwPad(\"─\".repeat(18), COL_BRANCH)}${cwPad(\"─\".repeat(9), COL_CHANGES)}──────────\\n`);\n\n repos.forEach((repo, i) => {\n const shortPath = cwTruncate(repo.path.split(\"/\").slice(-2).join(\"/\"), COL_REPO - 2);\n const branch = cwTruncate(repo.branch, COL_BRANCH - 2);\n const changes =\n repo.files.length > 0\n ? `${repo.files.length} files`\n : repo.unpushedCommits > 0\n ? `${repo.unpushedCommits} unpushed`\n : \"-\";\n const status = statusIcon(repo.status);\n const num = String(i + 1).padStart(2);\n\n const line = ` ${cwPad(num, COL_NUM)}${cwPad(shortPath, COL_REPO)}${cwPad(branch, COL_BRANCH)}${cwPad(changes, COL_CHANGES)}${status}\\n`;\n\n if (repo.status === \"dirty\") {\n term.yellow(line);\n } else {\n term(line);\n }\n });\n\n term(\"\\n\");\n },\n\n showBlocked(repo, files) {\n const shortPath = repo.path.split(\"/\").slice(-1)[0];\n term.red(` ✖ ${shortPath}: 차단된 파일 (커밋 제외)\\n`);\n for (const f of files) {\n term.red(` - ${f.path}\\n`);\n }\n term(\"\\n\");\n },\n\n async confirmWarned(repo, files) {\n const shortPath = repo.path.split(\"/\").slice(-1)[0];\n term.yellow(` ⚠ ${shortPath}: 주의 필요한 파일\\n`);\n for (const f of files) {\n term.yellow(` - ${f.path}\\n`);\n }\n\n term(\"\\n 포함하시겠습니까? \");\n const result = await term.yesOrNo({ yes: [\"y\", \"ENTER\"], no: [\"n\"] })\n .promise;\n term(\"\\n\");\n return result ?? false;\n },\n\n showCommitPreview(repo, message, files) {\n const shortPath = repo.path.split(\"/\").slice(-2).join(\"/\");\n term.bold(`\\n 📂 ${shortPath}\\n`);\n term(\" ─────────────────────────────────────────\\n\");\n term.green(` ${message.split(\"\\n\")[0]}\\n`);\n\n const body = message.split(\"\\n\").slice(1).join(\"\\n\").trim();\n if (body) {\n term.gray(` ${body.replace(/\\n/g, \"\\n \")}\\n`);\n }\n\n term(\" ─────────────────────────────────────────\\n\");\n term.gray(` Files (${files.length}):\\n`);\n for (const f of files.slice(0, 10)) {\n const icon = f.status === \"added\" ? \"A\" : f.status === \"deleted\" ? \"D\" : \"M\";\n term.gray(` ${icon} ${f.path}\\n`);\n }\n if (files.length > 10) {\n term.gray(` ... and ${files.length - 10} more\\n`);\n }\n term(\"\\n\");\n },\n\n async promptAction() {\n const items = [\n \"Push (푸시 실행)\",\n \"Skip (로컬 커밋 유지)\",\n \"Cancel (커밋 취소)\",\n \"Skip repo (이 저장소 건너뛰기)\",\n \"Exit (종료)\",\n ];\n\n term(\" ▶ Select action:\\n\");\n const response = await term.singleColumnMenu(items).promise;\n term(\"\\n\");\n\n const map: UserAction[] = [\"push\", \"skip\", \"cancel\", \"skip-repo\", \"exit\"];\n return map[response.selectedIndex] ?? \"skip\";\n },\n\n async promptOfflineTemplate(templates) {\n term.yellow(\" ⚠ AI 사용 불가 — 오프라인 템플릿을 선택하세요:\\n\");\n const response = await term.singleColumnMenu(templates).promise;\n term(\"\\n\");\n\n const selected = templates[response.selectedIndex];\n term(\" 커밋 메시지를 입력하세요 (접두사 포함): \");\n const input = await term.inputField({ default: selected }).promise;\n term(\"\\n\");\n return input ?? selected;\n },\n\n async promptInput(label) {\n term(` ${label}: `);\n const input = await term.inputField().promise;\n term(\"\\n\");\n return input ?? \"\";\n },\n\n showMessage(msg, level) {\n const icon = { info: \"ℹ\", success: \"✅\", warn: \"⚠️\", error: \"✖\" };\n const text = ` ${icon[level]} ${msg}\\n`;\n switch (level) {\n case \"info\": term.cyan(text); break;\n case \"success\": term.green(text); break;\n case \"warn\": term.yellow(text); break;\n case \"error\": term.red(text); break;\n }\n },\n\n showSpinner(label) {\n const frames = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\n let idx = 0;\n let running = true;\n\n const interval = setInterval(() => {\n if (!running) return;\n term.column(1);\n term.eraseLine();\n term.cyan(` ${frames[idx % frames.length]} ${label}`);\n idx++;\n }, 80);\n\n return () => {\n running = false;\n clearInterval(interval);\n term.column(1);\n term.eraseLine();\n };\n },\n\n showComplete() {\n term(\"\\n\");\n term.bold.green(\" 🎉 모든 저장소 작업 완료!\\n\\n\");\n },\n\n cleanup() {\n term.processExit(0);\n },\n };\n}\n\nfunction statusIcon(status: RepoState[\"status\"]): string {\n switch (status) {\n case \"dirty\":\n return \"📝 변경됨\";\n case \"clean\":\n return \"✅ Clean\";\n case \"detached\":\n return \"⚠️ Detached\";\n case \"rebasing\":\n return \"🔄 Rebasing\";\n case \"merging\":\n return \"🔀 Merging\";\n case \"locked\":\n return \"🔒 Locked\";\n }\n}\n\n// ─── CJK-aware string utilities ───\n\n/** Truncate string to fit within `maxWidth` terminal columns */\nfunction cwTruncate(str: string, maxWidth: number): string {\n let width = 0;\n let i = 0;\n for (const char of str) {\n const cw = stringWidth(char);\n if (width + cw > maxWidth - 1) {\n return str.slice(0, i) + \"…\";\n }\n width += cw;\n i += char.length;\n }\n return str;\n}\n\n/** Pad string to exactly `targetWidth` terminal columns */\nfunction cwPad(str: string, targetWidth: number): string {\n const sw = stringWidth(str);\n if (sw >= targetWidth) return str;\n return str + \" \".repeat(targetWidth - sw);\n}\n"],"mappings":"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","names":["require","simpleGit"]}
@@ -36,6 +36,8 @@ var noopUI = {
36
36
  promptInput: async () => "",
37
37
  showMessage: () => {
38
38
  },
39
+ showSpinner: () => () => {
40
+ },
39
41
  showComplete: () => {
40
42
  },
41
43
  cleanup: () => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/mcp-server.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport { createRequire } from \"node:module\";\nimport { simpleGit } from \"simple-git\";\nimport { loadConfig } from \"./config.js\";\n\nconst require = createRequire(import.meta.url);\nconst { version: PKG_VERSION } = require(\"../package.json\");\nimport { scanRepositories } from \"./scanner.js\";\nimport { classifyFiles, groupFiles } from \"./classifier.js\";\nimport { createAiClient, isAiAvailable, getOfflineTemplates } from \"./ai-client.js\";\nimport { createLogger } from \"./logger.js\";\nimport type { RepoState } from \"./types.js\";\n\nconst logger = createLogger();\n\n// Noop UI for MCP (no terminal interaction)\nconst noopUI = {\n showHeader: () => {},\n showProgress: () => {},\n showRepoTable: () => {},\n showBlocked: () => {},\n confirmWarned: async () => true,\n showCommitPreview: () => {},\n promptAction: async () => \"push\" as const,\n promptOfflineTemplate: async (t: string[]) => t[0] + \"auto-commit\",\n promptInput: async () => \"\",\n showMessage: () => {},\n showComplete: () => {},\n cleanup: () => {},\n};\n\nconst server = new McpServer({\n name: \"smart-commit\",\n version: PKG_VERSION,\n});\n\n// ─── Tool: scan ───\n\nserver.tool(\n \"scan\",\n \"현재 디렉토리 하위의 Git 저장소를 스캔하여 변경 사항을 확인합니다\",\n {\n path: z.string().optional().describe(\"스캔할 디렉토리 경로 (기본: 현재 디렉토리)\"),\n },\n async ({ path }) => {\n const scanPath = path || process.cwd();\n const repos = await scanRepositories(scanPath, noopUI, logger);\n\n const summary = repos.map((r) => ({\n path: r.path,\n branch: r.branch,\n status: r.status,\n files: r.files.length,\n unpushedCommits: r.unpushedCommits,\n }));\n\n const dirty = repos.filter((r) => r.status === \"dirty\");\n const text = [\n `총 ${repos.length}개 저장소 스캔 완료`,\n `변경됨: ${dirty.length}개`,\n \"\",\n ...summary.map(\n (r) =>\n `${r.status === \"dirty\" ? \"📝\" : \"✅\"} ${r.path} [${r.branch}] — ${r.files > 0 ? `${r.files} files` : r.status}`,\n ),\n ].join(\"\\n\");\n\n return { content: [{ type: \"text\", text }] };\n },\n);\n\n// ─── Tool: analyze ───\n\nserver.tool(\n \"analyze\",\n \"특정 저장소의 변경 파일을 분석하고 안전 필터를 적용합니다\",\n {\n repoPath: z.string().describe(\"분석할 Git 저장소 경로\"),\n },\n async ({ repoPath }) => {\n const config = await loadConfig();\n const git = simpleGit(repoPath);\n const status = await git.status();\n\n const files = status.files.map((f) => ({\n path: f.path,\n status: f.working_dir === \"?\" ? \"untracked\" as const : \"modified\" as const,\n size: 0,\n isBinary: false,\n }));\n\n const safety = await classifyFiles(files, config);\n\n const text = [\n `저장소: ${repoPath}`,\n `브랜치: ${status.current}`,\n \"\",\n `✖ 차단: ${safety.blocked.length}개`,\n ...safety.blocked.map((f) => ` - ${f.path}`),\n \"\",\n `⚠ 경고: ${safety.warned.length}개`,\n ...safety.warned.map((f) => ` - ${f.path}`),\n \"\",\n `✅ 안전: ${safety.safe.length}개`,\n ...safety.safe.map((f) => ` - ${f.path}`),\n ].join(\"\\n\");\n\n return { content: [{ type: \"text\", text }] };\n },\n);\n\n// ─── Tool: generate-message ───\n\nserver.tool(\n \"generate-message\",\n \"AI를 사용하여 Git diff 기반 커밋 메시지를 생성합니다\",\n {\n repoPath: z.string().describe(\"Git 저장소 경로\"),\n files: z.array(z.string()).optional().describe(\"특정 파일만 대상으로 지정 (기본: 전체)\"),\n },\n async ({ repoPath, files }) => {\n const config = await loadConfig();\n const ai = createAiClient(config, logger);\n const git = simpleGit(repoPath);\n\n const targetFiles = files ?? (await git.status()).files.map((f) => f.path);\n await git.add(targetFiles);\n const diff = await git.diff([\"--cached\", \"--\", ...targetFiles]);\n\n if (!diff.trim()) {\n return { content: [{ type: \"text\", text: \"변경 사항이 없습니다.\" }] };\n }\n\n const summarized = await ai.summarizeDiff(diff);\n const message = await ai.generateCommitMessage(summarized, config.commit.language);\n\n if (!message) {\n return { content: [{ type: \"text\", text: \"커밋 메시지 생성 실패. AI 도구를 확인하세요.\" }] };\n }\n\n return { content: [{ type: \"text\", text: message }] };\n },\n);\n\n// ─── Tool: commit ───\n\nserver.tool(\n \"commit\",\n \"변경 사항을 커밋합니다 (AI 메시지 자동 생성 또는 직접 지정)\",\n {\n repoPath: z.string().describe(\"Git 저장소 경로\"),\n message: z.string().optional().describe(\"커밋 메시지 (미지정 시 AI 자동 생성)\"),\n push: z.boolean().optional().describe(\"커밋 후 푸시 여부 (기본: false)\"),\n },\n async ({ repoPath, message, push }) => {\n const config = await loadConfig();\n const ai = createAiClient(config, logger);\n const git = simpleGit(repoPath);\n\n const status = await git.status();\n if (status.files.length === 0) {\n return { content: [{ type: \"text\", text: \"커밋할 변경 사항이 없습니다.\" }] };\n }\n\n const files = status.files.map((f) => f.path);\n const safety = await classifyFiles(\n status.files.map((f) => ({\n path: f.path,\n status: \"modified\" as const,\n size: 0,\n isBinary: false,\n })),\n config,\n );\n\n const safeFiles = safety.safe.map((f) => f.path);\n if (safeFiles.length === 0) {\n return {\n content: [{\n type: \"text\",\n text: `안전한 파일이 없습니다. 차단: ${safety.blocked.map((f) => f.path).join(\", \")}`,\n }],\n };\n }\n\n // Generate or use provided message\n let commitMsg = message;\n if (!commitMsg) {\n await git.add(safeFiles);\n const diff = await git.diff([\"--cached\", \"--\", ...safeFiles]);\n const summarized = await ai.summarizeDiff(diff);\n commitMsg = await ai.generateCommitMessage(summarized, config.commit.language) ?? undefined;\n }\n\n if (!commitMsg) {\n return { content: [{ type: \"text\", text: \"커밋 메시지 생성 실패.\" }] };\n }\n\n // Commit\n await git.add(safeFiles);\n const commitResult = await git.commit(commitMsg);\n\n const lines = [\n `커밋 완료: ${commitResult.commit}`,\n `메시지: ${commitMsg.split(\"\\n\")[0]}`,\n `파일: ${safeFiles.length}개`,\n ];\n\n if (safety.blocked.length > 0) {\n lines.push(`차단됨: ${safety.blocked.map((f) => f.path).join(\", \")}`);\n }\n\n // Push if requested\n if (push) {\n try {\n await git.push();\n lines.push(\"푸시 완료!\");\n } catch {\n try {\n const branch = status.current ?? \"main\";\n await git.push([\"--set-upstream\", \"origin\", branch]);\n lines.push(\"푸시 완료! (upstream 설정됨)\");\n } catch (pushErr) {\n lines.push(`푸시 실패: ${pushErr}`);\n }\n }\n }\n\n return { content: [{ type: \"text\", text: lines.join(\"\\n\") }] };\n },\n);\n\n// ─── Tool: config ───\n\nserver.tool(\n \"config\",\n \"현재 smart-commit 설정을 확인합니다\",\n {},\n async () => {\n const config = await loadConfig();\n const primaryAvail = await isAiAvailable(config.ai.primary);\n const fallbackAvail = await isAiAvailable(config.ai.fallback);\n\n const text = [\n \"smart-commit 설정\",\n \"\",\n `AI Primary: ${config.ai.primary} (${primaryAvail ? \"✅ 사용 가능\" : \"❌ 미설치\"})`,\n `AI Fallback: ${config.ai.fallback} (${fallbackAvail ? \"✅ 사용 가능\" : \"❌ 미설치\"})`,\n `AI Timeout: ${config.ai.timeout}초`,\n \"\",\n `Commit Style: ${config.commit.style}`,\n `Language: ${config.commit.language}`,\n `Max Diff Size: ${config.commit.maxDiffSize}`,\n \"\",\n `Grouping: ${config.grouping.strategy}`,\n \"\",\n `Blocked Patterns: ${config.safety.blockedPatterns.join(\", \")}`,\n `Warn Patterns: ${config.safety.warnPatterns.join(\", \")}`,\n `Max File Size: ${config.safety.maxFileSize}`,\n ].join(\"\\n\");\n\n return { content: [{ type: \"text\", text }] };\n },\n);\n\n// ─── Start server ───\n\nasync function main() {\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nmain().catch(console.error);\n"],"mappings":"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","names":["require"]}
1
+ {"version":3,"sources":["../src/mcp-server.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport { createRequire } from \"node:module\";\nimport { simpleGit } from \"simple-git\";\nimport { loadConfig } from \"./config.js\";\n\nconst require = createRequire(import.meta.url);\nconst { version: PKG_VERSION } = require(\"../package.json\");\nimport { scanRepositories } from \"./scanner.js\";\nimport { classifyFiles, groupFiles } from \"./classifier.js\";\nimport { createAiClient, isAiAvailable, getOfflineTemplates } from \"./ai-client.js\";\nimport { createLogger } from \"./logger.js\";\nimport type { RepoState } from \"./types.js\";\n\nconst logger = createLogger();\n\n// Noop UI for MCP (no terminal interaction)\nconst noopUI = {\n showHeader: () => {},\n showProgress: () => {},\n showRepoTable: () => {},\n showBlocked: () => {},\n confirmWarned: async () => true,\n showCommitPreview: () => {},\n promptAction: async () => \"push\" as const,\n promptOfflineTemplate: async (t: string[]) => t[0] + \"auto-commit\",\n promptInput: async () => \"\",\n showMessage: () => {},\n showSpinner: () => () => {},\n showComplete: () => {},\n cleanup: () => {},\n};\n\nconst server = new McpServer({\n name: \"smart-commit\",\n version: PKG_VERSION,\n});\n\n// ─── Tool: scan ───\n\nserver.tool(\n \"scan\",\n \"현재 디렉토리 하위의 Git 저장소를 스캔하여 변경 사항을 확인합니다\",\n {\n path: z.string().optional().describe(\"스캔할 디렉토리 경로 (기본: 현재 디렉토리)\"),\n },\n async ({ path }) => {\n const scanPath = path || process.cwd();\n const repos = await scanRepositories(scanPath, noopUI, logger);\n\n const summary = repos.map((r) => ({\n path: r.path,\n branch: r.branch,\n status: r.status,\n files: r.files.length,\n unpushedCommits: r.unpushedCommits,\n }));\n\n const dirty = repos.filter((r) => r.status === \"dirty\");\n const text = [\n `총 ${repos.length}개 저장소 스캔 완료`,\n `변경됨: ${dirty.length}개`,\n \"\",\n ...summary.map(\n (r) =>\n `${r.status === \"dirty\" ? \"📝\" : \"✅\"} ${r.path} [${r.branch}] — ${r.files > 0 ? `${r.files} files` : r.status}`,\n ),\n ].join(\"\\n\");\n\n return { content: [{ type: \"text\", text }] };\n },\n);\n\n// ─── Tool: analyze ───\n\nserver.tool(\n \"analyze\",\n \"특정 저장소의 변경 파일을 분석하고 안전 필터를 적용합니다\",\n {\n repoPath: z.string().describe(\"분석할 Git 저장소 경로\"),\n },\n async ({ repoPath }) => {\n const config = await loadConfig();\n const git = simpleGit(repoPath);\n const status = await git.status();\n\n const files = status.files.map((f) => ({\n path: f.path,\n status: f.working_dir === \"?\" ? \"untracked\" as const : \"modified\" as const,\n size: 0,\n isBinary: false,\n }));\n\n const safety = await classifyFiles(files, config);\n\n const text = [\n `저장소: ${repoPath}`,\n `브랜치: ${status.current}`,\n \"\",\n `✖ 차단: ${safety.blocked.length}개`,\n ...safety.blocked.map((f) => ` - ${f.path}`),\n \"\",\n `⚠ 경고: ${safety.warned.length}개`,\n ...safety.warned.map((f) => ` - ${f.path}`),\n \"\",\n `✅ 안전: ${safety.safe.length}개`,\n ...safety.safe.map((f) => ` - ${f.path}`),\n ].join(\"\\n\");\n\n return { content: [{ type: \"text\", text }] };\n },\n);\n\n// ─── Tool: generate-message ───\n\nserver.tool(\n \"generate-message\",\n \"AI를 사용하여 Git diff 기반 커밋 메시지를 생성합니다\",\n {\n repoPath: z.string().describe(\"Git 저장소 경로\"),\n files: z.array(z.string()).optional().describe(\"특정 파일만 대상으로 지정 (기본: 전체)\"),\n },\n async ({ repoPath, files }) => {\n const config = await loadConfig();\n const ai = createAiClient(config, logger);\n const git = simpleGit(repoPath);\n\n const targetFiles = files ?? (await git.status()).files.map((f) => f.path);\n await git.add(targetFiles);\n const diff = await git.diff([\"--cached\", \"--\", ...targetFiles]);\n\n if (!diff.trim()) {\n return { content: [{ type: \"text\", text: \"변경 사항이 없습니다.\" }] };\n }\n\n const summarized = await ai.summarizeDiff(diff);\n const message = await ai.generateCommitMessage(summarized, config.commit.language);\n\n if (!message) {\n return { content: [{ type: \"text\", text: \"커밋 메시지 생성 실패. AI 도구를 확인하세요.\" }] };\n }\n\n return { content: [{ type: \"text\", text: message }] };\n },\n);\n\n// ─── Tool: commit ───\n\nserver.tool(\n \"commit\",\n \"변경 사항을 커밋합니다 (AI 메시지 자동 생성 또는 직접 지정)\",\n {\n repoPath: z.string().describe(\"Git 저장소 경로\"),\n message: z.string().optional().describe(\"커밋 메시지 (미지정 시 AI 자동 생성)\"),\n push: z.boolean().optional().describe(\"커밋 후 푸시 여부 (기본: false)\"),\n },\n async ({ repoPath, message, push }) => {\n const config = await loadConfig();\n const ai = createAiClient(config, logger);\n const git = simpleGit(repoPath);\n\n const status = await git.status();\n if (status.files.length === 0) {\n return { content: [{ type: \"text\", text: \"커밋할 변경 사항이 없습니다.\" }] };\n }\n\n const files = status.files.map((f) => f.path);\n const safety = await classifyFiles(\n status.files.map((f) => ({\n path: f.path,\n status: \"modified\" as const,\n size: 0,\n isBinary: false,\n })),\n config,\n );\n\n const safeFiles = safety.safe.map((f) => f.path);\n if (safeFiles.length === 0) {\n return {\n content: [{\n type: \"text\",\n text: `안전한 파일이 없습니다. 차단: ${safety.blocked.map((f) => f.path).join(\", \")}`,\n }],\n };\n }\n\n // Generate or use provided message\n let commitMsg = message;\n if (!commitMsg) {\n await git.add(safeFiles);\n const diff = await git.diff([\"--cached\", \"--\", ...safeFiles]);\n const summarized = await ai.summarizeDiff(diff);\n commitMsg = await ai.generateCommitMessage(summarized, config.commit.language) ?? undefined;\n }\n\n if (!commitMsg) {\n return { content: [{ type: \"text\", text: \"커밋 메시지 생성 실패.\" }] };\n }\n\n // Commit\n await git.add(safeFiles);\n const commitResult = await git.commit(commitMsg);\n\n const lines = [\n `커밋 완료: ${commitResult.commit}`,\n `메시지: ${commitMsg.split(\"\\n\")[0]}`,\n `파일: ${safeFiles.length}개`,\n ];\n\n if (safety.blocked.length > 0) {\n lines.push(`차단됨: ${safety.blocked.map((f) => f.path).join(\", \")}`);\n }\n\n // Push if requested\n if (push) {\n try {\n await git.push();\n lines.push(\"푸시 완료!\");\n } catch {\n try {\n const branch = status.current ?? \"main\";\n await git.push([\"--set-upstream\", \"origin\", branch]);\n lines.push(\"푸시 완료! (upstream 설정됨)\");\n } catch (pushErr) {\n lines.push(`푸시 실패: ${pushErr}`);\n }\n }\n }\n\n return { content: [{ type: \"text\", text: lines.join(\"\\n\") }] };\n },\n);\n\n// ─── Tool: config ───\n\nserver.tool(\n \"config\",\n \"현재 smart-commit 설정을 확인합니다\",\n {},\n async () => {\n const config = await loadConfig();\n const primaryAvail = await isAiAvailable(config.ai.primary);\n const fallbackAvail = await isAiAvailable(config.ai.fallback);\n\n const text = [\n \"smart-commit 설정\",\n \"\",\n `AI Primary: ${config.ai.primary} (${primaryAvail ? \"✅ 사용 가능\" : \"❌ 미설치\"})`,\n `AI Fallback: ${config.ai.fallback} (${fallbackAvail ? \"✅ 사용 가능\" : \"❌ 미설치\"})`,\n `AI Timeout: ${config.ai.timeout}초`,\n \"\",\n `Commit Style: ${config.commit.style}`,\n `Language: ${config.commit.language}`,\n `Max Diff Size: ${config.commit.maxDiffSize}`,\n \"\",\n `Grouping: ${config.grouping.strategy}`,\n \"\",\n `Blocked Patterns: ${config.safety.blockedPatterns.join(\", \")}`,\n `Warn Patterns: ${config.safety.warnPatterns.join(\", \")}`,\n `Max File Size: ${config.safety.maxFileSize}`,\n ].join(\"\\n\");\n\n return { content: [{ type: \"text\", text }] };\n },\n);\n\n// ─── Start server ───\n\nasync function main() {\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nmain().catch(console.error);\n"],"mappings":"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","names":["require"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blum84/smart-commit",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "AI-powered intelligent Git auto-commit & push CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,6 +46,7 @@
46
46
  "minimatch": "^10.0.1",
47
47
  "pino": "^9.6.0",
48
48
  "simple-git": "^3.27.0",
49
+ "string-width": "^8.2.0",
49
50
  "terminal-kit": "^3.1.2",
50
51
  "zod": "^4.3.6"
51
52
  },