extension.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. const vscode = require('vscode');
  2. const path = require('path');
  3. const fs = require('fs');
  4. const gh = require('./gh');
  5. const ai = require('./ai');
  6. let panel = null;
  7. function activate(context) {
  8. // Init AI
  9. ai.initAI(process.env.ANTHROPIC_API_KEY);
  10. // Main command: open dashboard
  11. context.subscriptions.push(
  12. vscode.commands.registerCommand('gh-hub.open', () => openDashboard(context))
  13. );
  14. // Quick push current workspace
  15. context.subscriptions.push(
  16. vscode.commands.registerCommand('gh-hub.pushCurrent', () => quickPush())
  17. );
  18. // Publish file as README
  19. context.subscriptions.push(
  20. vscode.commands.registerCommand('gh-hub.publishReadme', () => publishReadme())
  21. );
  22. }
  23. // ═══════════════════════════════════════════════════════════════
  24. // Dashboard Panel
  25. // ═══════════════════════════════════════════════════════════════
  26. function openDashboard(context) {
  27. if (panel) {
  28. panel.reveal();
  29. return;
  30. }
  31. panel = vscode.window.createWebviewPanel(
  32. 'ghHub', 'GH-Hub', vscode.ViewColumn.One,
  33. { enableScripts: true, retainContextWhenHidden: true }
  34. );
  35. const htmlPath = path.join(context.extensionPath, 'media', 'dashboard.html');
  36. panel.webview.html = fs.readFileSync(htmlPath, 'utf-8');
  37. // Handle messages from webview
  38. panel.webview.onDidReceiveMessage(async (msg) => {
  39. try {
  40. const result = await handleMessage(msg);
  41. panel.webview.postMessage({ id: msg.id, command: msg.command, data: result });
  42. } catch (e) {
  43. panel.webview.postMessage({ id: msg.id, command: msg.command, error: e.message });
  44. }
  45. }, null, context.subscriptions);
  46. panel.onDidDispose(() => { panel = null; });
  47. // Start notification polling
  48. startPolling();
  49. }
  50. async function handleMessage(msg) {
  51. const { command, args } = msg;
  52. switch (command) {
  53. case 'status':
  54. const status = await gh.checkStatus();
  55. return { ...status, aiReady: ai.isReady() };
  56. case 'listRepos':
  57. return await gh.listRepos(args);
  58. case 'createRepo':
  59. return await gh.createRepo(args.name, args);
  60. case 'listIssues':
  61. return await gh.listIssues(args.repo, args);
  62. case 'viewIssue':
  63. return await gh.viewIssue(args.repo, args.number);
  64. case 'commentIssue':
  65. await gh.commentIssue(args.repo, args.number, args.body);
  66. return { ok: true };
  67. case 'closeIssue':
  68. await gh.closeIssue(args.repo, args.number);
  69. return { ok: true };
  70. case 'reopenIssue':
  71. await gh.reopenIssue(args.repo, args.number);
  72. return { ok: true };
  73. case 'listPRs':
  74. return await gh.listPRs(args.repo, args);
  75. case 'viewPR':
  76. return await gh.viewPR(args.repo, args.number);
  77. case 'commentPR':
  78. await gh.commentPR(args.repo, args.number, args.body);
  79. return { ok: true };
  80. case 'mergePR':
  81. await gh.mergePR(args.repo, args.number, args.method);
  82. return { ok: true };
  83. case 'closePR':
  84. await gh.closePR(args.repo, args.number);
  85. return { ok: true };
  86. case 'listNotifications':
  87. return await gh.listNotifications();
  88. case 'markRead':
  89. await gh.markNotificationRead(args.id);
  90. return { ok: true };
  91. case 'markAllRead':
  92. await gh.markAllRead();
  93. return { ok: true };
  94. case 'gitStatus':
  95. return await gh.gitStatus(args.dir);
  96. case 'gitPush':
  97. return await gh.gitPush(args.dir, args);
  98. case 'aiDraft':
  99. return await ai.draftReply(args);
  100. case 'updateFile':
  101. await gh.updateRepoFile(args.repo, args.path || 'README.md', args.content, args.message || 'Update file');
  102. return { ok: true };
  103. case 'uploadImage': {
  104. // args: { repo, filePath (in repo), base64, message }
  105. const imgArgs = ['api', '-X', 'PUT', `repos/${args.repo}/contents/${args.filePath}`,
  106. '-f', `message=${args.message || 'Upload image'}`,
  107. '-f', `content=${args.base64}`];
  108. // Check if file exists to get SHA
  109. try {
  110. const { execFile } = require('child_process');
  111. const { promisify } = require('util');
  112. const run = promisify(execFile);
  113. const existing = JSON.parse((await run('gh', ['api', `repos/${args.repo}/contents/${args.filePath}`])).stdout);
  114. if (existing?.sha) imgArgs.push('-f', `sha=${existing.sha}`);
  115. } catch {}
  116. const { execFile: ef } = require('child_process');
  117. const { promisify: p } = require('util');
  118. const r = p(ef);
  119. await r('gh', imgArgs);
  120. return { ok: true, url: `https://raw.githubusercontent.com/${args.repo}/main/${args.filePath}` };
  121. }
  122. case 'pickImage': {
  123. const imgFiles = await vscode.window.showOpenDialog({
  124. canSelectFiles: true, canSelectFolders: false,
  125. filters: { 'Images': ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'] }
  126. });
  127. if (imgFiles?.[0]) {
  128. const imgBuf = fs.readFileSync(imgFiles[0].fsPath);
  129. const base64 = imgBuf.toString('base64');
  130. const name = path.basename(imgFiles[0].fsPath);
  131. return { name, base64, size: imgBuf.length };
  132. }
  133. return null;
  134. }
  135. case 'fetchReadme': {
  136. try {
  137. const { execFile: ef2 } = require('child_process');
  138. const { promisify: p2 } = require('util');
  139. const run2 = p2(ef2);
  140. const res = JSON.parse((await run2('gh', ['api', `repos/${args.repo}/contents/README.md`])).stdout);
  141. return { content: Buffer.from(res.content, 'base64').toString('utf-8'), sha: res.sha };
  142. } catch { return { content: '', sha: '' }; }
  143. }
  144. case 'pickFolder':
  145. const uris = await vscode.window.showOpenDialog({ canSelectFolders: true, canSelectFiles: false });
  146. return uris?.[0]?.fsPath || null;
  147. case 'pickFile':
  148. const files = await vscode.window.showOpenDialog({ canSelectFiles: true, canSelectFolders: false });
  149. if (files?.[0]) {
  150. const content = fs.readFileSync(files[0].fsPath, 'utf-8');
  151. return { path: files[0].fsPath, name: path.basename(files[0].fsPath), content };
  152. }
  153. return null;
  154. case 'getWorkspaceDir':
  155. return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || null;
  156. case 'quickSetup': {
  157. const dir = args.dir;
  158. // git init if needed
  159. try { await gh.gitStatus(dir); }
  160. catch {
  161. await gh.gitInit(dir);
  162. }
  163. // Create repo if needed
  164. if (args.createRepo) {
  165. try { await gh.createRepo(args.repoName, { description: args.description, isPrivate: args.isPrivate }); }
  166. catch (e) { if (!e.message.includes('already exists')) throw e; }
  167. try { await gh.gitAddRemote(dir, 'origin', `https://github.com/${args.repoName}.git`); }
  168. catch (e) { if (!e.message.includes('already exists')) throw e; }
  169. }
  170. // Add, commit, push
  171. await gh.gitAddAll(dir);
  172. try { await gh.gitCommit(dir, args.commitMessage || 'Initial commit'); }
  173. catch (e) { if (!e.message.includes('nothing to commit')) throw e; }
  174. const pushResult = await gh.gitPush(dir, { remote: 'origin' });
  175. return { ok: true, message: pushResult };
  176. }
  177. default:
  178. throw new Error(`Unknown command: ${command}`);
  179. }
  180. }
  181. // ─── Notification Polling ────────────────────────────────────
  182. let pollInterval = null;
  183. function startPolling() {
  184. if (pollInterval) return;
  185. pollInterval = setInterval(async () => {
  186. if (!panel) { clearInterval(pollInterval); pollInterval = null; return; }
  187. try {
  188. const notifs = await gh.listNotifications();
  189. panel.webview.postMessage({ command: 'notifUpdate', data: notifs });
  190. } catch {}
  191. }, 60000);
  192. // First poll immediately
  193. (async () => {
  194. try {
  195. const notifs = await gh.listNotifications();
  196. if (panel) panel.webview.postMessage({ command: 'notifUpdate', data: notifs });
  197. } catch {}
  198. })();
  199. }
  200. // ─── Quick Push ──────────────────────────────────────────────
  201. async function quickPush() {
  202. const dir = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
  203. if (!dir) { vscode.window.showErrorMessage('No workspace folder open'); return; }
  204. try {
  205. const status = await gh.gitStatus(dir);
  206. const result = await gh.gitPush(dir, {});
  207. vscode.window.showInformationMessage(`Pushed ${status.branch} to origin`);
  208. } catch (e) {
  209. vscode.window.showErrorMessage(`Push failed: ${e.message}`);
  210. }
  211. }
  212. // ─── Publish README ──────────────────────────────────────────
  213. async function publishReadme() {
  214. const editor = vscode.window.activeTextEditor;
  215. if (!editor) { vscode.window.showErrorMessage('No file open'); return; }
  216. const content = editor.document.getText();
  217. const fileName = path.basename(editor.document.fileName);
  218. // Ask which repo
  219. const status = await gh.checkStatus();
  220. if (!status.ok) { vscode.window.showErrorMessage('Not authenticated. Run: gh auth login'); return; }
  221. const repos = await gh.listRepos({ limit: 30 });
  222. if (!repos?.length) { vscode.window.showErrorMessage('No repos found'); return; }
  223. const pick = await vscode.window.showQuickPick(
  224. repos.map(r => ({ label: `${r.owner.login}/${r.name}`, description: r.description || '' })),
  225. { placeHolder: 'Select target repo for README' }
  226. );
  227. if (!pick) return;
  228. const msg = await vscode.window.showInputBox({
  229. prompt: 'Commit message',
  230. value: `Update README from ${fileName}`
  231. });
  232. if (!msg) return;
  233. try {
  234. await gh.updateRepoFile(pick.label, 'README.md', content, msg);
  235. vscode.window.showInformationMessage(`README updated on ${pick.label}`);
  236. } catch (e) {
  237. vscode.window.showErrorMessage(`Failed: ${e.message}`);
  238. }
  239. }
  240. function deactivate() {
  241. if (pollInterval) clearInterval(pollInterval);
  242. }
  243. module.exports = { activate, deactivate };