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

183 lines 8.87 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; /** * Git Reset Tool * * Reset/undo operations: soft, mixed, hard. */ import { Box, Text } from 'ink'; import React from 'react'; import { getCurrentMode } from '../../context/mode-context.js'; import { useTheme } from '../../hooks/useTheme.js'; import { jsonSchema, tool } from '../../types/core.js'; import { execGit, getCommits, getDiffStats, parseGitStatus, } from './utils.js'; // ============================================================================ // Preview // ============================================================================ async function getResetPreview(args) { const target = args.target || 'HEAD'; // If resetting to HEAD, no commits affected if (target === 'HEAD' && !args.file) { // Just show currently staged changes const statusOutput = await execGit(['status', '--porcelain']); const { staged, unstaged } = parseGitStatus(statusOutput); const stagedStats = await getDiffStats(true); let additions = 0; let deletions = 0; for (const [, stats] of stagedStats) { additions += stats.additions; deletions += stats.deletions; } return { commitsAffected: [], filesAffected: staged.length + unstaged.length, additions, deletions, }; } // Get commits between target and HEAD try { const commits = await getCommits({ range: `${target}..HEAD` }); // Get diff stats between target and HEAD const diffOutput = await execGit(['diff', '--numstat', target, 'HEAD']); let additions = 0; let deletions = 0; let filesAffected = 0; for (const line of diffOutput.split('\n')) { if (!line.trim()) continue; const parts = line.split('\t'); if (parts.length >= 3) { additions += parts[0] === '-' ? 0 : parseInt(parts[0], 10) || 0; deletions += parts[1] === '-' ? 0 : parseInt(parts[1], 10) || 0; filesAffected++; } } return { commitsAffected: commits, filesAffected, additions, deletions }; } catch { return { commitsAffected: [], filesAffected: 0, additions: 0, deletions: 0 }; } } // ============================================================================ // Execution // ============================================================================ const executeGitReset = async (args) => { try { const target = args.target || 'HEAD'; // File-specific reset (ignores mode) if (args.file) { await execGit(['checkout', target, '--', args.file]); return `Reset '${args.file}' to ${target}`; } // Get preview for output const preview = await getResetPreview(args); // Build git command const gitArgs = ['reset', `--${args.mode}`, target]; await execGit(gitArgs); const lines = []; lines.push(`Reset to ${target} (${args.mode})`); lines.push(''); switch (args.mode) { case 'soft': lines.push('Changes kept in staging area.'); break; case 'mixed': lines.push('Changes moved to working tree (unstaged).'); break; case 'hard': lines.push('All changes discarded.'); break; } if (preview.commitsAffected.length > 0) { lines.push(''); lines.push(`Commits affected: ${preview.commitsAffected.length}`); for (const commit of preview.commitsAffected.slice(0, 5)) { lines.push(` ${commit.shortHash} ${commit.subject}`); } if (preview.commitsAffected.length > 5) { lines.push(` ... and ${preview.commitsAffected.length - 5} more`); } } if (preview.filesAffected > 0) { lines.push(''); lines.push(`Files affected: ${preview.filesAffected} (+${preview.additions}, -${preview.deletions})`); } return lines.join('\n'); } catch (error) { return `Error: ${error instanceof Error ? error.message : 'Unknown error'}`; } }; // ============================================================================ // Tool Definition // ============================================================================ const gitResetCoreTool = tool({ description: 'Reset repository state. soft=keep staged, mixed=unstage changes, hard=discard all changes (DANGEROUS).', inputSchema: jsonSchema({ type: 'object', properties: { mode: { type: 'string', enum: ['soft', 'mixed', 'hard'], description: 'Reset mode: soft (keep staged), mixed (unstage), hard (discard all)', }, target: { type: 'string', description: 'Target commit/ref to reset to (default: HEAD). Examples: HEAD~1, main, abc1234', }, file: { type: 'string', description: 'Reset a specific file only (uses checkout)', }, }, required: ['mode'], }), // Approval varies by mode needsApproval: (args) => { const mode = getCurrentMode(); // ALWAYS_APPROVE for hard reset (permanent data loss) if (args.mode === 'hard') { return true; } // STANDARD for soft and mixed return mode === 'normal'; }, execute: async (args, _options) => { return await executeGitReset(args); }, }); // ============================================================================ // Formatter // ============================================================================ function GitResetFormatter({ args, result, }) { const { colors } = useTheme(); const [preview, setPreview] = React.useState(null); // Load preview before execution React.useEffect(() => { if (!result) { getResetPreview(args) .then(setPreview) .catch(() => { }); } }, [args, result]); const target = args.target || 'HEAD'; // Mode descriptions const modeDescriptions = { soft: 'Keep changes staged', mixed: 'Unstage changes, keep in working tree', hard: 'Discard all changes permanently', }; return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: colors.tool, children: "\u2692 git_reset" }), args.mode === 'hard' && (_jsx(Box, { children: _jsx(Text, { color: colors.error, children: "This will permanently discard uncommitted work!" }) })), _jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Mode: " }), _jsx(Text, { color: args.mode === 'hard' ? colors.error : colors.text, children: args.mode }), _jsxs(Text, { color: colors.secondary, children: [" - ", modeDescriptions[args.mode]] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Target: " }), _jsx(Text, { color: colors.primary, children: target })] }), args.file && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "File: " }), _jsx(Text, { color: colors.text, children: args.file })] })), preview && !args.file && (_jsxs(_Fragment, { children: [preview.commitsAffected.length > 0 && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Commits affected: " }), _jsx(Text, { color: colors.warning, children: preview.commitsAffected.length })] })), preview.filesAffected > 0 && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Files affected: " }), _jsxs(Text, { color: colors.text, children: [preview.filesAffected, " "] }), _jsxs(Text, { color: colors.success, children: ["(+", preview.additions] }), _jsx(Text, { color: colors.text, children: ", " }), _jsxs(Text, { color: colors.error, children: ["-", preview.deletions] }), _jsx(Text, { color: colors.text, children: ")" })] }))] })), result?.includes('Reset to') && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.success, children: "\u2713 Reset completed" }) })), result?.includes('Error:') && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.error, children: ["\u2717 ", result] }) }))] })); } const formatter = (args, result) => { return _jsx(GitResetFormatter, { args: args, result: result }); }; // ============================================================================ // Export // ============================================================================ export const gitResetTool = { name: 'git_reset', tool: gitResetCoreTool, formatter, }; //# sourceMappingURL=git-reset.js.map