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

260 lines 11.6 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /** * Git Stash Tool * * Stash management: push, pop, apply, list, drop, clear. */ 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, getStashCount, getStashList, hasUncommittedChanges, } from './utils.js'; // ============================================================================ // Execution // ============================================================================ const executeGitStash = async (args) => { try { // Determine action const action = args.push ? 'push' : args.pop ? 'pop' : args.apply ? 'apply' : args.drop ? 'drop' : args.clear ? 'clear' : 'list'; // LIST if (action === 'list') { const stashes = await getStashList(); if (stashes.length === 0) { return 'No stashes found.'; } const lines = []; lines.push(`Stash list (${stashes.length}):`); lines.push(''); for (const stash of stashes) { lines.push(` [${stash.index}] ${stash.message}`); lines.push(` Branch: ${stash.branch}, ${stash.date}`); } return lines.join('\n'); } // PUSH if (action === 'push') { const hasChanges = await hasUncommittedChanges(); if (!hasChanges) { return 'No local changes to stash.'; } const gitArgs = ['stash', 'push']; if (args.push?.message) { gitArgs.push('-m', args.push.message); } if (args.push?.includeUntracked) { gitArgs.push('-u'); } await execGit(gitArgs); const lines = []; lines.push('Changes stashed successfully.'); if (args.push?.message) { lines.push(`Message: ${args.push.message}`); } return lines.join('\n'); } // POP if (action === 'pop') { const count = await getStashCount(); if (count === 0) { return 'No stashes to pop.'; } const index = args.pop?.index ?? 0; if (index >= count) { return `Error: Stash index ${index} does not exist. Available: 0-${count - 1}`; } await execGit(['stash', 'pop', `stash@{${index}}`]); return `Applied and removed stash@{${index}}`; } // APPLY if (action === 'apply') { const count = await getStashCount(); if (count === 0) { return 'No stashes to apply.'; } const index = args.apply?.index ?? 0; if (index >= count) { return `Error: Stash index ${index} does not exist. Available: 0-${count - 1}`; } await execGit(['stash', 'apply', `stash@{${index}}`]); return `Applied stash@{${index}} (stash kept)`; } // DROP if (action === 'drop') { const count = await getStashCount(); if (count === 0) { return 'No stashes to drop.'; } const index = args.drop?.index ?? 0; if (index >= count) { return `Error: Stash index ${index} does not exist. Available: 0-${count - 1}`; } // Get stash info before dropping const stashes = await getStashList(); const stash = stashes.find(s => s.index === index); await execGit(['stash', 'drop', `stash@{${index}}`]); const lines = []; lines.push(`Dropped stash@{${index}}`); if (stash) { lines.push(`Message: ${stash.message}`); } return lines.join('\n'); } // CLEAR if (action === 'clear') { const count = await getStashCount(); if (count === 0) { return 'No stashes to clear.'; } await execGit(['stash', 'clear']); return `Cleared all ${count} stash(es).`; } return 'Error: No valid action specified.'; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; // Check for merge conflicts if (message.includes('CONFLICT') || message.includes('conflict')) { return `Error: Merge conflicts while applying stash. Resolve conflicts manually.\n\n${message}`; } return `Error: ${message}`; } }; // ============================================================================ // Tool Definition // ============================================================================ const gitStashCoreTool = tool({ description: 'Manage git stash. Push to save changes, pop/apply to restore, list to view, drop/clear to remove.', inputSchema: jsonSchema({ type: 'object', properties: { push: { type: 'object', description: 'Stash current changes', properties: { message: { type: 'string', description: 'Optional message for the stash', }, includeUntracked: { type: 'boolean', description: 'Include untracked files', }, }, }, pop: { type: 'object', description: 'Apply and remove a stash', properties: { index: { type: 'number', description: 'Stash index (default: 0)', }, }, }, apply: { type: 'object', description: 'Apply a stash without removing it', properties: { index: { type: 'number', description: 'Stash index (default: 0)', }, }, }, list: { type: 'boolean', description: 'List all stashes', }, drop: { type: 'object', description: 'Remove a specific stash', properties: { index: { type: 'number', description: 'Stash index (default: 0)', }, }, }, clear: { type: 'boolean', description: 'Remove ALL stashes (use with caution!)', }, }, required: [], }), // Approval varies by action needsApproval: (args) => { const mode = getCurrentMode(); // AUTO for list if (args.list || (!args.push && !args.pop && !args.apply && !args.drop && !args.clear)) { return false; } // ALWAYS_APPROVE for drop and clear (permanent data loss) if (args.drop || args.clear) { return true; } // STANDARD for push, pop, apply return mode === 'normal'; }, execute: async (args, _options) => { return await executeGitStash(args); }, }); // ============================================================================ // Formatter // ============================================================================ function GitStashFormatter({ args, result, }) { const { colors } = useTheme(); const [preview, setPreview] = React.useState(null); // Determine action const action = args.push ? 'push' : args.pop ? 'pop' : args.apply ? 'apply' : args.drop ? 'drop' : args.clear ? 'clear' : 'list'; // Load preview before execution React.useEffect(() => { if (!result) { (async () => { const stashCount = await getStashCount(); const stashes = await getStashList(); setPreview({ stashCount, stashes }); })().catch(() => { }); } }, [result]); return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: colors.tool, children: "\u2692 git_stash" }), _jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Action: " }), _jsx(Text, { color: colors.text, children: action })] }), action === 'clear' && (_jsx(Box, { children: _jsxs(Text, { color: colors.error, children: ["\u26A0\uFE0F This will permanently delete all ", preview?.stashCount || 0, ' ', "stashes!"] }) })), action === 'drop' && (_jsx(Box, { children: _jsxs(Text, { color: colors.warning, children: ["\u26A0\uFE0F This will permanently delete stash@", '{', args.drop?.index ?? 0, '}'] }) })), action === 'push' && args.push?.message && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Message: " }), _jsx(Text, { color: colors.text, children: args.push.message })] })), (action === 'pop' || action === 'apply') && preview && preview.stashCount > 0 && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Stash: " }), _jsx(Text, { color: colors.text, children: preview.stashes[args.pop?.index ?? args.apply?.index ?? 0] ?.message || `stash@{${args.pop?.index ?? args.apply?.index ?? 0}}` })] })), action === 'list' && preview && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Count: " }), _jsxs(Text, { color: colors.text, children: [preview.stashCount, " stashes"] })] })), result?.includes('stashed successfully') && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.success, children: "\u2713 Changes stashed" }) })), result?.includes('Applied') && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.success, children: "\u2713 Stash applied" }) })), result?.includes('Dropped') && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.success, children: "\u2713 Stash dropped" }) })), result?.includes('Cleared all') && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.success, children: "\u2713 All stashes cleared" }) })), result?.includes('CONFLICT') && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.error, children: "\u2717 Merge conflicts detected." }) })), result?.includes('Error:') && !result.includes('CONFLICT') && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.error, children: ["\u2717 ", result] }) }))] })); } const formatter = (args, result) => { return _jsx(GitStashFormatter, { args: args, result: result }); }; // ============================================================================ // Export // ============================================================================ export const gitStashTool = { name: 'git_stash', tool: gitStashCoreTool, formatter, }; //# sourceMappingURL=git-stash.js.map