gh.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. /**
  2. * gh.js — GitHub CLI wrapper for VS Code extension
  3. * All GitHub operations via `gh` CLI for proper auth.
  4. */
  5. const { execFile } = require('child_process');
  6. const { promisify } = require('util');
  7. const run = promisify(execFile);
  8. async function gh(args, opts = {}) {
  9. try {
  10. const { stdout } = await run('gh', args, { timeout: 30000, maxBuffer: 10 * 1024 * 1024, ...opts });
  11. return stdout.trim();
  12. } catch (e) {
  13. throw new Error(e.stderr?.trim() || e.message);
  14. }
  15. }
  16. async function ghJSON(args, opts) {
  17. const raw = await gh(args, opts);
  18. if (!raw) return null;
  19. return JSON.parse(raw);
  20. }
  21. // ─── Auth ────────────────────────────────────────────────────
  22. async function checkStatus() {
  23. try {
  24. const out = await gh(['auth', 'status']);
  25. const user = out.match(/Logged in to github\.com account (\S+)/)?.[1]
  26. || out.match(/account (\S+)/)?.[1] || 'unknown';
  27. return { ok: true, user, raw: out };
  28. } catch (e) {
  29. return { ok: false, error: e.message };
  30. }
  31. }
  32. // ─── Repos ───────────────────────────────────────────────────
  33. async function listRepos({ limit = 30 } = {}) {
  34. return ghJSON([
  35. 'repo', 'list', '--json',
  36. 'name,owner,description,url,stargazerCount,forkCount,primaryLanguage,updatedAt,isPrivate',
  37. '--limit', String(limit)
  38. ]);
  39. }
  40. async function createRepo(name, { description = '', isPrivate = false } = {}) {
  41. const args = ['repo', 'create', name];
  42. if (description) args.push('--description', description);
  43. args.push(isPrivate ? '--private' : '--public');
  44. return gh(args);
  45. }
  46. // ─── Issues ──────────────────────────────────────────────────
  47. async function listIssues(nwo, { state = 'open', limit = 30 } = {}) {
  48. return ghJSON([
  49. 'issue', 'list', '-R', nwo, '--json',
  50. 'number,title,author,labels,state,createdAt,updatedAt,comments,url,body',
  51. '--limit', String(limit), '--state', state
  52. ]);
  53. }
  54. async function viewIssue(nwo, number) {
  55. const issue = await ghJSON([
  56. 'issue', 'view', String(number), '-R', nwo, '--json',
  57. 'number,title,author,labels,state,createdAt,updatedAt,body,url,comments'
  58. ]);
  59. try {
  60. const comments = await ghJSON([
  61. 'api', `repos/${nwo}/issues/${number}/comments`,
  62. '--jq', '[.[] | {id:.id, author:.user.login, body:.body, createdAt:.created_at}]'
  63. ]);
  64. issue.commentList = comments || [];
  65. } catch { issue.commentList = []; }
  66. return issue;
  67. }
  68. async function commentIssue(nwo, number, body) {
  69. return gh(['issue', 'comment', String(number), '-R', nwo, '--body', body]);
  70. }
  71. async function closeIssue(nwo, number) {
  72. return gh(['issue', 'close', String(number), '-R', nwo]);
  73. }
  74. async function reopenIssue(nwo, number) {
  75. return gh(['issue', 'reopen', String(number), '-R', nwo]);
  76. }
  77. // ─── Pull Requests ───────────────────────────────────────────
  78. async function listPRs(nwo, { state = 'open', limit = 30 } = {}) {
  79. return ghJSON([
  80. 'pr', 'list', '-R', nwo, '--json',
  81. 'number,title,author,state,createdAt,updatedAt,url,body,headRefName,baseRefName,isDraft,comments',
  82. '--limit', String(limit), '--state', state
  83. ]);
  84. }
  85. async function viewPR(nwo, number) {
  86. const pr = await ghJSON([
  87. 'pr', 'view', String(number), '-R', nwo, '--json',
  88. 'number,title,author,state,createdAt,updatedAt,body,url,headRefName,baseRefName,isDraft,comments,additions,deletions,changedFiles'
  89. ]);
  90. try {
  91. const comments = await ghJSON([
  92. 'api', `repos/${nwo}/issues/${number}/comments`,
  93. '--jq', '[.[] | {id:.id, author:.user.login, body:.body, createdAt:.created_at}]'
  94. ]);
  95. pr.commentList = comments || [];
  96. } catch { pr.commentList = []; }
  97. return pr;
  98. }
  99. async function commentPR(nwo, number, body) {
  100. return gh(['pr', 'comment', String(number), '-R', nwo, '--body', body]);
  101. }
  102. async function mergePR(nwo, number, method = 'merge') {
  103. return gh(['pr', 'merge', String(number), '-R', nwo, `--${method}`, '--yes']);
  104. }
  105. async function closePR(nwo, number) {
  106. return gh(['pr', 'close', String(number), '-R', nwo]);
  107. }
  108. // ─── Notifications ───────────────────────────────────────────
  109. async function listNotifications() {
  110. try {
  111. const raw = await gh(['api', '/notifications']);
  112. if (!raw) return [];
  113. return JSON.parse(raw).map(n => ({
  114. id: n.id, reason: n.reason, unread: n.unread,
  115. updatedAt: n.updated_at, title: n.subject?.title,
  116. type: n.subject?.type, url: n.subject?.url,
  117. repo: n.repository?.full_name
  118. }));
  119. } catch { return []; }
  120. }
  121. async function markNotificationRead(threadId) {
  122. return gh(['api', '-X', 'PATCH', `/notifications/threads/${threadId}`]);
  123. }
  124. async function markAllRead() {
  125. return gh(['api', '-X', 'PUT', '/notifications', '-f', `last_read_at=${new Date().toISOString()}`]);
  126. }
  127. // ─── Git Operations ──────────────────────────────────────────
  128. async function gitStatus(dir) {
  129. const branch = (await run('git', ['branch', '--show-current'], { cwd: dir })).stdout.trim();
  130. const status = (await run('git', ['status', '--short'], { cwd: dir })).stdout.trim();
  131. const remotes = (await run('git', ['remote', '-v'], { cwd: dir })).stdout.trim();
  132. let ahead = 0, behind = 0;
  133. try {
  134. const ab = (await run('git', ['rev-list', '--left-right', '--count', `origin/${branch}...HEAD`], { cwd: dir })).stdout.trim();
  135. const p = ab.split(/\s+/);
  136. behind = parseInt(p[0]) || 0;
  137. ahead = parseInt(p[1]) || 0;
  138. } catch {}
  139. return { branch, status, remotes, ahead, behind };
  140. }
  141. async function gitPush(dir, { remote = 'origin', branch = '' } = {}) {
  142. if (!branch) branch = (await run('git', ['branch', '--show-current'], { cwd: dir })).stdout.trim();
  143. const result = await run('git', ['push', '-u', remote, branch], { cwd: dir, timeout: 60000 });
  144. return result.stdout || result.stderr || 'Push completed.';
  145. }
  146. async function gitInit(dir) {
  147. return (await run('git', ['init'], { cwd: dir })).stdout.trim();
  148. }
  149. async function gitAddRemote(dir, name, url) {
  150. return (await run('git', ['remote', 'add', name, url], { cwd: dir })).stdout.trim();
  151. }
  152. async function gitAddAll(dir) {
  153. return (await run('git', ['add', '-A'], { cwd: dir })).stdout.trim();
  154. }
  155. async function gitCommit(dir, message) {
  156. return (await run('git', ['commit', '-m', message], { cwd: dir })).stdout.trim();
  157. }
  158. // ─── README / File Push ──────────────────────────────────────
  159. async function updateRepoFile(nwo, filePath, content, message) {
  160. // Get current file SHA if exists
  161. let sha = '';
  162. try {
  163. const existing = await ghJSON(['api', `repos/${nwo}/contents/${filePath}`]);
  164. sha = existing?.sha || '';
  165. } catch {}
  166. const base64 = Buffer.from(content).toString('base64');
  167. const args = ['api', '-X', 'PUT', `repos/${nwo}/contents/${filePath}`,
  168. '-f', `message=${message}`,
  169. '-f', `content=${base64}`];
  170. if (sha) args.push('-f', `sha=${sha}`);
  171. return gh(args);
  172. }
  173. module.exports = {
  174. checkStatus, listRepos, createRepo,
  175. listIssues, viewIssue, commentIssue, closeIssue, reopenIssue,
  176. listPRs, viewPR, commentPR, mergePR, closePR,
  177. listNotifications, markNotificationRead, markAllRead,
  178. gitStatus, gitPush, gitInit, gitAddRemote, gitAddAll, gitCommit,
  179. updateRepoFile
  180. };