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