Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
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.

extension.tsBlame209 lines · 1 contributor
eae38d1Claude1/**
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
14import * as vscode from "vscode";
15import { execSync } from "node:child_process";
16import { basename, relative } from "node:path";
17
18function getHost(): string {
19 return (
20 vscode.workspace.getConfiguration("gluecron").get<string>("host") ||
21 "http://localhost:3000"
22 );
23}
24
25function getToken(): string {
26 return (
27 vscode.workspace.getConfiguration("gluecron").get<string>("token") || ""
28 );
29}
30
31async 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 */
56export 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
79export 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
90export 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
207export function deactivate() {
208 // nothing to clean up
209}