| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- /**
- * 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
- };
|