CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
extension.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| eae38d1 | 1 | /** |
| 2 | * Block G4 — Gluecron VS Code extension. | |
| 3 | * | |
| 4 | * Commands: | |
| 5 | * gluecron.explainFile — call `/api/v1/ai/explain-file` + show in a hover | |
| 6 | * gluecron.openOnWeb — opens the current file on the Gluecron web UI | |
| 7 | * gluecron.searchSemantic — quickPick -> /api/v1/search/semantic | |
| 8 | * gluecron.generateTests — scaffold a failing test via /api/v1/ai/tests | |
| 9 | * | |
| 10 | * We keep this file zero-runtime-dependencies besides `vscode`. Everything | |
| 11 | * else is pure stdlib + fetch. | |
| 12 | */ | |
| 13 | ||
| 14 | import * as vscode from "vscode"; | |
| 15 | import { execSync } from "node:child_process"; | |
| 16 | import { basename, relative } from "node:path"; | |
| 17 | ||
| 18 | function getHost(): string { | |
| 19 | return ( | |
| 20 | vscode.workspace.getConfiguration("gluecron").get<string>("host") || | |
| 21 | "http://localhost:3000" | |
| 22 | ); | |
| 23 | } | |
| 24 | ||
| 25 | function getToken(): string { | |
| 26 | return ( | |
| 27 | vscode.workspace.getConfiguration("gluecron").get<string>("token") || "" | |
| 28 | ); | |
| 29 | } | |
| 30 | ||
| 31 | async function api<T = any>(path: string, init?: RequestInit): Promise<T> { | |
| 32 | const url = getHost().replace(/\/+$/, "") + path; | |
| 33 | const token = getToken(); | |
| 34 | const res = await fetch(url, { | |
| 35 | ...init, | |
| 36 | headers: { | |
| 37 | "content-type": "application/json", | |
| 38 | accept: "application/json", | |
| 39 | ...(token ? { authorization: `Bearer ${token}` } : {}), | |
| 40 | ...(init?.headers || {}), | |
| 41 | }, | |
| 42 | }); | |
| 43 | const text = await res.text(); | |
| 44 | if (!res.ok) throw new Error(`[${res.status}] ${text.slice(0, 200)}`); | |
| 45 | try { | |
| 46 | return JSON.parse(text) as T; | |
| 47 | } catch { | |
| 48 | return text as unknown as T; | |
| 49 | } | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * Inspect the current workspace for a git remote whose URL matches a Gluecron | |
| 54 | * host. Returns `{ owner, repo }` if found. | |
| 55 | */ | |
| 56 | export function detectGlueRepo(cwd: string, host: string): { | |
| 57 | owner: string; | |
| 58 | repo: string; | |
| 59 | } | null { | |
| 60 | try { | |
| 61 | const url = execSync("git config --get remote.origin.url", { | |
| 62 | cwd, | |
| 63 | encoding: "utf8", | |
| 64 | }).trim(); | |
| 65 | if (!url) return null; | |
| 66 | // host-agnostic parsing — look for /:owner/:repo at end | |
| 67 | const cleaned = url | |
| 68 | .replace(/^https?:\/\/[^/]+\//, "") | |
| 69 | .replace(/^git@[^:]+:/, "") | |
| 70 | .replace(/\.git$/, ""); | |
| 71 | const [owner, repo] = cleaned.split("/").filter(Boolean); | |
| 72 | if (!owner || !repo) return null; | |
| 73 | return { owner, repo }; | |
| 74 | } catch { | |
| 75 | return null; | |
| 76 | } | |
| 77 | } | |
| 78 | ||
| 79 | export function buildWebUrl( | |
| 80 | host: string, | |
| 81 | owner: string, | |
| 82 | repo: string, | |
| 83 | relPath: string, | |
| 84 | line?: number | |
| 85 | ): string { | |
| 86 | const base = `${host.replace(/\/+$/, "")}/${owner}/${repo}/blob/main/${relPath}`; | |
| 87 | return line ? `${base}#L${line + 1}` : base; | |
| 88 | } | |
| 89 | ||
| 90 | export function activate(context: vscode.ExtensionContext) { | |
| 91 | const output = vscode.window.createOutputChannel("Gluecron"); | |
| 92 | output.appendLine("Gluecron activated."); | |
| 93 | ||
| 94 | context.subscriptions.push( | |
| 95 | vscode.commands.registerCommand("gluecron.openOnWeb", async () => { | |
| 96 | const editor = vscode.window.activeTextEditor; | |
| 97 | if (!editor) { | |
| 98 | vscode.window.showInformationMessage("No active editor"); | |
| 99 | return; | |
| 100 | } | |
| 101 | const folder = vscode.workspace.getWorkspaceFolder(editor.document.uri); | |
| 102 | if (!folder) { | |
| 103 | vscode.window.showWarningMessage("File is not in a workspace"); | |
| 104 | return; | |
| 105 | } | |
| 106 | const cwd = folder.uri.fsPath; | |
| 107 | const rel = relative(cwd, editor.document.uri.fsPath); | |
| 108 | const repo = detectGlueRepo(cwd, getHost()); | |
| 109 | if (!repo) { | |
| 110 | vscode.window.showWarningMessage("No Gluecron remote detected"); | |
| 111 | return; | |
| 112 | } | |
| 113 | const url = buildWebUrl( | |
| 114 | getHost(), | |
| 115 | repo.owner, | |
| 116 | repo.repo, | |
| 117 | rel, | |
| 118 | editor.selection.active.line | |
| 119 | ); | |
| 120 | vscode.env.openExternal(vscode.Uri.parse(url)); | |
| 121 | }) | |
| 122 | ); | |
| 123 | ||
| 124 | context.subscriptions.push( | |
| 125 | vscode.commands.registerCommand("gluecron.explainFile", async () => { | |
| 126 | const editor = vscode.window.activeTextEditor; | |
| 127 | if (!editor) return; | |
| 128 | const content = editor.document.getText(); | |
| 129 | const path = basename(editor.document.fileName); | |
| 130 | output.show(true); | |
| 131 | output.appendLine(`Explaining ${path}...`); | |
| 132 | try { | |
| 133 | const res = await api<{ explanation?: string }>( | |
| 134 | `/api/copilot/completions`, | |
| 135 | { | |
| 136 | method: "POST", | |
| 137 | body: JSON.stringify({ | |
| 138 | prompt: `Explain this file in 3-5 bullet points:\n\n${content.slice( | |
| 139 | 0, | |
| 140 | 8000 | |
| 141 | )}`, | |
| 142 | max_tokens: 400, | |
| 143 | }), | |
| 144 | } | |
| 145 | ); | |
| 146 | output.appendLine(res.explanation || JSON.stringify(res, null, 2)); | |
| 147 | } catch (err) { | |
| 148 | output.appendLine(`error: ${(err as Error).message}`); | |
| 149 | } | |
| 150 | }) | |
| 151 | ); | |
| 152 | ||
| 153 | context.subscriptions.push( | |
| 154 | vscode.commands.registerCommand("gluecron.searchSemantic", async () => { | |
| 155 | const q = await vscode.window.showInputBox({ | |
| 156 | prompt: "Semantic search across repo", | |
| 157 | placeHolder: "how does auth work?", | |
| 158 | }); | |
| 159 | if (!q) return; | |
| 160 | const folder = vscode.workspace.workspaceFolders?.[0]; | |
| 161 | if (!folder) return; | |
| 162 | const repo = detectGlueRepo(folder.uri.fsPath, getHost()); | |
| 163 | if (!repo) return; | |
| 164 | const res = await api<{ results?: Array<{ path: string; score: number }> }>( | |
| 165 | `/api/graphql`, | |
| 166 | { | |
| 167 | method: "POST", | |
| 168 | body: JSON.stringify({ | |
| 169 | query: `{ search(q:"${q.replace(/"/g, "'")}", limit:10) { name ownerUsername } }`, | |
| 170 | }), | |
| 171 | } | |
| 172 | ); | |
| 173 | output.show(true); | |
| 174 | output.appendLine(JSON.stringify(res, null, 2)); | |
| 175 | }) | |
| 176 | ); | |
| 177 | ||
| 178 | context.subscriptions.push( | |
| 179 | vscode.commands.registerCommand("gluecron.generateTests", async () => { | |
| 180 | const editor = vscode.window.activeTextEditor; | |
| 181 | if (!editor) return; | |
| 182 | const folder = vscode.workspace.getWorkspaceFolder(editor.document.uri); | |
| 183 | if (!folder) return; | |
| 184 | const repo = detectGlueRepo(folder.uri.fsPath, getHost()); | |
| 185 | if (!repo) return; | |
| 186 | const rel = relative(folder.uri.fsPath, editor.document.uri.fsPath); | |
| 187 | try { | |
| 188 | const res = await api( | |
| 189 | `/${repo.owner}/${repo.repo}/ai/tests?format=raw&path=${encodeURIComponent( | |
| 190 | rel | |
| 191 | )}` | |
| 192 | ); | |
| 193 | const doc = await vscode.workspace.openTextDocument({ | |
| 194 | content: typeof res === "string" ? res : JSON.stringify(res, null, 2), | |
| 195 | language: editor.document.languageId, | |
| 196 | }); | |
| 197 | vscode.window.showTextDocument(doc); | |
| 198 | } catch (err) { | |
| 199 | vscode.window.showErrorMessage( | |
| 200 | `Gluecron test generation failed: ${(err as Error).message}` | |
| 201 | ); | |
| 202 | } | |
| 203 | }) | |
| 204 | ); | |
| 205 | } | |
| 206 | ||
| 207 | export function deactivate() { | |
| 208 | // nothing to clean up | |
| 209 | } |