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

251 lines 11.9 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; /** * Git Branch Tool * * Branch management: list, create, switch, delete. */ import { Box, Text } from 'ink'; import { getCurrentMode } from '../../context/mode-context.js'; import { useTheme } from '../../hooks/useTheme.js'; import { jsonSchema, tool } from '../../types/core.js'; import { branchExists, execGit, getCurrentBranch, getLocalBranches, getRemoteBranches, hasUncommittedChanges, } from './utils.js'; // ============================================================================ // Execution // ============================================================================ const executeGitBranch = async (args) => { try { // Determine action const action = args.create ? 'create' : args.switch ? 'switch' : args.delete ? 'delete' : 'list'; // LIST if (action === 'list') { const local = await getLocalBranches(); const current = await getCurrentBranch(); const lines = []; lines.push(`Current branch: ${current}`); lines.push(''); lines.push(`Local branches (${local.length}):`); for (const branch of local) { const marker = branch.current ? '* ' : ' '; let info = branch.name; if (branch.upstream) { const sync = []; if (branch.ahead > 0) sync.push(`${branch.ahead} ahead`); if (branch.behind > 0) sync.push(`${branch.behind} behind`); if (sync.length > 0) { info += ` [${branch.upstream}: ${sync.join(', ')}]`; } else { info += ` [${branch.upstream}]`; } } lines.push(`${marker}${info}`); } if (args.all) { const remote = await getRemoteBranches(); lines.push(''); lines.push(`Remote branches (${remote.length}):`); for (const branch of remote.slice(0, 20)) { lines.push(` ${branch}`); } if (remote.length > 20) { lines.push(` ... and ${remote.length - 20} more`); } } return lines.join('\n'); } // CREATE if (action === 'create' && args.create) { const name = args.create; // Check if branch already exists const exists = await branchExists(name); if (exists) { return `Error: Branch '${name}' already exists.`; } // Create and switch to the new branch const gitArgs = ['checkout', '-b', name]; if (args.from) { gitArgs.push(args.from); } await execGit(gitArgs); const lines = []; lines.push(`Created and switched to branch '${name}'`); if (args.from) { lines.push(`Based on: ${args.from}`); } return lines.join('\n'); } // SWITCH if (action === 'switch' && args.switch) { const name = args.switch; // Check if branch exists const exists = await branchExists(name); if (!exists) { // Check if it's a remote branch we can track const remote = await getRemoteBranches(); const remoteBranch = remote.find(r => r === `origin/${name}` || r.endsWith(`/${name}`)); if (remoteBranch) { // Create tracking branch await execGit(['checkout', '-b', name, '--track', remoteBranch]); return `Switched to new branch '${name}' tracking '${remoteBranch}'`; } return `Error: Branch '${name}' does not exist.`; } // Check for uncommitted changes const hasChanges = await hasUncommittedChanges(); if (hasChanges && !args.force) { return 'Error: You have uncommitted changes. Commit, stash, or use force=true to discard them.'; } const gitArgs = ['checkout']; if (args.force) { gitArgs.push('-f'); } gitArgs.push(name); await execGit(gitArgs); return `Switched to branch '${name}'`; } // DELETE if (action === 'delete' && args.delete) { const name = args.delete; const current = await getCurrentBranch(); // Can't delete current branch if (name === current) { return `Error: Cannot delete the currently checked out branch '${name}'.`; } // Check if branch exists const exists = await branchExists(name); if (!exists) { return `Error: Branch '${name}' does not exist.`; } const gitArgs = ['branch']; if (args.force) { gitArgs.push('-D'); } else { gitArgs.push('-d'); } gitArgs.push(name); try { await execGit(gitArgs); return `Deleted branch '${name}'`; } catch (error) { const message = error instanceof Error ? error.message : ''; if (message.includes('not fully merged')) { return `Error: Branch '${name}' is not fully merged. Use force=true to delete anyway (you will lose commits).`; } throw error; } } return 'Error: No valid action specified.'; } catch (error) { return `Error: ${error instanceof Error ? error.message : 'Unknown error'}`; } }; // ============================================================================ // Tool Definition // ============================================================================ const gitBranchCoreTool = tool({ description: 'Manage git branches. List branches (default), create new branch, switch to branch, or delete branch.', inputSchema: jsonSchema({ type: 'object', properties: { list: { type: 'boolean', description: 'List branches (default action)', }, all: { type: 'boolean', description: 'Include remote branches in list', }, create: { type: 'string', description: 'Create a new branch with this name and switch to it', }, from: { type: 'string', description: 'Base branch/commit for new branch (default: HEAD)', }, switch: { type: 'string', description: 'Switch to this branch', }, delete: { type: 'string', description: 'Delete this branch', }, force: { type: 'boolean', description: 'Force operation (discard changes on switch, delete unmerged on delete)', }, }, required: [], }), // Approval varies by action needsApproval: (args) => { const mode = getCurrentMode(); // AUTO for list if (!args.create && !args.switch && !args.delete) { return false; } // ALWAYS_APPROVE for force delete if (args.delete && args.force) { return true; } // STANDARD for create, switch, normal delete return mode === 'normal'; }, execute: async (args, _options) => { return await executeGitBranch(args); }, }); // ============================================================================ // Formatter // ============================================================================ function GitBranchFormatter({ args, result, }) { const { colors } = useTheme(); // Determine action const action = args.create ? 'create' : args.switch ? 'switch' : args.delete ? 'delete' : 'list'; // Parse result for list let currentBranch = ''; let localCount = 0; let remoteCount = 0; if (result && action === 'list') { const currentMatch = result.match(/Current branch: (.+)/); if (currentMatch) currentBranch = currentMatch[1]; const localMatch = result.match(/Local branches \((\d+)\)/); if (localMatch) localCount = parseInt(localMatch[1], 10); const remoteMatch = result.match(/Remote branches \((\d+)\)/); if (remoteMatch) remoteCount = parseInt(remoteMatch[1], 10); } return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: colors.tool, children: "\u2692 git_branch" }), _jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Action: " }), _jsx(Text, { color: colors.text, children: action })] }), action === 'list' && (_jsxs(_Fragment, { children: [currentBranch && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Current: " }), _jsx(Text, { color: colors.primary, children: currentBranch })] })), localCount > 0 && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Local: " }), _jsxs(Text, { color: colors.text, children: [localCount, " branches"] })] })), remoteCount > 0 && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Remote: " }), _jsxs(Text, { color: colors.text, children: [remoteCount, " branches"] })] }))] })), action === 'create' && (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Name: " }), _jsx(Text, { color: colors.primary, children: args.create })] }), args.from && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "From: " }), _jsx(Text, { color: colors.text, children: args.from })] }))] })), action === 'switch' && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Target: " }), _jsx(Text, { color: colors.primary, children: args.switch })] })), action === 'delete' && (_jsxs(_Fragment, { children: [args.force && (_jsx(Box, { children: _jsx(Text, { color: colors.error, children: "\u26A0\uFE0F FORCE DELETE - May lose unmerged commits!" }) })), _jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Branch: " }), _jsx(Text, { color: colors.warning, children: args.delete })] })] })), result?.includes('Switched to') && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.success, children: "\u2713 Switch completed" }) })), result?.includes('Created and switched') && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.success, children: "\u2713 Branch created" }) })), result?.includes('Deleted branch') && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.success, children: "\u2713 Branch deleted" }) })), result?.includes('Error:') && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.error, children: ["\u2717 ", result] }) }))] })); } const formatter = (args, result) => { return _jsx(GitBranchFormatter, { args: args, result: result }); }; // ============================================================================ // Export // ============================================================================ export const gitBranchTool = { name: 'git_branch', tool: gitBranchCoreTool, formatter, }; //# sourceMappingURL=git-branch.js.map