const vscode = require('vscode'); const path = require('path'); const fs = require('fs'); const gh = require('./gh'); const ai = require('./ai'); let panel = null; function activate(context) { // Init AI ai.initAI(process.env.ANTHROPIC_API_KEY); // Main command: open dashboard context.subscriptions.push( vscode.commands.registerCommand('gh-hub.open', () => openDashboard(context)) ); // Quick push current workspace context.subscriptions.push( vscode.commands.registerCommand('gh-hub.pushCurrent', () => quickPush()) ); // Publish file as README context.subscriptions.push( vscode.commands.registerCommand('gh-hub.publishReadme', () => publishReadme()) ); } // ═══════════════════════════════════════════════════════════════ // Dashboard Panel // ═══════════════════════════════════════════════════════════════ function openDashboard(context) { if (panel) { panel.reveal(); return; } panel = vscode.window.createWebviewPanel( 'ghHub', 'GH-Hub', vscode.ViewColumn.One, { enableScripts: true, retainContextWhenHidden: true } ); const htmlPath = path.join(context.extensionPath, 'media', 'dashboard.html'); panel.webview.html = fs.readFileSync(htmlPath, 'utf-8'); // Handle messages from webview panel.webview.onDidReceiveMessage(async (msg) => { try { const result = await handleMessage(msg); panel.webview.postMessage({ id: msg.id, command: msg.command, data: result }); } catch (e) { panel.webview.postMessage({ id: msg.id, command: msg.command, error: e.message }); } }, null, context.subscriptions); panel.onDidDispose(() => { panel = null; }); // Start notification polling startPolling(); } async function handleMessage(msg) { const { command, args } = msg; switch (command) { case 'status': const status = await gh.checkStatus(); return { ...status, aiReady: ai.isReady() }; case 'listRepos': return await gh.listRepos(args); case 'createRepo': return await gh.createRepo(args.name, args); case 'listIssues': return await gh.listIssues(args.repo, args); case 'viewIssue': return await gh.viewIssue(args.repo, args.number); case 'commentIssue': await gh.commentIssue(args.repo, args.number, args.body); return { ok: true }; case 'closeIssue': await gh.closeIssue(args.repo, args.number); return { ok: true }; case 'reopenIssue': await gh.reopenIssue(args.repo, args.number); return { ok: true }; case 'listPRs': return await gh.listPRs(args.repo, args); case 'viewPR': return await gh.viewPR(args.repo, args.number); case 'commentPR': await gh.commentPR(args.repo, args.number, args.body); return { ok: true }; case 'mergePR': await gh.mergePR(args.repo, args.number, args.method); return { ok: true }; case 'closePR': await gh.closePR(args.repo, args.number); return { ok: true }; case 'listNotifications': return await gh.listNotifications(); case 'markRead': await gh.markNotificationRead(args.id); return { ok: true }; case 'markAllRead': await gh.markAllRead(); return { ok: true }; case 'gitStatus': return await gh.gitStatus(args.dir); case 'gitPush': return await gh.gitPush(args.dir, args); case 'aiDraft': return await ai.draftReply(args); case 'updateFile': await gh.updateRepoFile(args.repo, args.path || 'README.md', args.content, args.message || 'Update file'); return { ok: true }; case 'uploadImage': { // args: { repo, filePath (in repo), base64, message } const imgArgs = ['api', '-X', 'PUT', `repos/${args.repo}/contents/${args.filePath}`, '-f', `message=${args.message || 'Upload image'}`, '-f', `content=${args.base64}`]; // Check if file exists to get SHA try { const { execFile } = require('child_process'); const { promisify } = require('util'); const run = promisify(execFile); const existing = JSON.parse((await run('gh', ['api', `repos/${args.repo}/contents/${args.filePath}`])).stdout); if (existing?.sha) imgArgs.push('-f', `sha=${existing.sha}`); } catch {} const { execFile: ef } = require('child_process'); const { promisify: p } = require('util'); const r = p(ef); await r('gh', imgArgs); return { ok: true, url: `https://raw.githubusercontent.com/${args.repo}/main/${args.filePath}` }; } case 'pickImage': { const imgFiles = await vscode.window.showOpenDialog({ canSelectFiles: true, canSelectFolders: false, filters: { 'Images': ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'] } }); if (imgFiles?.[0]) { const imgBuf = fs.readFileSync(imgFiles[0].fsPath); const base64 = imgBuf.toString('base64'); const name = path.basename(imgFiles[0].fsPath); return { name, base64, size: imgBuf.length }; } return null; } case 'fetchReadme': { try { const { execFile: ef2 } = require('child_process'); const { promisify: p2 } = require('util'); const run2 = p2(ef2); const res = JSON.parse((await run2('gh', ['api', `repos/${args.repo}/contents/README.md`])).stdout); return { content: Buffer.from(res.content, 'base64').toString('utf-8'), sha: res.sha }; } catch { return { content: '', sha: '' }; } } case 'pickFolder': const uris = await vscode.window.showOpenDialog({ canSelectFolders: true, canSelectFiles: false }); return uris?.[0]?.fsPath || null; case 'pickFile': const files = await vscode.window.showOpenDialog({ canSelectFiles: true, canSelectFolders: false }); if (files?.[0]) { const content = fs.readFileSync(files[0].fsPath, 'utf-8'); return { path: files[0].fsPath, name: path.basename(files[0].fsPath), content }; } return null; case 'getWorkspaceDir': return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || null; case 'quickSetup': { const dir = args.dir; // git init if needed try { await gh.gitStatus(dir); } catch { await gh.gitInit(dir); } // Create repo if needed if (args.createRepo) { try { await gh.createRepo(args.repoName, { description: args.description, isPrivate: args.isPrivate }); } catch (e) { if (!e.message.includes('already exists')) throw e; } try { await gh.gitAddRemote(dir, 'origin', `https://github.com/${args.repoName}.git`); } catch (e) { if (!e.message.includes('already exists')) throw e; } } // Add, commit, push await gh.gitAddAll(dir); try { await gh.gitCommit(dir, args.commitMessage || 'Initial commit'); } catch (e) { if (!e.message.includes('nothing to commit')) throw e; } const pushResult = await gh.gitPush(dir, { remote: 'origin' }); return { ok: true, message: pushResult }; } default: throw new Error(`Unknown command: ${command}`); } } // ─── Notification Polling ──────────────────────────────────── let pollInterval = null; function startPolling() { if (pollInterval) return; pollInterval = setInterval(async () => { if (!panel) { clearInterval(pollInterval); pollInterval = null; return; } try { const notifs = await gh.listNotifications(); panel.webview.postMessage({ command: 'notifUpdate', data: notifs }); } catch {} }, 60000); // First poll immediately (async () => { try { const notifs = await gh.listNotifications(); if (panel) panel.webview.postMessage({ command: 'notifUpdate', data: notifs }); } catch {} })(); } // ─── Quick Push ────────────────────────────────────────────── async function quickPush() { const dir = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; if (!dir) { vscode.window.showErrorMessage('No workspace folder open'); return; } try { const status = await gh.gitStatus(dir); const result = await gh.gitPush(dir, {}); vscode.window.showInformationMessage(`Pushed ${status.branch} to origin`); } catch (e) { vscode.window.showErrorMessage(`Push failed: ${e.message}`); } } // ─── Publish README ────────────────────────────────────────── async function publishReadme() { const editor = vscode.window.activeTextEditor; if (!editor) { vscode.window.showErrorMessage('No file open'); return; } const content = editor.document.getText(); const fileName = path.basename(editor.document.fileName); // Ask which repo const status = await gh.checkStatus(); if (!status.ok) { vscode.window.showErrorMessage('Not authenticated. Run: gh auth login'); return; } const repos = await gh.listRepos({ limit: 30 }); if (!repos?.length) { vscode.window.showErrorMessage('No repos found'); return; } const pick = await vscode.window.showQuickPick( repos.map(r => ({ label: `${r.owner.login}/${r.name}`, description: r.description || '' })), { placeHolder: 'Select target repo for README' } ); if (!pick) return; const msg = await vscode.window.showInputBox({ prompt: 'Commit message', value: `Update README from ${fileName}` }); if (!msg) return; try { await gh.updateRepoFile(pick.label, 'README.md', content, msg); vscode.window.showInformationMessage(`README updated on ${pick.label}`); } catch (e) { vscode.window.showErrorMessage(`Failed: ${e.message}`); } } function deactivate() { if (pollInterval) clearInterval(pollInterval); } module.exports = { activate, deactivate };