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

252 lines 11.7 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; /** * Git PR Tool * * Pull request management using gh CLI: create, view, list. */ import { Box, Text } from 'ink'; import React from 'react'; import { useTheme } from '../../hooks/useTheme.js'; import { jsonSchema, tool } from '../../types/core.js'; import { execGh, getCommits, getCurrentBranch, getDefaultBranch, getUpstreamBranch, } from './utils.js'; // ============================================================================ // Preview // ============================================================================ async function getCreatePreview(base) { const branch = await getCurrentBranch(); const commits = await getCommits({ range: `${base}..HEAD` }); return { commits, branch }; } // ============================================================================ // Execution // ============================================================================ const executeGitPr = async (args) => { try { // CREATE if (args.create) { const base = args.create.base || (await getDefaultBranch()); const branch = await getCurrentBranch(); // Check if upstream is set const upstream = await getUpstreamBranch(); if (!upstream) { return 'Error: No upstream branch set. Push your branch first with git_push (setUpstream: true).'; } // Build gh command const ghArgs = [ 'pr', 'create', '--title', args.create.title, '--base', base, ]; if (args.create.body) { ghArgs.push('--body', args.create.body); } else { ghArgs.push('--body', ''); } if (args.create.draft) { ghArgs.push('--draft'); } const output = await execGh(ghArgs); const lines = []; lines.push('Pull request created successfully!'); lines.push(''); lines.push(`Title: ${args.create.title}`); lines.push(`Base: ${base} ← ${branch}`); // Extract PR URL from output const urlMatch = output.match(/https:\/\/github\.com\/[^\s]+/); if (urlMatch) { lines.push(''); lines.push(`URL: ${urlMatch[0]}`); } if (args.create.draft) { lines.push(''); lines.push('(Created as draft)'); } return lines.join('\n'); } // VIEW if (args.view !== undefined) { const output = await execGh([ 'pr', 'view', args.view.toString(), '--json', 'number,title,state,author,url,body,headRefName,baseRefName,additions,deletions,changedFiles', ]); const pr = JSON.parse(output); const lines = []; lines.push(`PR #${pr.number}: ${pr.title}`); lines.push(''); lines.push(`State: ${pr.state}`); lines.push(`Author: ${pr.author?.login || 'unknown'}`); lines.push(`Branch: ${pr.baseRefName} ← ${pr.headRefName}`); lines.push(`Changes: ${pr.changedFiles} files (+${pr.additions}, -${pr.deletions})`); lines.push(''); lines.push(`URL: ${pr.url}`); if (pr.body) { lines.push(''); lines.push('Description:'); lines.push(pr.body.substring(0, 500)); if (pr.body.length > 500) { lines.push('... (truncated)'); } } return lines.join('\n'); } // LIST if (args.list || (!args.create && args.view === undefined)) { const state = args.list?.state || 'open'; const limit = args.list?.limit || 10; const ghArgs = [ 'pr', 'list', '--state', state, '--limit', limit.toString(), '--json', 'number,title,state,author,headRefName,updatedAt', ]; if (args.list?.author) { ghArgs.push('--author', args.list.author); } const output = await execGh(ghArgs); const prs = JSON.parse(output); if (prs.length === 0) { return `No ${state} pull requests found.`; } const lines = []; lines.push(`Pull requests (${state}, ${prs.length} found):`); lines.push(''); for (const pr of prs) { lines.push(`#${pr.number} ${pr.title}`); lines.push(` ${pr.headRefName} by ${pr.author?.login || 'unknown'} (${pr.state})`); } return lines.join('\n'); } return 'Error: No valid action specified. Use create, view, or list.'; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; // Check for common gh errors if (message.includes('gh auth login')) { return 'Error: Not authenticated with GitHub. Run "gh auth login" first.'; } if (message.includes('not a git repository')) { return 'Error: Not in a git repository.'; } if (message.includes('no upstream')) { return 'Error: No upstream branch. Push your branch first.'; } return `Error: ${message}`; } }; // ============================================================================ // Tool Definition // ============================================================================ const gitPrCoreTool = tool({ description: 'Manage GitHub pull requests. Create new PR, view PR details, or list PRs. Requires gh CLI to be installed and authenticated.', inputSchema: jsonSchema({ type: 'object', properties: { create: { type: 'object', description: 'Create a new pull request', properties: { title: { type: 'string', description: 'PR title (required)', }, body: { type: 'string', description: 'PR description/body', }, base: { type: 'string', description: 'Base branch (default: main/master)', }, draft: { type: 'boolean', description: 'Create as draft PR', }, }, required: ['title'], }, view: { type: 'number', description: 'View details of a specific PR by number', }, list: { type: 'object', description: 'List pull requests', properties: { state: { type: 'string', enum: ['open', 'closed', 'merged', 'all'], description: 'Filter by state (default: open)', }, author: { type: 'string', description: 'Filter by author (use "@me" for self)', }, limit: { type: 'number', description: 'Max results (default: 10)', }, }, }, }, required: [], }), // Approval varies by action needsApproval: (args) => { // ALWAYS_APPROVE for create (user should see title/body) if (args.create) { return true; } // AUTO for view and list return false; }, execute: async (args, _options) => { return await executeGitPr(args); }, }); // ============================================================================ // Formatter // ============================================================================ function GitPrFormatter({ args, result, }) { const { colors } = useTheme(); const [preview, setPreview] = React.useState(null); // Determine action const action = args.create ? 'create' : args.view !== undefined ? 'view' : 'list'; // Load preview for create before execution React.useEffect(() => { if (!result && args.create) { (async () => { const base = args.create?.base || (await getDefaultBranch()); const { commits, branch } = await getCreatePreview(base); setPreview({ commits, branch, base }); })().catch(() => { }); } }, [args, result]); return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: colors.tool, children: "\u2692 git_pr" }), _jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Action: " }), _jsx(Text, { color: colors.text, children: action })] }), action === 'create' && args.create && (_jsxs(_Fragment, { children: [preview && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Branch: " }), _jsx(Text, { color: colors.text, children: preview.branch })] })), preview && preview.commits.length > 0 && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Commits: " }), _jsx(Text, { color: colors.text, children: preview.commits.length })] })), args.create.draft && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Draft: " }), _jsx(Text, { color: colors.warning, children: "yes" })] })), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: colors.secondary, children: "Title:" }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: colors.primary, children: args.create.title }) })] }), args.create.body && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: colors.secondary, children: "Body:" }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: _jsx(Text, { color: colors.text, children: args.create.body }) })] }))] })), action === 'view' && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "PR: " }), _jsxs(Text, { color: colors.primary, children: ["#", args.view] })] })), action === 'list' && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "State: " }), _jsx(Text, { color: colors.text, children: args.list?.state || 'open' }), args.list?.author && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.secondary, children: " by " }), _jsx(Text, { color: colors.text, children: args.list.author })] }))] })), result?.includes('created successfully') && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.success, children: "\u2713 PR created successfully" }) })), result?.includes('Error:') && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.error, children: ["\u2717 ", result] }) }))] })); } const formatter = (args, result) => { return _jsx(GitPrFormatter, { args: args, result: result }); }; // ============================================================================ // Export // ============================================================================ export const gitPrTool = { name: 'git_pr', tool: gitPrCoreTool, formatter, }; //# sourceMappingURL=git-pr.js.map