/** * gh.js — GitHub CLI wrapper for VS Code extension * All GitHub operations via `gh` CLI for proper auth. */ const { execFile } = require('child_process'); const { promisify } = require('util'); const run = promisify(execFile); async function gh(args, opts = {}) { try { const { stdout } = await run('gh', args, { timeout: 30000, maxBuffer: 10 * 1024 * 1024, ...opts }); return stdout.trim(); } catch (e) { throw new Error(e.stderr?.trim() || e.message); } } async function ghJSON(args, opts) { const raw = await gh(args, opts); if (!raw) return null; return JSON.parse(raw); } // ─── Auth ──────────────────────────────────────────────────── async function checkStatus() { try { const out = await gh(['auth', 'status']); const user = out.match(/Logged in to github\.com account (\S+)/)?.[1] || out.match(/account (\S+)/)?.[1] || 'unknown'; return { ok: true, user, raw: out }; } catch (e) { return { ok: false, error: e.message }; } } // ─── Repos ─────────────────────────────────────────────────── async function listRepos({ limit = 30 } = {}) { return ghJSON([ 'repo', 'list', '--json', 'name,owner,description,url,stargazerCount,forkCount,primaryLanguage,updatedAt,isPrivate', '--limit', String(limit) ]); } async function createRepo(name, { description = '', isPrivate = false } = {}) { const args = ['repo', 'create', name]; if (description) args.push('--description', description); args.push(isPrivate ? '--private' : '--public'); return gh(args); } // ─── Issues ────────────────────────────────────────────────── async function listIssues(nwo, { state = 'open', limit = 30 } = {}) { return ghJSON([ 'issue', 'list', '-R', nwo, '--json', 'number,title,author,labels,state,createdAt,updatedAt,comments,url,body', '--limit', String(limit), '--state', state ]); } async function viewIssue(nwo, number) { const issue = await ghJSON([ 'issue', 'view', String(number), '-R', nwo, '--json', 'number,title,author,labels,state,createdAt,updatedAt,body,url,comments' ]); try { const comments = await ghJSON([ 'api', `repos/${nwo}/issues/${number}/comments`, '--jq', '[.[] | {id:.id, author:.user.login, body:.body, createdAt:.created_at}]' ]); issue.commentList = comments || []; } catch { issue.commentList = []; } return issue; } async function commentIssue(nwo, number, body) { return gh(['issue', 'comment', String(number), '-R', nwo, '--body', body]); } async function closeIssue(nwo, number) { return gh(['issue', 'close', String(number), '-R', nwo]); } async function reopenIssue(nwo, number) { return gh(['issue', 'reopen', String(number), '-R', nwo]); } // ─── Pull Requests ─────────────────────────────────────────── async function listPRs(nwo, { state = 'open', limit = 30 } = {}) { return ghJSON([ 'pr', 'list', '-R', nwo, '--json', 'number,title,author,state,createdAt,updatedAt,url,body,headRefName,baseRefName,isDraft,comments', '--limit', String(limit), '--state', state ]); } async function viewPR(nwo, number) { const pr = await ghJSON([ 'pr', 'view', String(number), '-R', nwo, '--json', 'number,title,author,state,createdAt,updatedAt,body,url,headRefName,baseRefName,isDraft,comments,additions,deletions,changedFiles' ]); try { const comments = await ghJSON([ 'api', `repos/${nwo}/issues/${number}/comments`, '--jq', '[.[] | {id:.id, author:.user.login, body:.body, createdAt:.created_at}]' ]); pr.commentList = comments || []; } catch { pr.commentList = []; } return pr; } async function commentPR(nwo, number, body) { return gh(['pr', 'comment', String(number), '-R', nwo, '--body', body]); } async function mergePR(nwo, number, method = 'merge') { return gh(['pr', 'merge', String(number), '-R', nwo, `--${method}`, '--yes']); } async function closePR(nwo, number) { return gh(['pr', 'close', String(number), '-R', nwo]); } // ─── Notifications ─────────────────────────────────────────── async function listNotifications() { try { const raw = await gh(['api', '/notifications']); if (!raw) return []; return JSON.parse(raw).map(n => ({ id: n.id, reason: n.reason, unread: n.unread, updatedAt: n.updated_at, title: n.subject?.title, type: n.subject?.type, url: n.subject?.url, repo: n.repository?.full_name })); } catch { return []; } } async function markNotificationRead(threadId) { return gh(['api', '-X', 'PATCH', `/notifications/threads/${threadId}`]); } async function markAllRead() { return gh(['api', '-X', 'PUT', '/notifications', '-f', `last_read_at=${new Date().toISOString()}`]); } // ─── Git Operations ────────────────────────────────────────── async function gitStatus(dir) { const branch = (await run('git', ['branch', '--show-current'], { cwd: dir })).stdout.trim(); const status = (await run('git', ['status', '--short'], { cwd: dir })).stdout.trim(); const remotes = (await run('git', ['remote', '-v'], { cwd: dir })).stdout.trim(); let ahead = 0, behind = 0; try { const ab = (await run('git', ['rev-list', '--left-right', '--count', `origin/${branch}...HEAD`], { cwd: dir })).stdout.trim(); const p = ab.split(/\s+/); behind = parseInt(p[0]) || 0; ahead = parseInt(p[1]) || 0; } catch {} return { branch, status, remotes, ahead, behind }; } async function gitPush(dir, { remote = 'origin', branch = '' } = {}) { if (!branch) branch = (await run('git', ['branch', '--show-current'], { cwd: dir })).stdout.trim(); const result = await run('git', ['push', '-u', remote, branch], { cwd: dir, timeout: 60000 }); return result.stdout || result.stderr || 'Push completed.'; } async function gitInit(dir) { return (await run('git', ['init'], { cwd: dir })).stdout.trim(); } async function gitAddRemote(dir, name, url) { return (await run('git', ['remote', 'add', name, url], { cwd: dir })).stdout.trim(); } async function gitAddAll(dir) { return (await run('git', ['add', '-A'], { cwd: dir })).stdout.trim(); } async function gitCommit(dir, message) { return (await run('git', ['commit', '-m', message], { cwd: dir })).stdout.trim(); } // ─── README / File Push ────────────────────────────────────── async function updateRepoFile(nwo, filePath, content, message) { // Get current file SHA if exists let sha = ''; try { const existing = await ghJSON(['api', `repos/${nwo}/contents/${filePath}`]); sha = existing?.sha || ''; } catch {} const base64 = Buffer.from(content).toString('base64'); const args = ['api', '-X', 'PUT', `repos/${nwo}/contents/${filePath}`, '-f', `message=${message}`, '-f', `content=${base64}`]; if (sha) args.push('-f', `sha=${sha}`); return gh(args); } module.exports = { checkStatus, listRepos, createRepo, listIssues, viewIssue, commentIssue, closeIssue, reopenIssue, listPRs, viewPR, commentPR, mergePR, closePR, listNotifications, markNotificationRead, markAllRead, gitStatus, gitPush, gitInit, gitAddRemote, gitAddAll, gitCommit, updateRepoFile };