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