Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

Editor sign-in returns through a loopback listener; single clone URL; icon id; real-editor smoke test passes in VS Code

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ3j7radTeNPDXuJ936W3K
Claude authored signed1 parent 8226028commit 3898f268d103d284a1af5ce8391473ed1c3a6053Browse files
6 files changed+66−36
Files (6)
modifiedapps/diffinery/test/e2e/editor-smoke.mjs+38−19View file
@@ -123,16 +123,29 @@
123123 );
124124 // The browser the editor opens: record the URL, as a person's browser would receive it.
125125 const opened = join(work, "opened.txt");
126-const browserScript = join(work, "browser.sh");
126+// Electron opens links with xdg-open on Linux, which hands them to $BROWSER when there's no desktop session.
127+const bin = join(work, "bin");
128+mkdirSync(bin);
129+const browserScript = join(bin, "xdg-open");
127130 writeFileSync(browserScript, `#!/bin/sh\necho "$1" >> ${opened}\n`);
128131 chmodSync(browserScript, 0o755);
129-const env = { ...process.env, HOME: home, BROWSER: browserScript, XDG_CURRENT_DESKTOP: "", DONT_PROMPT_WSL_INSTALL: "1" };
130-const common = ["--user-data-dir", userData, "--extensions-dir", exts, "--no-sandbox", "--disable-gpu"];
132+const env = { ...process.env, HOME: home, PATH: `${bin}:${process.env.PATH}`, BROWSER: browserScript, XDG_CURRENT_DESKTOP: "", DONT_PROMPT_WSL_INSTALL: "1" };
133+// No OS keyring in a test container: the editor keeps secrets with its own weaker encryption instead.
134+const common = ["--user-data-dir", userData, "--extensions-dir", exts, "--no-sandbox", "--disable-gpu", "--password-store=basic"];
131135 execFileSync(CLI, [...common, "--install-extension", VSIX, "--force"], { env, stdio: "pipe", timeout: 120_000 });
132136 console.log("extension installed");
133137
134138 const app = await _electron.launch({ executablePath: EDITOR, args: [...common, "--disable-workspace-trust", "--skip-welcome", "--skip-release-notes", "--new-window"], env, timeout: 120_000 });
135139 const win = await app.firstWindow();
140+// On a failure, a screenshot of the editor as it was.
141+async function fail(e) {
142+ if (SHOTS) await win.screenshot({ path: join(SHOTS, "failed.png") }).catch(() => {});
143+ console.error(e);
144+ cleanup();
145+ process.exit(1);
146+}
147+process.on("unhandledRejection", fail);
148+process.on("uncaughtException", fail);
136149 await win.waitForSelector(".monaco-workbench", { timeout: 120_000 });
137150 console.log("editor window open");
138151 await sleep(4000);
@@ -149,6 +162,14 @@
149162 await palette("Git: Clone");
150163 await until("Clone from Diffinery in the picker", async () => (await quickItems()).some((t) => t.includes("Clone from Diffinery")), 30_000);
151164 console.log("clone picker:", await quickItems());
165+if (process.env.DEBUG_ICON)
166+ console.log(
167+ await win.evaluate(async () => {
168+ const el = document.querySelector(".codicon-diffinery-logo");
169+ const st = el && getComputedStyle(el, "::before");
170+ return { content: st?.content, font: st?.fontFamily, fonts: [...document.fonts].map((f) => `${f.family} ${f.status}`).filter((f) => !/codicon|Segoe/.test(f)) };
171+ }),
172+ );
152173 await shot(win, "1-clone-picker");
153174 await win.keyboard.type("Clone from Diffinery");
154175 await sleep(300);
@@ -162,17 +183,14 @@
162183 const au = new URL(authorize);
163184 assert.equal(au.origin + au.pathname, `${base}/oauth/authorize`);
164185 assert.equal(au.searchParams.get("client_id"), `${base}/oauth/clients/editor.json`);
165-const scheme = au.searchParams.get("redirect_uri").split(":")[0];
186+const redirect = new URL(au.searchParams.get("redirect_uri"));
187+// The extension listens on 127.0.0.1 for the browser's answer (no prompts on the way back to the editor).
188+assert.equal(redirect.origin, `http://127.0.0.1:${redirect.port}`);
189+assert.equal(redirect.pathname, "/callback");
166190 console.log("authorize:", authorize);
167191
168192 const browser = await chromium.launch();
169193 const ctx = await browser.newContext();
170-let callback;
171-// The consent form's answer redirects to the editor's own scheme, which the browser hands to the operating system.
172-ctx.on("response", (r) => {
173- const loc = r.headers().location;
174- if (loc?.startsWith(`${scheme}://`)) callback = loc;
175-});
176194 const page = await ctx.newPage();
177195 await page.goto(`${base}/login`);
178196 await page.evaluate((v) => (document.cookie = `__Host-dfy_session=${v}; path=/; secure; samesite=lax`), ADA_SESSION);
@@ -180,13 +198,14 @@
180198 const consent = ((await page.textContent("main")) ?? "").replace(/\s+/g, " ");
181199 assert.match(consent, /Connect Diffinery extension for VS Code and Cursor to Diffinery/);
182200 assert.match(consent, /published by localhost/);
201+assert.match(consent, /Sends you back to\s*an app on this computer/);
183202 if (SHOTS) await page.screenshot({ path: join(SHOTS, "3-consent.png"), fullPage: true });
184-await page.click('button[value="approve"]', { noWaitAfter: true }).catch(() => {});
185-// The browser hands the editor's own scheme to the operating system, which passes it to the editor.
186-const cb = await until("the redirect back to the editor", async () => callback, 30_000);
187-console.log("callback:", cb.replace(/code=[^&]+/, "code=…"));
203+await page.click('button[value="approve"]');
204+await page.waitForURL(/^http:\/\/127\.0\.0\.1:\d+\/callback/);
205+assert.match(await page.textContent("body"), /You're signed in to Diffinery/);
206+if (SHOTS) await page.screenshot({ path: join(SHOTS, "3b-signed-in.png") });
207+console.log("browser: signed in page shown");
188208 await browser.close();
189-execFileSync(CLI, [...common, "--open-url", cb], { env, stdio: "pipe" });
190209
191210 // 3. Signed in: the picker lists ada's repositories; search narrows it.
192211 await until("ada's repositories in the picker", async () => (await quickItems()).some((t) => t.includes("ada/hello-editor")), 60_000);
@@ -201,11 +220,11 @@
201220 await win.keyboard.press("Enter");
202221
203222 // 4. Choosing where to clone (the editor's own folder dialog), then Git clones with the extension's password.
204-await until("the folder dialog", async () => (await quickItems()).length > 0 || (await win.$(".quick-input-widget .quick-input-action")), 30_000).catch(() => {});
205-await sleep(800);
206-await shot(win, "6-folder");
207-await win.keyboard.press("Enter");
223+const destination = win.getByText("Select as Repository Destination", { exact: true });
224+await destination.waitFor({ timeout: 30_000 });
208225 await sleep(500);
226+await shot(win, "6-folder");
227+await destination.click();
209228 const clonedReadme = join(clones, "hello-editor", "README.md");
210229 await until("the clone on disk", async () => existsSync(clonedReadme), 90_000).catch(async (e) => {
211230 await shot(win, "6b-folder-stuck");
modifiedapps/vscode/package.json+2−2View file
@@ -59,7 +59,7 @@
5959 }
6060 ],
6161 "icons": {
62- "diffinery": {
62+ "diffinery-logo": {
6363 "description": "Diffinery",
6464 "default": {
6565 "fontPath": "./media/diffinery-icons.woff",
@@ -72,7 +72,7 @@
7272 "command": "diffinery.clone",
7373 "title": "Clone Repository",
7474 "category": "Diffinery",
75- "icon": "$(diffinery)"
75+ "icon": "$(diffinery-logo)"
7676 },
7777 {
7878 "command": "diffinery.signIn",
modifiedapps/vscode/scripts/make-icons.py+1−1View file
@@ -5,7 +5,7 @@
55
66 Writes apps/vscode/media/icon.png (the extension's 128 px icon, the site's app icon scaled
77 down) and apps/vscode/media/diffinery-icons.woff (a one-glyph icon font, so the clone picker
8-can show the mark as `$(diffinery)` next to "Clone from Diffinery", the way GitHub's shows its
8+can show the mark as `$(diffinery-logo)` next to "Clone from Diffinery", the way GitHub's shows its
99 logo). The output is committed, so builds need none of these tools.
1010 """
1111 import base64
modifiedapps/vscode/src/auth.ts+22−11View file
@@ -225,17 +225,28 @@
225225 if (vscode.env.uiKind === vscode.UIKind.Web) throw new SignInError("Signing in through the browser needs the desktop editor. In a browser-based editor, run “Diffinery: Sign In with an Access Token”.");
226226 const server = this.server();
227227 const { verifier, challenge, state } = pkce();
228- let redirectUri = editorRedirect(vscode.env.uriScheme);
229- let wait: Promise<string>;
230- let loopback: Loopback | undefined;
231- if (redirectUri) {
228+ // The browser comes back to a one-time listener on this computer (RFC 8252): no prompt to open the editor, and
229+ // no "Allow the extension to open this URI?" dialog. Where that can't work (a browser-based remote), it comes
230+ // back through the editor's own URL scheme instead.
231+ let redirectUri: string | null = null;
232+ let wait: Promise<string> | undefined;
233+ const loopback = await listenOnce(state).catch((e) => {
234+ this.log.warn("couldn't listen on 127.0.0.1 for the sign-in", e);
235+ return undefined;
236+ });
237+ if (loopback) {
238+ const external = new URL((await vscode.env.asExternalUri(vscode.Uri.parse(loopback.uri))).toString(true));
239+ if (external.protocol === "http:" && /^(127\.0\.0\.1|localhost)$/.test(external.hostname)) {
240+ redirectUri = `http://127.0.0.1:${external.port || loopback.port}/callback`;
241+ wait = loopback.result;
242+ } else {
243+ loopback.close();
244+ }
245+ }
246+ if (!redirectUri) {
247+ redirectUri = editorRedirect(vscode.env.uriScheme);
248+ if (!redirectUri) throw new SignInError("This editor can't receive Diffinery's sign-in. Run “Diffinery: Sign In with an Access Token” instead.");
232249 wait = new Promise((resolve) => this.callbacks.set(state, resolve));
233- } else {
234- // An editor the server has no scheme for: a one-time listener on this computer instead (RFC 8252).
235- loopback = await listenOnce(state);
236- const external = await vscode.env.asExternalUri(vscode.Uri.parse(loopback.uri));
237- redirectUri = loopback.uri.replace(/:\d+\//, `:${new URL(external.toString(true)).port || loopback.port}/`);
238- wait = loopback.result;
239250 }
240251 try {
241252 const url = authorizeUrl(server, redirectUri, challenge, state);
@@ -246,7 +257,7 @@
246257 async (progress, cancel) => {
247258 progress.report({ message: opened ? "Approve the connection in your browser…" : `Open ${url} in your browser and approve the connection…` });
248259 return Promise.race([
249- wait,
260+ wait!,
250261 new Promise<never>((_, reject) => cancel.onCancellationRequested(() => reject(new vscode.CancellationError()))),
251262 new Promise<never>((_, reject) => setTimeout(() => reject(new SignInError("Signing in to Diffinery timed out. Try again.")), SIGN_IN_TIMEOUT_MS)),
252263 ]);
modifiedapps/vscode/src/git.ts+1−1View file
@@ -62,7 +62,7 @@
6262 /** "Publish to Diffinery" when publishing a repository that has no remote yet. */
6363 export class Publisher implements RemoteSourcePublisher {
6464 readonly name = "Diffinery";
65- readonly icon = "diffinery";
65+ readonly icon = "diffinery-logo";
6666
6767 constructor(private readonly auth: DiffineryAuth) {}
6868
modifiedapps/vscode/src/remoteSources.ts+2−2View file
@@ -12,7 +12,7 @@
1212 */
1313 export class RemoteSources implements RemoteSourceProvider {
1414 readonly name = "Diffinery";
15- readonly icon = "diffinery";
15+ readonly icon = "diffinery-logo";
1616 readonly supportsQuery = true;
1717 readonly placeholder = "Repository name or owner/name (type to search)";
1818
@@ -68,5 +68,5 @@
6868
6969 function source(r: RepoJson): RemoteSource {
7070 const tags = [r.private ? "Private" : "Public", r.archived ? "archived" : "", r.fork ? "fork" : ""].filter(Boolean);
71- return { name: r.full_name, icon: "diffinery", description: tags.join(" · "), detail: r.description || undefined, url: [r.clone_url] };
71+ return { name: r.full_name, icon: "diffinery-logo", description: tags.join(" · "), detail: r.description || undefined, url: r.clone_url };
7272 }