UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

176 lines 8.33 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /** * Git Commit Tool * * Create a commit with the staged changes. */ import { Box, Text } from 'ink'; import React from 'react'; import { useTheme } from '../../hooks/useTheme.js'; import { jsonSchema, tool } from '../../types/core.js'; import { execGit, getDiffStats, hasStagedChanges, isLastCommitPushed, parseGitStatus, } from './utils.js'; // ============================================================================ // Preview (for formatter before execution) // ============================================================================ async function getCommitPreview(args) { // Get staged files const statusOutput = await execGit(['status', '--porcelain']); const { staged } = parseGitStatus(statusOutput); // Get diff stats const stats = await getDiffStats(true); for (const file of staged) { const fileStats = stats.get(file.path); if (fileStats) { file.additions = fileStats.additions; file.deletions = fileStats.deletions; } } const totalAdditions = staged.reduce((sum, f) => sum + f.additions, 0); const totalDeletions = staged.reduce((sum, f) => sum + f.deletions, 0); // Check if amending a pushed commit let amendWarning = null; if (args.amend) { const pushed = await isLastCommitPushed(); if (pushed) { amendWarning = 'Warning: Amending a commit that has already been pushed!'; } } return { staged, totalAdditions, totalDeletions, amendWarning }; } // ============================================================================ // Execution // ============================================================================ const executeGitCommit = async (args) => { try { // Check for staged changes (unless amending) if (!args.amend) { const hasStaged = await hasStagedChanges(); if (!hasStaged) { return 'Error: No staged changes to commit. Use git_add to stage changes first.'; } } // Build commit message let fullMessage = args.message; if (args.body) { fullMessage = `${args.message}\n\n${args.body}`; } // Build git command const gitArgs = ['commit', '-m', fullMessage]; if (args.amend) { gitArgs.push('--amend'); } if (args.noVerify) { gitArgs.push('--no-verify'); } const output = await execGit(gitArgs); // Parse the output to get commit info const lines = []; // Extract commit hash if present const hashMatch = output.match(/\[[\w-]+\s+([a-f0-9]+)\]/); if (hashMatch) { lines.push(`Commit created: ${hashMatch[1]}`); } else { lines.push('Commit created successfully.'); } lines.push(''); lines.push(`Message: ${args.message}`); if (args.body) { lines.push(`Body: ${args.body.substring(0, 100)}${args.body.length > 100 ? '...' : ''}`); } if (args.amend) { lines.push(''); lines.push('(Amended previous commit)'); } return lines.join('\n'); } catch (error) { return `Error: ${error instanceof Error ? error.message : 'Unknown error'}`; } }; // ============================================================================ // Tool Definition // ============================================================================ const gitCommitCoreTool = tool({ description: 'Create a git commit with staged changes. Requires a commit message. Use body for extended description, amend to modify the last commit.', inputSchema: jsonSchema({ type: 'object', properties: { message: { type: 'string', description: 'The commit message (required)', }, body: { type: 'string', description: 'Extended description (will be separated by blank line)', }, amend: { type: 'boolean', description: 'Amend the previous commit instead of creating a new one', }, noVerify: { type: 'boolean', description: 'Skip pre-commit and commit-msg hooks', }, }, required: ['message'], }), // ALWAYS_APPROVE - user should see the commit message before creation needsApproval: () => true, execute: async (args, _options) => { return await executeGitCommit(args); }, }); // ============================================================================ // Formatter // ============================================================================ function GitCommitFormatter({ args, result, }) { const { colors } = useTheme(); const [preview, setPreview] = React.useState(null); // Load preview before execution React.useEffect(() => { if (!result) { getCommitPreview(args) .then(setPreview) .catch(() => { }); } }, [args, result]); const stagedCount = preview?.staged.length || 0; const additions = preview?.totalAdditions || 0; const deletions = preview?.totalDeletions || 0; return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: colors.tool, children: "\u2692 git_commit" }), !result && stagedCount > 0 && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Staged: " }), _jsxs(Text, { color: colors.text, children: [stagedCount, " files "] }), _jsxs(Text, { color: colors.success, children: ["(+", additions] }), _jsx(Text, { color: colors.text, children: ", " }), _jsxs(Text, { color: colors.error, children: ["-", deletions] }), _jsx(Text, { color: colors.text, children: ")" })] })), args.amend && (_jsx(Box, { children: _jsx(Text, { color: colors.warning, children: "Amending previous commit" }) })), preview?.amendWarning && (_jsx(Box, { children: _jsx(Text, { color: colors.error, children: preview.amendWarning }) })), args.noVerify && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Hooks: " }), _jsx(Text, { color: colors.warning, children: "skipped" })] })), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: colors.secondary, children: "Message:" }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: colors.primary, children: args.message }) })] }), args.body && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: colors.secondary, children: "Body:" }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: colors.text, children: args.body.length > 100 ? `${args.body.substring(0, 100)}...` : args.body }) })] })), result?.includes('Commit created') && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.success, children: "\u2713 Commit created successfully" }) })), result?.includes('Error:') && (_jsx(Box, { children: _jsx(Text, { color: colors.error, children: result }) }))] })); } const formatter = (args, result) => { return _jsx(GitCommitFormatter, { args: args, result: result }); }; // ============================================================================ // Validator // ============================================================================ const validator = async (args) => { if (!args.message || args.message.trim().length === 0) { return { valid: false, error: 'Commit message cannot be empty' }; } // Check for staged changes (unless amending) if (!args.amend) { const hasStaged = await hasStagedChanges(); if (!hasStaged) { return { valid: false, error: 'No staged changes. Use git_add to stage changes first.', }; } } return { valid: true }; }; // ============================================================================ // Export // ============================================================================ export const gitCommitTool = { name: 'git_commit', tool: gitCommitCoreTool, formatter, validator, }; //# sourceMappingURL=git-commit.js.map