@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
JavaScript
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