@blum84/smart-commit 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +240 -29
- package/dist/chunk-LYR3U55E.js +496 -0
- package/dist/chunk-LYR3U55E.js.map +1 -0
- package/dist/{chunk-ZS27WQDW.js → chunk-MYMEBX2Q.js} +27 -3
- package/dist/chunk-MYMEBX2Q.js.map +1 -0
- package/dist/{classifier-TTZQUM7N.js → classifier-AINQPFLU.js} +4 -2
- package/dist/index.js +36 -459
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.js +239 -0
- package/dist/mcp-server.js.map +1 -0
- package/package.json +8 -4
- package/dist/chunk-ZS27WQDW.js.map +0 -1
- /package/dist/{classifier-TTZQUM7N.js.map → classifier-AINQPFLU.js.map} +0 -0
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/config.ts","../src/scanner.ts","../src/ai-client.ts","../src/committer.ts","../src/ui.ts","../src/logger.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { loadConfig } from \"./config.js\";\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(\"0.1.0\")\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);\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 (!isHeadless) {\n const action = await ui.promptAction();\n if (action === \"push\") {\n await commitAndPush(repo, [], \"\", \"push\", ui, logger);\n }\n }\n }\n continue;\n }\n\n const safety = 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 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 await git.add(filePaths);\n const diff = await git.diff([\"--cached\", \"--\", ...filePaths]);\n return diff;\n}\n\nprogram.parse();\n","import { cosmiconfig } from \"cosmiconfig\";\nimport type { SmartCommitConfig } from \"./types.js\";\n\nconst DEFAULT_CONFIG: SmartCommitConfig = {\n ai: {\n primary: \"gemini\",\n fallback: \"claude\",\n timeout: 30,\n },\n safety: {\n maxFileSize: \"10MB\",\n blockedPatterns: [\n \"*.env\",\n \".env.*\",\n \"*.pem\",\n \"*.key\",\n \"credentials*\",\n \"*.sqlite\",\n \"*.sqlite3\",\n ],\n warnPatterns: [\n \"*.log\",\n \"*.csv\",\n \"package-lock.json\",\n \"yarn.lock\",\n \"pnpm-lock.yaml\",\n ],\n },\n commit: {\n style: \"conventional\",\n language: \"ko\",\n maxDiffSize: 10000,\n },\n grouping: {\n strategy: \"smart\",\n },\n};\n\nexport async function loadConfig(\n cliOptions: Record<string, unknown> = {},\n): Promise<SmartCommitConfig> {\n const explorer = cosmiconfig(\"smart-commit\", {\n searchPlaces: [\n \".smart-commitrc\",\n \".smart-commitrc.yaml\",\n \".smart-commitrc.yml\",\n \".smart-commitrc.json\",\n \"smart-commit.config.js\",\n \"package.json\",\n ],\n });\n\n const result = await explorer.search();\n const fileConfig = result?.config ?? {};\n\n const config = deepMerge(\n DEFAULT_CONFIG as unknown as Record<string, unknown>,\n fileConfig as Record<string, unknown>,\n ) as unknown as SmartCommitConfig;\n\n if (cliOptions.ai && typeof cliOptions.ai === \"string\") {\n config.ai.primary = cliOptions.ai as string;\n }\n if (cliOptions.group && typeof cliOptions.group === \"string\") {\n config.grouping.strategy = cliOptions.group as \"smart\" | \"single\" | \"manual\";\n }\n\n return config;\n}\n\nfunction deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {\n const result = { ...target };\n for (const key of Object.keys(source)) {\n if (\n source[key] &&\n typeof source[key] === \"object\" &&\n !Array.isArray(source[key]) &&\n target[key] &&\n typeof target[key] === \"object\" &&\n !Array.isArray(target[key])\n ) {\n result[key] = deepMerge(\n target[key] as Record<string, unknown>,\n source[key] as Record<string, unknown>,\n );\n } else {\n result[key] = source[key];\n }\n }\n return result;\n}\n","import { simpleGit, type SimpleGit } from \"simple-git\";\nimport { readdir, stat, access } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport type { RepoState, RepoGitStatus, FileChange } from \"./types.js\";\nimport type { UI } from \"./ui.js\";\nimport type { Logger } from \"pino\";\n\nexport async function scanRepositories(\n baseDir: string,\n ui: UI,\n logger: Logger,\n): Promise<RepoState[]> {\n const gitDirs = await findGitDirs(baseDir);\n const repos: RepoState[] = [];\n\n ui.showProgress(\"Scanning repositories...\", 0, gitDirs.length);\n\n for (let i = 0; i < gitDirs.length; i++) {\n const dir = gitDirs[i];\n ui.showProgress(`Scanning: ${dir}`, i + 1, gitDirs.length);\n\n try {\n const repo = await inspectRepo(dir, logger);\n repos.push(repo);\n } catch (err) {\n logger.warn({ dir, err }, \"Failed to inspect repository\");\n }\n }\n\n return repos;\n}\n\nasync function findGitDirs(baseDir: string): Promise<string[]> {\n const dirs: string[] = [];\n const entries = await readdir(baseDir, { withFileTypes: true });\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n if (entry.name === \"node_modules\" || entry.name.startsWith(\".\")) continue;\n\n const fullPath = join(baseDir, entry.name);\n const gitPath = join(fullPath, \".git\");\n\n try {\n await access(gitPath);\n dirs.push(fullPath);\n } catch {\n // not a git repo, check subdirectories\n const subDirs = await findGitDirs(fullPath);\n dirs.push(...subDirs);\n }\n }\n\n // also check if baseDir itself is a git repo\n try {\n const selfGit = join(baseDir, \".git\");\n await access(selfGit);\n if (!dirs.some((d) => d === baseDir)) {\n dirs.unshift(baseDir);\n }\n } catch {\n // baseDir is not a git repo\n }\n\n return dirs;\n}\n\nasync function inspectRepo(dir: string, logger: Logger): Promise<RepoState> {\n const git: SimpleGit = simpleGit(dir);\n\n const gitStatus = await detectGitStatus(dir, git);\n\n if (gitStatus === \"locked\") {\n logger.warn({ dir }, \"Git index locked — skipping\");\n return { path: dir, branch: \"\", status: \"locked\", files: [], unpushedCommits: 0 };\n }\n if (gitStatus === \"detached\") {\n logger.warn({ dir }, \"Detached HEAD — skipping\");\n return { path: dir, branch: \"HEAD (detached)\", status: \"detached\", files: [], unpushedCommits: 0 };\n }\n if (gitStatus === \"rebasing\") {\n logger.warn({ dir }, \"Rebase in progress — skipping\");\n return { path: dir, branch: \"\", status: \"rebasing\", files: [], unpushedCommits: 0 };\n }\n\n const statusResult = await git.status();\n const branch = statusResult.current ?? \"unknown\";\n\n const files: FileChange[] = [];\n for (const f of statusResult.files) {\n const filePath = join(dir, f.path);\n let size = 0;\n try {\n const s = await stat(filePath);\n size = s.size;\n } catch {\n // file might have been deleted\n }\n\n files.push({\n path: f.path,\n status: mapGitStatus(f.working_dir, f.index),\n size,\n isBinary: false, // will be checked by classifier\n });\n }\n\n let unpushedCommits = 0;\n try {\n const log = await git.log([\"@{u}..HEAD\"]);\n unpushedCommits = log.total;\n } catch {\n // no upstream set\n }\n\n const repoStatus: RepoGitStatus =\n gitStatus === \"merging\"\n ? \"merging\"\n : files.length > 0\n ? \"dirty\"\n : \"clean\";\n\n return { path: dir, branch, status: repoStatus, files, unpushedCommits };\n}\n\nasync function detectGitStatus(dir: string, git: SimpleGit): Promise<RepoGitStatus> {\n // Check lock file\n try {\n await access(join(dir, \".git\", \"index.lock\"));\n return \"locked\";\n } catch {\n // no lock\n }\n\n // Check rebase\n try {\n await access(join(dir, \".git\", \"rebase-merge\"));\n return \"rebasing\";\n } catch {\n // not rebasing\n }\n try {\n await access(join(dir, \".git\", \"rebase-apply\"));\n return \"rebasing\";\n } catch {\n // not rebasing\n }\n\n // Check merge\n try {\n await access(join(dir, \".git\", \"MERGE_HEAD\"));\n return \"merging\";\n } catch {\n // not merging\n }\n\n // Check detached HEAD\n try {\n await git.raw([\"symbolic-ref\", \"HEAD\"]);\n } catch {\n return \"detached\";\n }\n\n return \"clean\";\n}\n\nfunction mapGitStatus(workingDir: string, index: string): FileChange[\"status\"] {\n if (index === \"?\" || workingDir === \"?\") return \"untracked\";\n if (index === \"A\" || workingDir === \"A\") return \"added\";\n if (index === \"D\" || workingDir === \"D\") return \"deleted\";\n if (index === \"R\" || workingDir === \"R\") return \"renamed\";\n return \"modified\";\n}\n","import { execa } from \"execa\";\nimport type { SmartCommitConfig, AiTool } from \"./types.js\";\nimport type { Logger } from \"pino\";\n\nconst CONVENTIONAL_PREFIXES = [\n \"feat\", \"fix\", \"refactor\", \"docs\", \"style\", \"test\", \"chore\", \"perf\", \"ci\", \"build\", \"revert\",\n];\nconst CONVENTIONAL_RE = new RegExp(`^(${CONVENTIONAL_PREFIXES.join(\"|\")})(\\\\(.+\\\\))?!?:\\\\s.+`);\n\n// ─── Offline templates ───\n\nconst OFFLINE_TEMPLATES = CONVENTIONAL_PREFIXES.map((prefix) => `${prefix}: `);\n\nexport function getOfflineTemplates(): string[] {\n return OFFLINE_TEMPLATES;\n}\n\nexport async function isAiAvailable(tool: AiTool): Promise<boolean> {\n try {\n const cmd = tool === \"gpt\" ? \"openai\" : tool;\n await execa(\"which\", [cmd], { timeout: 3000 });\n return true;\n } catch {\n return false;\n }\n}\n\nexport interface AiClient {\n generateCommitMessage(diff: string, language: string): Promise<string | null>;\n resolveConflict(localContent: string, remoteContent: string): Promise<string | null>;\n groupFiles(fileList: string): Promise<string | null>;\n summarizeDiff(diff: string): Promise<string>;\n}\n\nexport function createAiClient(config: SmartCommitConfig, logger: Logger): AiClient {\n async function callWithFallback(prompt: string): Promise<string | null> {\n let result = await callAi(config.ai.primary, prompt, config.ai.timeout, logger, config);\n if (!result && config.ai.fallback !== config.ai.primary) {\n logger.warn({ fallback: config.ai.fallback }, \"Primary AI failed, trying fallback\");\n result = await callAi(config.ai.fallback, prompt, config.ai.timeout, logger, config);\n }\n return result;\n }\n\n return {\n async generateCommitMessage(diff, language) {\n const summarized = await this.summarizeDiff(diff);\n const prompt = buildCommitPrompt(summarized, language, config.commit.style);\n\n logger.info({ tool: config.ai.primary, diffLength: summarized.length }, \"Requesting commit message\");\n\n let result = await callWithFallback(prompt);\n\n if (result) {\n // Conventional commit validation + retry\n if (config.commit.style === \"conventional\" && !validateConventionalCommit(result)) {\n logger.warn({ message: result.split(\"\\n\")[0] }, \"Invalid conventional commit, retrying\");\n const retryPrompt = buildRetryPrompt(result, language);\n const retried = await callWithFallback(retryPrompt);\n if (retried && validateConventionalCommit(retried)) {\n result = retried;\n }\n // use original if retry also fails — better than nothing\n }\n\n // Strip markdown code blocks if AI wrapped it\n result = stripCodeBlocks(result);\n\n logger.info({ messageLength: result.length }, \"Commit message generated\");\n }\n\n return result;\n },\n\n async resolveConflict(localContent, remoteContent) {\n const prompt = buildConflictPrompt(localContent, remoteContent);\n return callWithFallback(prompt);\n },\n\n async groupFiles(fileList) {\n const { buildGroupingPrompt } = await import(\"./classifier.js\");\n const prompt = buildGroupingPrompt(fileList);\n return callWithFallback(prompt);\n },\n\n async summarizeDiff(diff) {\n if (diff.length <= config.commit.maxDiffSize) {\n return diff;\n }\n\n // Smart truncation: stat header + most important hunks\n const statSection = extractDiffStat(diff);\n const hunks = extractKeyHunks(diff, config.commit.maxDiffSize - statSection.length - 200);\n\n const truncated = `${statSection}\\n\\n[주요 변경 내용 (전체 ${diff.length}자 중 핵심부만 추출)]\\n${hunks}`;\n\n // If still too large, ask AI to summarize\n if (truncated.length > config.commit.maxDiffSize * 1.5) {\n logger.info(\"Diff too large, requesting AI summary\");\n const summaryPrompt = buildDiffSummaryPrompt(truncated.slice(0, config.commit.maxDiffSize));\n const summary = await callWithFallback(summaryPrompt);\n return summary ?? truncated.slice(0, config.commit.maxDiffSize);\n }\n\n return truncated;\n },\n };\n}\n\n// ─── Conventional commit validation ───\n\nexport function validateConventionalCommit(message: string): boolean {\n const firstLine = message.split(\"\\n\")[0].trim();\n return CONVENTIONAL_RE.test(firstLine);\n}\n\nfunction stripCodeBlocks(text: string): string {\n return text\n .replace(/^```[\\w]*\\n?/gm, \"\")\n .replace(/^```\\s*$/gm, \"\")\n .trim();\n}\n\n// ─── Diff summarization ───\n\nfunction extractDiffStat(diff: string): string {\n const lines = diff.split(\"\\n\");\n const statLines: string[] = [];\n\n for (const line of lines) {\n if (line.startsWith(\"diff --git\")) {\n statLines.push(line);\n } else if (line.startsWith(\"--- \") || line.startsWith(\"+++ \")) {\n statLines.push(line);\n }\n }\n\n return statLines.join(\"\\n\");\n}\n\nfunction extractKeyHunks(diff: string, maxLength: number): string {\n const hunks: string[] = [];\n let currentHunk = \"\";\n let totalLength = 0;\n\n for (const line of diff.split(\"\\n\")) {\n if (line.startsWith(\"@@\")) {\n if (currentHunk && totalLength + currentHunk.length <= maxLength) {\n hunks.push(currentHunk);\n totalLength += currentHunk.length;\n }\n currentHunk = line + \"\\n\";\n } else if (line.startsWith(\"+\") || line.startsWith(\"-\")) {\n // Prioritize actual changes over context\n currentHunk += line + \"\\n\";\n }\n }\n\n // Don't forget the last hunk\n if (currentHunk && totalLength + currentHunk.length <= maxLength) {\n hunks.push(currentHunk);\n }\n\n return hunks.join(\"\\n\");\n}\n\n// ─── AI call ───\n\nasync function callAi(\n tool: AiTool,\n prompt: string,\n timeout: number,\n logger: Logger,\n config?: SmartCommitConfig,\n): Promise<string | null> {\n try {\n const { command, args } = buildAiCommand(tool, prompt, config);\n\n const { stdout } = await execa(command, args, {\n timeout: timeout * 1000,\n stdin: \"ignore\",\n });\n\n const trimmed = stdout.trim();\n return trimmed || null;\n } catch (err) {\n logger.error({ tool, err }, \"AI call failed\");\n return null;\n }\n}\n\nfunction buildAiCommand(\n tool: AiTool,\n prompt: string,\n config?: SmartCommitConfig,\n): { command: string; args: string[] } {\n switch (tool) {\n case \"gemini\":\n return { command: \"gemini\", args: [prompt] };\n case \"claude\":\n return { command: \"claude\", args: [\"-p\", prompt] };\n case \"gpt\":\n // OpenAI CLI: https://platform.openai.com/docs/guides/command-line\n return { command: \"openai\", args: [\"api\", \"chat.completions.create\", \"-m\", \"gpt-4o\", \"-g\", \"user\", prompt] };\n case \"ollama\": {\n const model = config?.ai?.ollama?.model ?? \"llama3\";\n return { command: \"ollama\", args: [\"run\", model, prompt] };\n }\n default:\n // Generic: treat tool name as command, pass prompt as first arg\n return { command: tool, args: [prompt] };\n }\n}\n\n// ─── Prompt builders ───\n\nfunction buildCommitPrompt(diff: string, language: string, style: string): string {\n const langLabel = language === \"ko\" ? \"한국어\" : \"English\";\n const styleGuide =\n style === \"conventional\"\n ? `Conventional Commits 형식을 반드시 따르세요.\n접두사는 다음 중 선택: ${CONVENTIONAL_PREFIXES.join(\", \")}\n형식: <접두사>(<범위>): <설명> (범위는 선택사항)`\n : \"\";\n\n return `아래의 [Git Diff] 내용을 분석하여 Git Commit Message를 작성해줘.\n\n[CRITICAL INSTRUCTION]\n**결과는 무조건 '${langLabel}'로 작성되어야 합니다.**\n${styleGuide}\n\n[작성 예시]\nfeat(auth): 사용자 로그인 API 구현\n\n- 로그인 요청 처리를 위한 컨트롤러 메서드 추가\n- JWT 토큰 발급 로직 구현\n\n[필수 규칙]\n1. 언어: **100% ${langLabel}**로 작성할 것.\n2. 형식:\n - 첫 줄: 변경 사항을 50자 이내로 요약 (제목)\n - 두 번째 줄: 빈 줄\n - 세 번째 줄부터: 변경된 상세 내용을 불릿 포인트(-)로 정리\n3. 출력: 마크다운 코드 블록이나 부가 설명 없이, 오직 커밋 메시지 텍스트만 출력할 것.\n4. 제한: 어떠한 도구(Functions/Tools)도 사용하지 말 것. 오직 텍스트만 생성하라.\n\n[Git Diff]\n${diff}`;\n}\n\nfunction buildRetryPrompt(invalidMessage: string, language: string): string {\n const langLabel = language === \"ko\" ? \"한국어\" : \"English\";\n return `아래 커밋 메시지가 Conventional Commits 형식에 맞지 않습니다. 수정해주세요.\n\n[현재 메시지]\n${invalidMessage}\n\n[규칙]\n- 첫 줄은 반드시 \"${CONVENTIONAL_PREFIXES.join(\"|\")}(<범위>): <설명>\" 형식이어야 합니다.\n- ${langLabel}로 작성하세요.\n- 수정된 커밋 메시지만 출력하세요.`;\n}\n\nfunction buildConflictPrompt(localContent: string, remoteContent: string): string {\n return `아래에 Git 충돌이 발생한 파일의 [로컬 버전]과 [원격 버전]이 있습니다.\n두 버전을 분석하여 **올바르게 병합된 최종 파일 내용**을 생성해주세요.\n\n[필수 규칙]\n1. 두 버전의 변경 사항을 모두 포함하여 병합할 것\n2. 충돌 마커(<<<<<<, ======, >>>>>>)는 절대 포함하지 말 것\n3. 코드의 논리적 일관성을 유지할 것\n4. 출력은 **오직 병합된 파일 내용만** 출력할 것\n\n[로컬 버전]\n${localContent}\n\n[원격 버전]\n${remoteContent}`;\n}\n\nfunction buildDiffSummaryPrompt(diff: string): string {\n return `아래 Git Diff가 너무 큽니다. 핵심 변경 사항만 요약해주세요.\n\n[규칙]\n1. 어떤 파일에서 무엇이 변경되었는지 요약\n2. 추가/수정/삭제된 주요 함수/클래스/변수 나열\n3. diff 형식으로 출력 (+ / - 접두사 사용)\n4. 500자 이내로 요약\n\n[Diff]\n${diff}`;\n}\n","import { 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\n const filePaths = files.map((f) => f.path);\n await git.add(filePaths);\n logger.info({ repo: repo.path, files: filePaths }, \"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): 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) {\n term.clear();\n term.bold.cyan(\"\\n Smart Commit v0.1.0\\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 ];\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\"];\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","import pino from \"pino\";\nimport { join } from \"node:path\";\nimport { mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\n\nexport function createLogger(): pino.Logger {\n const logDir = join(homedir(), \".smart-commit\", \"logs\");\n\n try {\n mkdirSync(logDir, { recursive: true });\n } catch {\n // fallback: log to stderr only\n return pino({ level: \"info\" });\n }\n\n const today = new Date().toISOString().slice(0, 10);\n const logFile = join(logDir, `${today}.log`);\n\n return pino(\n { level: \"info\" },\n pino.destination({ dest: logFile, append: true, sync: false }),\n );\n}\n"],"mappings":"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","names":["simpleGit","join","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 (!isHeadless) {\n const action = await ui.promptAction();\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 await git.add(filePaths);\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\n const filePaths = files.map((f) => f.path);\n await git.add(filePaths);\n logger.info({ repo: repo.path, files: filePaths }, \"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"]}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createAiClient,
|
|
4
|
+
createLogger,
|
|
5
|
+
isAiAvailable,
|
|
6
|
+
loadConfig,
|
|
7
|
+
scanRepositories
|
|
8
|
+
} from "./chunk-LYR3U55E.js";
|
|
9
|
+
import {
|
|
10
|
+
classifyFiles
|
|
11
|
+
} from "./chunk-MYMEBX2Q.js";
|
|
12
|
+
|
|
13
|
+
// src/mcp-server.ts
|
|
14
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { createRequire } from "module";
|
|
18
|
+
import { simpleGit } from "simple-git";
|
|
19
|
+
var require2 = createRequire(import.meta.url);
|
|
20
|
+
var { version: PKG_VERSION } = require2("../package.json");
|
|
21
|
+
var logger = createLogger();
|
|
22
|
+
var noopUI = {
|
|
23
|
+
showHeader: () => {
|
|
24
|
+
},
|
|
25
|
+
showProgress: () => {
|
|
26
|
+
},
|
|
27
|
+
showRepoTable: () => {
|
|
28
|
+
},
|
|
29
|
+
showBlocked: () => {
|
|
30
|
+
},
|
|
31
|
+
confirmWarned: async () => true,
|
|
32
|
+
showCommitPreview: () => {
|
|
33
|
+
},
|
|
34
|
+
promptAction: async () => "push",
|
|
35
|
+
promptOfflineTemplate: async (t) => t[0] + "auto-commit",
|
|
36
|
+
promptInput: async () => "",
|
|
37
|
+
showMessage: () => {
|
|
38
|
+
},
|
|
39
|
+
showComplete: () => {
|
|
40
|
+
},
|
|
41
|
+
cleanup: () => {
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var server = new McpServer({
|
|
45
|
+
name: "smart-commit",
|
|
46
|
+
version: PKG_VERSION
|
|
47
|
+
});
|
|
48
|
+
server.tool(
|
|
49
|
+
"scan",
|
|
50
|
+
"\uD604\uC7AC \uB514\uB809\uD1A0\uB9AC \uD558\uC704\uC758 Git \uC800\uC7A5\uC18C\uB97C \uC2A4\uCE94\uD558\uC5EC \uBCC0\uACBD \uC0AC\uD56D\uC744 \uD655\uC778\uD569\uB2C8\uB2E4",
|
|
51
|
+
{
|
|
52
|
+
path: z.string().optional().describe("\uC2A4\uCE94\uD560 \uB514\uB809\uD1A0\uB9AC \uACBD\uB85C (\uAE30\uBCF8: \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC)")
|
|
53
|
+
},
|
|
54
|
+
async ({ path }) => {
|
|
55
|
+
const scanPath = path || process.cwd();
|
|
56
|
+
const repos = await scanRepositories(scanPath, noopUI, logger);
|
|
57
|
+
const summary = repos.map((r) => ({
|
|
58
|
+
path: r.path,
|
|
59
|
+
branch: r.branch,
|
|
60
|
+
status: r.status,
|
|
61
|
+
files: r.files.length,
|
|
62
|
+
unpushedCommits: r.unpushedCommits
|
|
63
|
+
}));
|
|
64
|
+
const dirty = repos.filter((r) => r.status === "dirty");
|
|
65
|
+
const text = [
|
|
66
|
+
`\uCD1D ${repos.length}\uAC1C \uC800\uC7A5\uC18C \uC2A4\uCE94 \uC644\uB8CC`,
|
|
67
|
+
`\uBCC0\uACBD\uB428: ${dirty.length}\uAC1C`,
|
|
68
|
+
"",
|
|
69
|
+
...summary.map(
|
|
70
|
+
(r) => `${r.status === "dirty" ? "\u{1F4DD}" : "\u2705"} ${r.path} [${r.branch}] \u2014 ${r.files > 0 ? `${r.files} files` : r.status}`
|
|
71
|
+
)
|
|
72
|
+
].join("\n");
|
|
73
|
+
return { content: [{ type: "text", text }] };
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
server.tool(
|
|
77
|
+
"analyze",
|
|
78
|
+
"\uD2B9\uC815 \uC800\uC7A5\uC18C\uC758 \uBCC0\uACBD \uD30C\uC77C\uC744 \uBD84\uC11D\uD558\uACE0 \uC548\uC804 \uD544\uD130\uB97C \uC801\uC6A9\uD569\uB2C8\uB2E4",
|
|
79
|
+
{
|
|
80
|
+
repoPath: z.string().describe("\uBD84\uC11D\uD560 Git \uC800\uC7A5\uC18C \uACBD\uB85C")
|
|
81
|
+
},
|
|
82
|
+
async ({ repoPath }) => {
|
|
83
|
+
const config = await loadConfig();
|
|
84
|
+
const git = simpleGit(repoPath);
|
|
85
|
+
const status = await git.status();
|
|
86
|
+
const files = status.files.map((f) => ({
|
|
87
|
+
path: f.path,
|
|
88
|
+
status: f.working_dir === "?" ? "untracked" : "modified",
|
|
89
|
+
size: 0,
|
|
90
|
+
isBinary: false
|
|
91
|
+
}));
|
|
92
|
+
const safety = await classifyFiles(files, config);
|
|
93
|
+
const text = [
|
|
94
|
+
`\uC800\uC7A5\uC18C: ${repoPath}`,
|
|
95
|
+
`\uBE0C\uB79C\uCE58: ${status.current}`,
|
|
96
|
+
"",
|
|
97
|
+
`\u2716 \uCC28\uB2E8: ${safety.blocked.length}\uAC1C`,
|
|
98
|
+
...safety.blocked.map((f) => ` - ${f.path}`),
|
|
99
|
+
"",
|
|
100
|
+
`\u26A0 \uACBD\uACE0: ${safety.warned.length}\uAC1C`,
|
|
101
|
+
...safety.warned.map((f) => ` - ${f.path}`),
|
|
102
|
+
"",
|
|
103
|
+
`\u2705 \uC548\uC804: ${safety.safe.length}\uAC1C`,
|
|
104
|
+
...safety.safe.map((f) => ` - ${f.path}`)
|
|
105
|
+
].join("\n");
|
|
106
|
+
return { content: [{ type: "text", text }] };
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
server.tool(
|
|
110
|
+
"generate-message",
|
|
111
|
+
"AI\uB97C \uC0AC\uC6A9\uD558\uC5EC Git diff \uAE30\uBC18 \uCEE4\uBC0B \uBA54\uC2DC\uC9C0\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4",
|
|
112
|
+
{
|
|
113
|
+
repoPath: z.string().describe("Git \uC800\uC7A5\uC18C \uACBD\uB85C"),
|
|
114
|
+
files: z.array(z.string()).optional().describe("\uD2B9\uC815 \uD30C\uC77C\uB9CC \uB300\uC0C1\uC73C\uB85C \uC9C0\uC815 (\uAE30\uBCF8: \uC804\uCCB4)")
|
|
115
|
+
},
|
|
116
|
+
async ({ repoPath, files }) => {
|
|
117
|
+
const config = await loadConfig();
|
|
118
|
+
const ai = createAiClient(config, logger);
|
|
119
|
+
const git = simpleGit(repoPath);
|
|
120
|
+
const targetFiles = files ?? (await git.status()).files.map((f) => f.path);
|
|
121
|
+
await git.add(targetFiles);
|
|
122
|
+
const diff = await git.diff(["--cached", "--", ...targetFiles]);
|
|
123
|
+
if (!diff.trim()) {
|
|
124
|
+
return { content: [{ type: "text", text: "\uBCC0\uACBD \uC0AC\uD56D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." }] };
|
|
125
|
+
}
|
|
126
|
+
const summarized = await ai.summarizeDiff(diff);
|
|
127
|
+
const message = await ai.generateCommitMessage(summarized, config.commit.language);
|
|
128
|
+
if (!message) {
|
|
129
|
+
return { content: [{ type: "text", text: "\uCEE4\uBC0B \uBA54\uC2DC\uC9C0 \uC0DD\uC131 \uC2E4\uD328. AI \uB3C4\uAD6C\uB97C \uD655\uC778\uD558\uC138\uC694." }] };
|
|
130
|
+
}
|
|
131
|
+
return { content: [{ type: "text", text: message }] };
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
server.tool(
|
|
135
|
+
"commit",
|
|
136
|
+
"\uBCC0\uACBD \uC0AC\uD56D\uC744 \uCEE4\uBC0B\uD569\uB2C8\uB2E4 (AI \uBA54\uC2DC\uC9C0 \uC790\uB3D9 \uC0DD\uC131 \uB610\uB294 \uC9C1\uC811 \uC9C0\uC815)",
|
|
137
|
+
{
|
|
138
|
+
repoPath: z.string().describe("Git \uC800\uC7A5\uC18C \uACBD\uB85C"),
|
|
139
|
+
message: z.string().optional().describe("\uCEE4\uBC0B \uBA54\uC2DC\uC9C0 (\uBBF8\uC9C0\uC815 \uC2DC AI \uC790\uB3D9 \uC0DD\uC131)"),
|
|
140
|
+
push: z.boolean().optional().describe("\uCEE4\uBC0B \uD6C4 \uD478\uC2DC \uC5EC\uBD80 (\uAE30\uBCF8: false)")
|
|
141
|
+
},
|
|
142
|
+
async ({ repoPath, message, push }) => {
|
|
143
|
+
const config = await loadConfig();
|
|
144
|
+
const ai = createAiClient(config, logger);
|
|
145
|
+
const git = simpleGit(repoPath);
|
|
146
|
+
const status = await git.status();
|
|
147
|
+
if (status.files.length === 0) {
|
|
148
|
+
return { content: [{ type: "text", text: "\uCEE4\uBC0B\uD560 \uBCC0\uACBD \uC0AC\uD56D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." }] };
|
|
149
|
+
}
|
|
150
|
+
const files = status.files.map((f) => f.path);
|
|
151
|
+
const safety = await classifyFiles(
|
|
152
|
+
status.files.map((f) => ({
|
|
153
|
+
path: f.path,
|
|
154
|
+
status: "modified",
|
|
155
|
+
size: 0,
|
|
156
|
+
isBinary: false
|
|
157
|
+
})),
|
|
158
|
+
config
|
|
159
|
+
);
|
|
160
|
+
const safeFiles = safety.safe.map((f) => f.path);
|
|
161
|
+
if (safeFiles.length === 0) {
|
|
162
|
+
return {
|
|
163
|
+
content: [{
|
|
164
|
+
type: "text",
|
|
165
|
+
text: `\uC548\uC804\uD55C \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uCC28\uB2E8: ${safety.blocked.map((f) => f.path).join(", ")}`
|
|
166
|
+
}]
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
let commitMsg = message;
|
|
170
|
+
if (!commitMsg) {
|
|
171
|
+
await git.add(safeFiles);
|
|
172
|
+
const diff = await git.diff(["--cached", "--", ...safeFiles]);
|
|
173
|
+
const summarized = await ai.summarizeDiff(diff);
|
|
174
|
+
commitMsg = await ai.generateCommitMessage(summarized, config.commit.language) ?? void 0;
|
|
175
|
+
}
|
|
176
|
+
if (!commitMsg) {
|
|
177
|
+
return { content: [{ type: "text", text: "\uCEE4\uBC0B \uBA54\uC2DC\uC9C0 \uC0DD\uC131 \uC2E4\uD328." }] };
|
|
178
|
+
}
|
|
179
|
+
await git.add(safeFiles);
|
|
180
|
+
const commitResult = await git.commit(commitMsg);
|
|
181
|
+
const lines = [
|
|
182
|
+
`\uCEE4\uBC0B \uC644\uB8CC: ${commitResult.commit}`,
|
|
183
|
+
`\uBA54\uC2DC\uC9C0: ${commitMsg.split("\n")[0]}`,
|
|
184
|
+
`\uD30C\uC77C: ${safeFiles.length}\uAC1C`
|
|
185
|
+
];
|
|
186
|
+
if (safety.blocked.length > 0) {
|
|
187
|
+
lines.push(`\uCC28\uB2E8\uB428: ${safety.blocked.map((f) => f.path).join(", ")}`);
|
|
188
|
+
}
|
|
189
|
+
if (push) {
|
|
190
|
+
try {
|
|
191
|
+
await git.push();
|
|
192
|
+
lines.push("\uD478\uC2DC \uC644\uB8CC!");
|
|
193
|
+
} catch {
|
|
194
|
+
try {
|
|
195
|
+
const branch = status.current ?? "main";
|
|
196
|
+
await git.push(["--set-upstream", "origin", branch]);
|
|
197
|
+
lines.push("\uD478\uC2DC \uC644\uB8CC! (upstream \uC124\uC815\uB428)");
|
|
198
|
+
} catch (pushErr) {
|
|
199
|
+
lines.push(`\uD478\uC2DC \uC2E4\uD328: ${pushErr}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
server.tool(
|
|
207
|
+
"config",
|
|
208
|
+
"\uD604\uC7AC smart-commit \uC124\uC815\uC744 \uD655\uC778\uD569\uB2C8\uB2E4",
|
|
209
|
+
{},
|
|
210
|
+
async () => {
|
|
211
|
+
const config = await loadConfig();
|
|
212
|
+
const primaryAvail = await isAiAvailable(config.ai.primary);
|
|
213
|
+
const fallbackAvail = await isAiAvailable(config.ai.fallback);
|
|
214
|
+
const text = [
|
|
215
|
+
"smart-commit \uC124\uC815",
|
|
216
|
+
"",
|
|
217
|
+
`AI Primary: ${config.ai.primary} (${primaryAvail ? "\u2705 \uC0AC\uC6A9 \uAC00\uB2A5" : "\u274C \uBBF8\uC124\uCE58"})`,
|
|
218
|
+
`AI Fallback: ${config.ai.fallback} (${fallbackAvail ? "\u2705 \uC0AC\uC6A9 \uAC00\uB2A5" : "\u274C \uBBF8\uC124\uCE58"})`,
|
|
219
|
+
`AI Timeout: ${config.ai.timeout}\uCD08`,
|
|
220
|
+
"",
|
|
221
|
+
`Commit Style: ${config.commit.style}`,
|
|
222
|
+
`Language: ${config.commit.language}`,
|
|
223
|
+
`Max Diff Size: ${config.commit.maxDiffSize}`,
|
|
224
|
+
"",
|
|
225
|
+
`Grouping: ${config.grouping.strategy}`,
|
|
226
|
+
"",
|
|
227
|
+
`Blocked Patterns: ${config.safety.blockedPatterns.join(", ")}`,
|
|
228
|
+
`Warn Patterns: ${config.safety.warnPatterns.join(", ")}`,
|
|
229
|
+
`Max File Size: ${config.safety.maxFileSize}`
|
|
230
|
+
].join("\n");
|
|
231
|
+
return { content: [{ type: "text", text }] };
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
async function main() {
|
|
235
|
+
const transport = new StdioServerTransport();
|
|
236
|
+
await server.connect(transport);
|
|
237
|
+
}
|
|
238
|
+
main().catch(console.error);
|
|
239
|
+
//# sourceMappingURL=mcp-server.js.map
|
|
@@ -0,0 +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"]}
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blum84/smart-commit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "AI-powered intelligent Git auto-commit & push CLI tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"smart-commit": "dist/index.js"
|
|
7
|
+
"smart-commit": "dist/index.js",
|
|
8
|
+
"smart-commit-mcp": "dist/mcp-server.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
|
-
"dist"
|
|
11
|
+
"dist",
|
|
12
|
+
"package.json"
|
|
11
13
|
],
|
|
12
14
|
"scripts": {
|
|
13
15
|
"build": "tsup",
|
|
@@ -37,13 +39,15 @@
|
|
|
37
39
|
"node": ">=18"
|
|
38
40
|
},
|
|
39
41
|
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
40
43
|
"commander": "^13.1.0",
|
|
41
44
|
"cosmiconfig": "^9.0.0",
|
|
42
45
|
"execa": "^9.5.2",
|
|
43
46
|
"minimatch": "^10.0.1",
|
|
44
47
|
"pino": "^9.6.0",
|
|
45
48
|
"simple-git": "^3.27.0",
|
|
46
|
-
"terminal-kit": "^3.1.2"
|
|
49
|
+
"terminal-kit": "^3.1.2",
|
|
50
|
+
"zod": "^4.3.6"
|
|
47
51
|
},
|
|
48
52
|
"devDependencies": {
|
|
49
53
|
"@types/node": "^22.15.3",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/classifier.ts"],"sourcesContent":["import { minimatch } from \"minimatch\";\nimport { dirname, extname } from \"node:path\";\nimport type {\n FileChange,\n SmartCommitConfig,\n SafetyResult,\n CommitGroup,\n AiGroupingResult,\n} from \"./types.js\";\nimport type { Logger } from \"pino\";\n\nconst SIZE_UNITS: Record<string, number> = {\n B: 1,\n KB: 1024,\n MB: 1024 * 1024,\n GB: 1024 * 1024 * 1024,\n};\n\n// ─── Safety classification ───\n\nexport function classifyFiles(\n files: FileChange[],\n config: SmartCommitConfig,\n): SafetyResult {\n const maxBytes = parseSize(config.safety.maxFileSize);\n const blocked: FileChange[] = [];\n const warned: FileChange[] = [];\n const safe: FileChange[] = [];\n\n for (const file of files) {\n if (isBlocked(file, config.safety.blockedPatterns, maxBytes)) {\n blocked.push(file);\n } else if (isWarned(file, config.safety.warnPatterns)) {\n warned.push(file);\n } else {\n safe.push(file);\n }\n }\n\n return { blocked, warned, safe };\n}\n\n// ─── Grouping ───\n\nexport async function groupFiles(\n files: FileChange[],\n strategy: SmartCommitConfig[\"grouping\"][\"strategy\"],\n callAiForGrouping: ((fileList: string) => Promise<string | null>) | null,\n logger: Logger,\n): Promise<CommitGroup[]> {\n if (files.length === 0) return [];\n\n if (strategy === \"single\") {\n return [{ label: \"all\", files, reason: \"single strategy\" }];\n }\n\n if (strategy === \"smart\" && callAiForGrouping) {\n const aiGroups = await tryAiGrouping(files, callAiForGrouping, logger);\n if (aiGroups) return aiGroups;\n logger.warn(\"AI grouping failed, falling back to rule-based grouping\");\n }\n\n return ruleBasedGrouping(files);\n}\n\nasync function tryAiGrouping(\n files: FileChange[],\n callAi: (fileList: string) => Promise<string | null>,\n logger: Logger,\n): Promise<CommitGroup[] | null> {\n const fileList = files\n .map((f) => `${f.status.charAt(0).toUpperCase()} ${f.path}`)\n .join(\"\\n\");\n\n try {\n const response = await callAi(fileList);\n if (!response) return null;\n\n const parsed = parseAiGroupingResponse(response, files);\n if (parsed.length === 0) return null;\n\n logger.info({ groupCount: parsed.length }, \"AI grouping succeeded\");\n return parsed;\n } catch (err) {\n logger.error({ err }, \"AI grouping parse error\");\n return null;\n }\n}\n\nfunction parseAiGroupingResponse(\n response: string,\n allFiles: FileChange[],\n): CommitGroup[] {\n // Try JSON parse first\n try {\n const cleaned = response.replace(/```json?\\s*/g, \"\").replace(/```\\s*/g, \"\").trim();\n const parsed: AiGroupingResult = JSON.parse(cleaned);\n\n if (!parsed.groups || !Array.isArray(parsed.groups)) return [];\n\n const fileMap = new Map(allFiles.map((f) => [f.path, f]));\n const usedFiles = new Set<string>();\n const groups: CommitGroup[] = [];\n\n for (const g of parsed.groups) {\n const matchedFiles = g.files\n .map((path) => fileMap.get(path))\n .filter((f): f is FileChange => f !== undefined && !usedFiles.has(f.path));\n\n for (const f of matchedFiles) usedFiles.add(f.path);\n\n if (matchedFiles.length > 0) {\n groups.push({\n label: g.label,\n files: matchedFiles,\n reason: g.reason,\n });\n }\n }\n\n // remaining files that AI didn't assign\n const remaining = allFiles.filter((f) => !usedFiles.has(f.path));\n if (remaining.length > 0) {\n groups.push({\n label: \"other\",\n files: remaining,\n reason: \"AI가 분류하지 않은 나머지 파일\",\n });\n }\n\n return groups;\n } catch {\n return [];\n }\n}\n\n// ─── Rule-based fallback grouping ───\n\nexport function ruleBasedGrouping(files: FileChange[]): CommitGroup[] {\n const dirMap = new Map<string, FileChange[]>();\n\n for (const file of files) {\n const dir = dirname(file.path);\n const ext = extname(file.path);\n\n // Group by top-level directory + extension category\n const topDir = dir.split(\"/\")[0] || \".\";\n const category = getCategory(ext);\n const key = `${topDir}/${category}`;\n\n if (!dirMap.has(key)) dirMap.set(key, []);\n dirMap.get(key)!.push(file);\n }\n\n const groups: CommitGroup[] = [];\n for (const [key, groupFiles] of dirMap) {\n groups.push({\n label: key,\n files: groupFiles,\n reason: `디렉토리/유형 기반 그룹핑: ${key}`,\n });\n }\n\n return groups;\n}\n\nfunction getCategory(ext: string): string {\n const categories: Record<string, string[]> = {\n source: [\".ts\", \".tsx\", \".js\", \".jsx\", \".py\", \".go\", \".rs\", \".java\", \".kt\", \".swift\", \".c\", \".cpp\", \".h\"],\n style: [\".css\", \".scss\", \".sass\", \".less\", \".styl\"],\n markup: [\".html\", \".xml\", \".svg\", \".vue\", \".svelte\"],\n config: [\".json\", \".yaml\", \".yml\", \".toml\", \".ini\", \".env\", \".conf\"],\n docs: [\".md\", \".txt\", \".rst\", \".adoc\"],\n test: [\".test.ts\", \".test.js\", \".spec.ts\", \".spec.js\", \".test.py\"],\n };\n\n for (const [category, exts] of Object.entries(categories)) {\n if (exts.includes(ext)) return category;\n }\n return \"other\";\n}\n\n// ─── AI grouping prompt builder ───\n\nexport function buildGroupingPrompt(fileList: string): string {\n return `아래 Git 변경 파일 목록을 분석하여 의미 있는 커밋 단위로 그룹핑해주세요.\n\n[규칙]\n1. 관련된 파일끼리 묶어 하나의 커밋 그룹으로 만들어주세요.\n2. 각 그룹에 짧은 라벨(한국어)과 이유를 붙여주세요.\n3. 반드시 아래 JSON 형식으로만 출력하세요. 다른 텍스트 없이 JSON만 출력하세요.\n\n[출력 형식]\n{\"groups\":[{\"label\":\"그룹명\",\"files\":[\"파일경로1\",\"파일경로2\"],\"reason\":\"그룹핑 이유\"}]}\n\n[변경 파일 목록]\n${fileList}`;\n}\n\n// ─── Utilities ───\n\nfunction isBlocked(\n file: FileChange,\n patterns: string[],\n maxBytes: number,\n): boolean {\n if (file.size > maxBytes) return true;\n if (file.isBinary) return true;\n return matchesAny(file.path, patterns);\n}\n\nfunction isWarned(file: FileChange, patterns: string[]): boolean {\n return matchesAny(file.path, patterns);\n}\n\nfunction matchesAny(filePath: string, patterns: string[]): boolean {\n return patterns.some((pattern) =>\n minimatch(filePath, pattern, { dot: true, matchBase: true }),\n );\n}\n\nfunction parseSize(sizeStr: string): number {\n const match = sizeStr.match(/^(\\d+(?:\\.\\d+)?)\\s*(B|KB|MB|GB)$/i);\n if (!match) return 10 * 1024 * 1024; // default 10MB\n const value = parseFloat(match[1]);\n const unit = match[2].toUpperCase();\n return value * (SIZE_UNITS[unit] ?? 1);\n}\n"],"mappings":"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","names":["groupFiles"]}
|
|
File without changes
|