| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299 |
- 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 };
|