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.

4 files changed+82−15
Files (4)
modifiedapps/diffinery/test/e2e/editor-smoke.mjs+42−1View file
@@ -32,6 +32,7 @@
3232 const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
3333 const ADA_SESSION = "ada-editor-session-0123456789abcdef";
3434 const ADA_TOKEN = `dfy_${randomBytes(24).toString("hex")}`;
35+const CHARLES_TOKEN = `dfy_${randomBytes(24).toString("hex")}`;
3536 let devLog = "";
3637
3738 const freePort = () =>
@@ -68,6 +69,9 @@
6869 `INSERT INTO accounts (id, login, kind, name, email, status, created_at) VALUES ('ada1', 'ada', 'user', 'Ada', 'ada@example.com', 'active', '${at}')`,
6970 `INSERT INTO sessions (id, account_id, method, created_at, last_seen_at, expires_at) VALUES ('${hash(ADA_SESSION)}', 'ada1', 'passkey', '${at}', '${at}', '${later}')`,
7071 `INSERT INTO tokens (id, account_id, name, token_hash, prefix, scopes, expires_at, created_at) VALUES ('t1', 'ada1', 'e2e', '${hash(ADA_TOKEN)}', '${ADA_TOKEN.slice(0, 10)}', '["admin"]', '${later}', '${at}')`,
72+ // Charles signs in with an access token he pastes, in step 0.
73+ `INSERT INTO accounts (id, login, kind, name, email, status, created_at) VALUES ('cb1', 'charles', 'user', 'Charles', 'charles@example.com', 'active', '${at}')`,
74+ `INSERT INTO tokens (id, account_id, name, token_hash, prefix, scopes, expires_at, created_at) VALUES ('t2', 'cb1', 'e2e', '${hash(CHARLES_TOKEN)}', '${CHARLES_TOKEN.slice(0, 10)}', '["write"]', '${later}', '${at}')`,
7175 ].join("; "),
7276 );
7377 const dev = spawn(
@@ -89,6 +93,7 @@
8993 const api = (method, path, body) => fetch(`${base}/api/v3${path}`, { method, headers: { authorization: `Bearer ${ADA_TOKEN}`, "content-type": "application/json" }, body: body && JSON.stringify(body) }).then(async (r) => (r.ok ? r.json() : Promise.reject(new Error(`${method} ${path}: ${r.status} ${await r.text()}`))));
9094 await api("POST", "/user/repos", { name: "hello-editor", private: true, description: "The analytical engine, in TypeScript" });
9195 await api("POST", "/user/repos", { name: "notes", private: false, description: "Notes on the engine, and drafts of the next paper" });
96+await fetch(`${base}/api/v3/user/repos`, { method: "POST", headers: { authorization: `Bearer ${CHARLES_TOKEN}`, "content-type": "application/json" }, body: JSON.stringify({ name: "engine-drawings", private: true }) }).then((r) => assert.ok(r.ok, `charles's repository: ${r.status}`));
9297 const seed = join(work, "seed");
9398 mkdirSync(seed);
9499 const git = (cwd, ...args) => execFileSync("git", args, { cwd, stdio: "pipe", env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } }).toString();
@@ -165,11 +170,39 @@
165170 await win.keyboard.press("Control+Shift+P");
166171 await win.waitForSelector(".quick-input-widget:not([style*='display: none']) .quick-input-box input");
167172 await win.keyboard.type(cmd);
168- await sleep(500);
173+ // The palette filters as it's typed: run the command only once it's the one highlighted.
174+ await until(`"${cmd}" highlighted in the Command Palette`, async () => (await win.textContent(".quick-input-list .monaco-list-row.focused").catch(() => ""))?.includes(cmd), 10_000);
169175 await win.keyboard.press("Enter");
170176 };
171177 const quickItems = async () => (await win.$$eval(".quick-input-list .monaco-list-row", (rows) => rows.map((r) => r.textContent?.trim() ?? ""))).filter(Boolean);
172178
179+// 0. Signing in with a pasted token, in a fresh editor, goes through the editor's own sign-in: the editor asks once,
180+// up front, and the clone picker then lists the account's repositories without asking again. Then sign out again.
181+await palette("Diffinery: Sign In with an Access Token");
182+const allowToken = await until("the sign-in prompt", async () => win.$(".monaco-dialog-box .monaco-button:has-text('Allow')"), 30_000);
183+await shot(win, "0-allow-token");
184+await allowToken.click();
185+await win.waitForSelector(".quick-input-widget:not([style*='display: none']) .quick-input-box input");
186+await sleep(300);
187+await win.fill(".quick-input-widget:not([style*='display: none']) .quick-input-box input", CHARLES_TOKEN);
188+await win.keyboard.press("Enter");
189+await until("the signed-in notice", async () => (await win.textContent(".notifications-toasts").catch(() => ""))?.includes("Signed in to Diffinery as charles"), 30_000);
190+await palette("Git: Clone");
191+await until("Clone from Diffinery in the picker", async () => (await quickItems()).some((t) => t.includes("Clone from Diffinery")), 30_000);
192+await win.keyboard.type("Clone from Diffinery");
193+await sleep(300);
194+await win.keyboard.press("Enter");
195+await until("charles's repositories", async () => {
196+ if (await win.$(".monaco-dialog-box")) throw new Error("the editor asked again to allow the sign-in");
197+ return (await quickItems()).some((t) => t.startsWith("charles/engine-drawings"));
198+}, 30_000);
199+await win.keyboard.press("Escape");
200+console.log("token sign-in: charles's repositories listed, no second prompt");
201+await sleep(500);
202+await palette("Diffinery: Sign Out");
203+await until("the signed-out notice", async () => (await win.textContent(".notifications-toasts").catch(() => ""))?.includes("Signed out of Diffinery"), 20_000);
204+await win.keyboard.press("Escape");
205+
173206 // 1. The clone picker lists Diffinery as a remote source.
174207 await palette("Git: Clone");
175208 await until("Clone from Diffinery in the picker", async () => (await quickItems()).some((t) => t.includes("Clone from Diffinery")), 30_000);
@@ -218,6 +251,13 @@
218251 assert.match(await page.textContent("body"), /You're signed in to Diffinery/);
219252 if (SHOTS) await page.screenshot({ path: join(SHOTS, "3b-signed-in.png") });
220253 console.log("browser: signed in page shown");
254+// The repository page offers the editors' own clone links.
255+await page.goto(`${base}/ada/hello-editor`);
256+const cloneUrl = encodeURIComponent(`${base}/ada/hello-editor.git`);
257+for (const scheme of ["vscode", "cursor", "windsurf"]) assert.ok(await page.$(`a[href="${scheme}://vscode.git/clone?url=${cloneUrl}"]`), `Open in ${scheme} link`);
258+assert.ok(await page.$(`a[href^="jetbrains://idea/checkout/git?"]`), "Open in IntelliJ IDEA link");
259+if (SHOTS) await page.screenshot({ path: join(SHOTS, "3c-repo-page.png"), fullPage: true });
260+console.log("repository page: Open in VS Code, Cursor, Windsurf and IntelliJ IDEA links");
221261 await browser.close();
222262
223263 // 3. Signed in: the picker lists ada's repositories; search narrows it.
@@ -282,6 +322,7 @@
282322 assert.ok(grants.length === 1 && grants[0].revoked_at, "the connection is revoked");
283323 console.log("signed out: credential removed, connection revoked");
284324
325+
285326 await app.close();
286327 cleanup();
287328 if (process.env.KEEP !== "1") rmSync(work, { recursive: true, force: true });
modifiedapps/vscode/CHANGELOG.md+5−0View file
@@ -1,5 +1,10 @@
11 # Changelog
22
3+## 0.1.1
4+
5+- "Sign In with an Access Token" goes through the editor's own sign-in, so the first clone afterwards no longer asks to allow the extension to sign in.
6+- Signing in again replaces the previous sign-in and disconnects it on Diffinery.
7+
38 ## 0.1.0
49
510 - "Clone from Diffinery" in the clone picker of VS Code, Cursor, Windsurf, VSCodium and other editors built on VS Code, listing your repositories and your organizations', with search.
modifiedapps/vscode/package.json+1−1View file
@@ -2,7 +2,7 @@
22 "name": "@diffinery/vscode",
33 "displayName": "Diffinery",
44 "description": "Clone, publish and push Diffinery repositories from VS Code, Cursor, Windsurf and other editors built on VS Code.",
5- "version": "0.1.0",
5+ "version": "0.1.1",
66 "publisher": "codician",
77 "private": true,
88 "homepage": "https://diffinery.com/docs/repositories/clone-in-your-editor",
modifiedapps/vscode/src/auth.ts+34−13View file
@@ -184,12 +184,18 @@
184184 }
185185
186186 async createSession(): Promise<vscode.AuthenticationSession> {
187- const s = await this.signInWithBrowser();
187+ const s = this.withToken ? await this.askForToken() : await this.signInWithBrowser();
188+ // Signing in again replaces the old sign-in, and ends its connection on the server.
189+ const old = await this.load();
190+ if (old) await this.removeSession(old.id);
188191 await this.save(s);
189192 void vscode.window.showInformationMessage(`Signed in to Diffinery as ${s.login}.`);
190193 return toSession(s);
191194 }
192195
196+ /** Set while "Sign In with an Access Token" runs, so createSession asks for a token instead of opening the browser. */
197+ private withToken = false;
198+
193199 async removeSession(id: string): Promise<void> {
194200 const s = await this.load();
195201 if (!s || s.id !== id) return;
@@ -283,20 +289,35 @@
283289 }
284290 }
285291
286- /** Signs in with an access token someone pastes (for an agent account, a self-hosted server, or a browser-based editor). */
292+ /**
293+ * Signs in with an access token someone pastes (for an agent account, a self-hosted server, or a browser-based
294+ * editor). It goes through the editor's own sign-in, like the browser sign-in, so the editor records that this
295+ * extension may use the account and doesn't ask again on the first clone.
296+ */
287297 async signInWithToken(): Promise<void> {
298+ const signedIn = await this.load();
299+ this.withToken = true;
300+ try {
301+ await vscode.authentication.getSession(AUTH_ID, SCOPES, signedIn ? { forceNewSession: { detail: `Replace ${signedIn.login}'s sign-in with an access token.` } } : { createIfNone: true });
302+ } finally {
303+ this.withToken = false;
304+ }
305+ }
306+
307+ private async askForToken(): Promise<Stored> {
288308 const server = this.server();
289- const token = await vscode.window.showInputBox({
290- title: "Sign in to Diffinery with an access token",
291- prompt: `Paste a token from ${server}/settings/tokens (write access lets you push). It stays in the editor's secret storage.`,
292- password: true,
293- ignoreFocusOut: true,
294- validateInput: (v) => (/^\s*\S+\s*$/.test(v) ? null : "Paste the whole token, without spaces."),
295- });
296- if (!token) return;
297- const user = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: "Checking the token with Diffinery…" }, () => new Api(server, token.trim(), this.version).user());
298- await this.save({ id: randomUUID(), server, login: user.login, accountId: String(user.id), kind: "token", accessToken: token.trim() });
299- void vscode.window.showInformationMessage(`Signed in to Diffinery as ${user.login}.`);
309+ const token = (
310+ await vscode.window.showInputBox({
311+ title: "Sign in to Diffinery with an access token",
312+ prompt: `Paste a token from ${server}/settings/tokens (write access lets you push). It stays in the editor's secret storage.`,
313+ password: true,
314+ ignoreFocusOut: true,
315+ validateInput: (v) => (/^\s*\S+\s*$/.test(v) ? null : "Paste the whole token, without spaces."),
316+ })
317+ )?.trim();
318+ if (!token) throw new vscode.CancellationError();
319+ const user = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: "Checking the token with Diffinery…" }, () => new Api(server, token, this.version).user());
320+ return { id: randomUUID(), server, login: user.login, accountId: String(user.id), kind: "token", accessToken: token };
300321 }
301322
302323 async signOut(): Promise<void> {