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.

122 lines · 6.4 KBRaw History
1import { readFile } from "node:fs/promises";
2import { homedir } from "node:os";
3import * as vscode from "vscode";
4import { DEFAULT_SERVER, installHintPath, normalizeServer, readInstallHint, serverInputError, serverLabel } from "./util";
5
6/** Set (globalState) once someone has chosen a server, so signing in stops asking which one. */
7const CHOSEN = "diffinery.serverChosen";
8/** The installer's server this editor already asked about (globalState), so it asks once per address. */
9const HINT_ANSWERED = "diffinery.installHintAnswered";
10
11/** The server in diffinery.serverUrl, or undefined when the setting isn't a usable address. */
12export function configuredServer(): string | undefined {
13 try {
14 return normalizeServer(vscode.workspace.getConfiguration("diffinery").get<string>("serverUrl"));
15 } catch {
16 return undefined;
17 }
18}
19
20/** Saves the server in the user settings; diffinery.com clears the setting, so it follows the default. */
21export async function useServer(state: vscode.Memento, server: string): Promise<void> {
22 await vscode.workspace.getConfiguration("diffinery").update("serverUrl", server === DEFAULT_SERVER ? undefined : server, vscode.ConfigurationTarget.Global);
23 await state.update(CHOSEN, true);
24}
25
26/**
27 * Asks for a self-hosted Diffinery's address, and checks that a Diffinery answers there before it's used, so a typo
28 * ends here rather than at an error page in the browser. Undefined when cancelled.
29 */
30export async function askForServer(value?: string): Promise<string | undefined> {
31 const raw = await vscode.window.showInputBox({
32 title: "Use Another Diffinery Server",
33 prompt: "The address of your Diffinery, as in your browser's address bar. The extension signs in, clones and pushes there.",
34 placeHolder: "git.example.com",
35 value,
36 ignoreFocusOut: true,
37 validateInput: serverInputError,
38 });
39 if (raw === undefined) return undefined;
40 const server = normalizeServer(raw);
41 const found = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: `Looking for Diffinery at ${serverLabel(server)}…` }, () => isDiffinery(server));
42 if (found !== true) {
43 const go = await vscode.window.showWarningMessage(`${server} doesn't look like a Diffinery server`, { modal: true, detail: `${found} Check the address, and use it anyway only if you're sure.` }, "Use Anyway");
44 if (!go) return undefined;
45 }
46 return server;
47}
48
49/** True when the server answers like Diffinery (every instance serves this extension at /-/vscode/), else what went wrong. */
50async function isDiffinery(server: string): Promise<true | string> {
51 try {
52 const res = await fetch(`${server}/-/vscode/version.json`, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(15_000) });
53 if (!res.ok) return `${server}/-/vscode/version.json answered ${res.status}.`;
54 const body = (await res.json()) as { version?: unknown };
55 return typeof body.version === "string" ? true : `${server}/-/vscode/version.json isn't what Diffinery serves.`;
56 } catch (e) {
57 return `It couldn't be reached (${e instanceof Error ? e.message : String(e)}).`;
58 }
59}
60
61/**
62 * The first sign-in asks which Diffinery: diffinery.com, or a self-hosted one by its address, so nobody has to find
63 * the setting first. Once someone has chosen, or set diffinery.serverUrl themselves, it doesn't ask again
64 * ("Diffinery: Use Another Server…" changes it later).
65 */
66export async function chooseServer(state: vscode.Memento): Promise<void> {
67 if (state.get<boolean>(CHOSEN) || vscode.workspace.getConfiguration("diffinery").inspect<string>("serverUrl")?.globalValue !== undefined) return;
68 const items: Array<vscode.QuickPickItem & { server?: string }> = [
69 { label: `$(diffinery-logo) Sign in to ${serverLabel(DEFAULT_SERVER)}`, server: DEFAULT_SERVER },
70 { label: "$(server) Use another Diffinery server…", detail: "A self-hosted Diffinery on your own domain" },
71 ];
72 const pick = await vscode.window.showQuickPick(items, { title: "Sign in to Diffinery", placeHolder: "Which Diffinery do you use?", ignoreFocusOut: true });
73 if (!pick) throw new vscode.CancellationError();
74 const server = pick.server ?? (await askForServer());
75 if (!server) throw new vscode.CancellationError();
76 await useServer(state, server);
77}
78
79/** "Diffinery: Use Another Server…": switches the server. Sign-ins on other servers stay, for switching back. */
80export async function switchServer(state: vscode.Memento, signedInAs: () => Promise<string | undefined>): Promise<void> {
81 const server = await askForServer(configuredServer());
82 if (!server) return;
83 await useServer(state, server);
84 await announce(server, await signedInAs());
85}
86
87async function announce(server: string, login: string | undefined): Promise<void> {
88 if (login) {
89 void vscode.window.showInformationMessage(`Diffinery now uses ${serverLabel(server)}, where you're signed in as ${login}.`);
90 return;
91 }
92 const pick = await vscode.window.showInformationMessage(`Diffinery now uses ${serverLabel(server)}. Sign in to clone and push there.`, "Sign In");
93 if (pick) void vscode.commands.executeCommand("diffinery.signIn");
94}
95
96/**
97 * The installer that a self-hosted Diffinery serves leaves its address in a file (util.ts installHintPath), since an
98 * editor's command line can't change a setting. The file only suggests: the person is asked, once per address.
99 */
100export async function offerInstallerServer(state: vscode.Memento, signedInAs: () => Promise<string | undefined>, log: vscode.LogOutputChannel): Promise<void> {
101 let server: string | null;
102 try {
103 server = readInstallHint(await readFile(installHintPath(process.env, homedir()), "utf8"));
104 } catch {
105 return; // no file: installed from diffinery.com, or by hand
106 }
107 if (!server || state.get<string>(HINT_ANSWERED) === server) return;
108 const current = configuredServer();
109 if (server === current) {
110 await state.update(HINT_ANSWERED, server);
111 return;
112 }
113 log.info(`installed from ${server}; asking whether to use it`);
114 const use = `Use ${serverLabel(server)}`;
115 const keep = current ? `Keep ${serverLabel(current)}` : "Not Now";
116 const pick = await vscode.window.showInformationMessage(`The Diffinery extension was installed from ${server}. Sign in, clone and push there?`, use, keep);
117 if (!pick) return; // closed without answering: ask again next time
118 await state.update(HINT_ANSWERED, server);
119 if (pick !== use) return;
120 await useServer(state, server);
121 await announce(server, await signedInAs());
122}