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

234 lines 10.6 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /** * Git Status Tool * * Shows repository status including branch, changes, and sync state. */ import { Box, Text } from 'ink'; import { useTheme } from '../../hooks/useTheme.js'; import { jsonSchema, tool } from '../../types/core.js'; import { execGit, formatStatusChar, getAheadBehind, getCurrentBranch, getDiffStats, getStashCount, getUpstreamBranch, isMergeInProgress, isRebaseInProgress, parseGitStatus, } from './utils.js'; // ============================================================================ // Execution // ============================================================================ async function getStatus() { // Get branch info const branch = await getCurrentBranch(); const upstream = await getUpstreamBranch(); const { ahead, behind } = await getAheadBehind(); // Get status const statusOutput = await execGit(['status', '--porcelain']); const { staged, unstaged, untracked, conflicts } = parseGitStatus(statusOutput); // Get diff stats for staged files const stagedStats = await getDiffStats(true); for (const file of staged) { const stats = stagedStats.get(file.path); if (stats) { file.additions = stats.additions; file.deletions = stats.deletions; } } // Get diff stats for unstaged files const unstagedStats = await getDiffStats(false); for (const file of unstaged) { const stats = unstagedStats.get(file.path); if (stats) { file.additions = stats.additions; file.deletions = stats.deletions; } } // Calculate totals const totalAdditions = staged.reduce((sum, f) => sum + f.additions, 0) + unstaged.reduce((sum, f) => sum + f.additions, 0); const totalDeletions = staged.reduce((sum, f) => sum + f.deletions, 0) + unstaged.reduce((sum, f) => sum + f.deletions, 0); // Get additional state const stashCount = await getStashCount(); const isRebase = await isRebaseInProgress(); const isMerge = await isMergeInProgress(); return { branch, upstream, ahead, behind, staged, unstaged, untracked, conflicts, stashCount, isRebase, isMerge, totalAdditions, totalDeletions, }; } const executeGitStatus = async (_args) => { try { const status = await getStatus(); const lines = []; // Branch info lines.push(`Branch: ${status.branch}`); if (status.upstream) { lines.push(`Upstream: ${status.upstream}`); if (status.ahead > 0 || status.behind > 0) { const parts = []; if (status.ahead > 0) parts.push(`${status.ahead} ahead`); if (status.behind > 0) parts.push(`${status.behind} behind`); lines.push(`Sync: ${parts.join(', ')}`); } } else { lines.push('Upstream: not set'); } lines.push(''); // Special states if (status.isRebase) { lines.push('STATE: Rebase in progress'); lines.push(''); } if (status.isMerge) { lines.push('STATE: Merge in progress'); lines.push(''); } // Conflicts if (status.conflicts.length > 0) { lines.push(`CONFLICTS (${status.conflicts.length}):`); for (const file of status.conflicts.slice(0, 10)) { lines.push(` UU ${file}`); } if (status.conflicts.length > 10) { lines.push(` ... and ${status.conflicts.length - 10} more`); } lines.push(''); } // Staged changes if (status.staged.length > 0) { const stagedAdd = status.staged.reduce((s, f) => s + f.additions, 0); const stagedDel = status.staged.reduce((s, f) => s + f.deletions, 0); lines.push(`Staged (${status.staged.length} files, +${stagedAdd}, -${stagedDel}):`); for (const file of status.staged.slice(0, 10)) { const char = formatStatusChar(file.status); lines.push(` ${char} ${file.path}`); } if (status.staged.length > 10) { lines.push(` ... and ${status.staged.length - 10} more`); } lines.push(''); } // Unstaged changes if (status.unstaged.length > 0) { const unstagedAdd = status.unstaged.reduce((s, f) => s + f.additions, 0); const unstagedDel = status.unstaged.reduce((s, f) => s + f.deletions, 0); lines.push(`Modified (${status.unstaged.length} files, +${unstagedAdd}, -${unstagedDel}):`); for (const file of status.unstaged.slice(0, 10)) { const char = formatStatusChar(file.status); lines.push(` ${char} ${file.path}`); } if (status.unstaged.length > 10) { lines.push(` ... and ${status.unstaged.length - 10} more`); } lines.push(''); } // Untracked files if (status.untracked.length > 0) { lines.push(`Untracked (${status.untracked.length} files):`); for (const file of status.untracked.slice(0, 10)) { lines.push(` ? ${file}`); } if (status.untracked.length > 10) { lines.push(` ... and ${status.untracked.length - 10} more`); } lines.push(''); } // Stash if (status.stashCount > 0) { lines.push(`Stash: ${status.stashCount} stashed change(s)`); lines.push(''); } // Clean state if (status.staged.length === 0 && status.unstaged.length === 0 && status.untracked.length === 0 && status.conflicts.length === 0) { lines.push('Working tree clean'); } return lines.join('\n'); } catch (error) { return `Error: ${error instanceof Error ? error.message : 'Unknown error'}`; } }; // ============================================================================ // Tool Definition // ============================================================================ const gitStatusCoreTool = tool({ description: 'Show git repository status including branch, staged/unstaged changes, and sync state with remote.', inputSchema: jsonSchema({ type: 'object', properties: {}, required: [], }), // AUTO - read-only operation, never needs approval needsApproval: () => false, execute: async (args, _options) => { return await executeGitStatus(args); }, }); // ============================================================================ // Formatter // ============================================================================ function GitStatusFormatter({ result }) { const { colors } = useTheme(); // Parse result for display let branch = ''; let ahead = 0; let behind = 0; let stagedCount = 0; let modifiedCount = 0; let untrackedCount = 0; let hasConflicts = false; if (result) { const branchMatch = result.match(/Branch: (.+)/); if (branchMatch) branch = branchMatch[1]; const syncMatch = result.match(/Sync: (.+)/); if (syncMatch) { const aheadMatch = syncMatch[1].match(/(\d+) ahead/); const behindMatch = syncMatch[1].match(/(\d+) behind/); if (aheadMatch) ahead = parseInt(aheadMatch[1], 10); if (behindMatch) behind = parseInt(behindMatch[1], 10); } const stagedMatch = result.match(/Staged \((\d+) files/); if (stagedMatch) stagedCount = parseInt(stagedMatch[1], 10); const modifiedMatch = result.match(/Modified \((\d+) files/); if (modifiedMatch) modifiedCount = parseInt(modifiedMatch[1], 10); const untrackedMatch = result.match(/Untracked \((\d+) files/); if (untrackedMatch) untrackedCount = parseInt(untrackedMatch[1], 10); hasConflicts = result.includes('CONFLICTS'); } return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: colors.tool, children: "\u2692 git_status" }), branch && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Branch: " }), _jsx(Text, { color: colors.primary, children: branch })] })), (ahead > 0 || behind > 0) && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Sync: " }), ahead > 0 && _jsxs(Text, { color: colors.success, children: [ahead, " ahead"] }), ahead > 0 && behind > 0 && _jsx(Text, { color: colors.secondary, children: ", " }), behind > 0 && _jsxs(Text, { color: colors.warning, children: [behind, " behind"] })] })), hasConflicts && (_jsx(Box, { children: _jsx(Text, { color: colors.error, children: "Conflicts detected!" }) })), (stagedCount > 0 || modifiedCount > 0 || untrackedCount > 0) && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Changes: " }), stagedCount > 0 && (_jsxs(Text, { color: colors.success, children: [stagedCount, " staged"] })), stagedCount > 0 && (modifiedCount > 0 || untrackedCount > 0) && (_jsx(Text, { color: colors.secondary, children: ", " })), modifiedCount > 0 && (_jsxs(Text, { color: colors.warning, children: [modifiedCount, " modified"] })), modifiedCount > 0 && untrackedCount > 0 && (_jsx(Text, { color: colors.secondary, children: ", " })), untrackedCount > 0 && (_jsxs(Text, { color: colors.text, children: [untrackedCount, " untracked"] }))] })), stagedCount === 0 && modifiedCount === 0 && untrackedCount === 0 && !hasConflicts && branch && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.success, children: "\u2713 Working tree clean" }) }))] })); } const formatter = (_args, result) => { return _jsx(GitStatusFormatter, { result: result }); }; // ============================================================================ // Export // ============================================================================ export const gitStatusTool = { name: 'git_status', tool: gitStatusCoreTool, formatter, readOnly: true, }; //# sourceMappingURL=git-status.js.map