| 1 | import { readFile } from "node:fs/promises"; |
| 2 | import { homedir } from "node:os"; |
| 3 | import * as vscode from "vscode"; |
| 4 | import { 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. */ |
| 7 | const CHOSEN = "diffinery.serverChosen"; |
| 8 | /** The installer's server this editor already asked about (globalState), so it asks once per address. */ |
| 9 | const HINT_ANSWERED = "diffinery.installHintAnswered"; |
| 10 | |
| 11 | /** The server in diffinery.serverUrl, or undefined when the setting isn't a usable address. */ |
| 12 | export 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. */ |
| 21 | export 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 | */ |
| 30 | export 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. */ |
| 50 | async 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 | */ |
| 66 | export 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. */ |
| 80 | export 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 | |
| 87 | async 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 | */ |
| 100 | export 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 | } |