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