| 1 |
// New Repository… (spec §8.5): a repository on a hub under you or one of |
| 2 |
// your organisations, optionally with an assistant's starting files (#347), |
| 3 |
// then cloned into a folder you choose. |
| 4 |
|
| 5 |
import * as vscode from "vscode"; |
| 6 |
import type * as api from "./hubapi"; |
| 7 |
import { HubAuth } from "./auth"; |
| 8 |
|
| 9 |
const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/; |
| 10 |
|
| 11 |
export async function newRepository(auth: HubAuth): Promise<void> { |
| 12 |
const hubs = auth.all().map((s) => s.base); |
| 13 |
const base = hubs.length === 1 ? hubs[0] : await vscode.window.showQuickPick(hubs, { placeHolder: "Create the repository on" }); |
| 14 |
if (!base) { |
| 15 |
if (!hubs.length) { |
| 16 |
void vscode.window.showWarningMessage("OxygenHub: sign in to a hub first."); |
| 17 |
} |
| 18 |
return; |
| 19 |
} |
| 20 |
const client = auth.clientFor(base); |
| 21 |
const [me, orgs] = await Promise.all([client.me(), client.orgs().catch(() => [] as api.Organisation[])]); |
| 22 |
const owner = orgs.length |
| 23 |
? (await vscode.window.showQuickPick([{ label: me.username, description: "you" }, ...orgs.map((o) => ({ label: o.name, description: o.display_name }))], { placeHolder: "Owner" }))?.label |
| 24 |
: me.username; |
| 25 |
if (!owner) { |
| 26 |
return; |
| 27 |
} |
| 28 |
const name = await vscode.window.showInputBox({ prompt: `Repository name under ${owner}`, validateInput: (v) => (NAME.test(v) ? undefined : "Letters, digits, '.', '_' and '-', starting with a letter or digit") }); |
| 29 |
if (!name) { |
| 30 |
return; |
| 31 |
} |
| 32 |
const description = (await vscode.window.showInputBox({ prompt: "What is it for? (optional; an assistant can draft its starting files from this)" })) ?? ""; |
| 33 |
const vis = await vscode.window.showQuickPick( |
| 34 |
[ |
| 35 |
{ label: "$(lock) Private", value: "private" as const }, |
| 36 |
{ label: "$(organization) Internal", description: "everyone signed in to the hub", value: "internal" as const }, |
| 37 |
{ label: "$(globe) Public", value: "public" as const }, |
| 38 |
], |
| 39 |
{ placeHolder: "Visibility" }, |
| 40 |
); |
| 41 |
if (!vis) { |
| 42 |
return; |
| 43 |
} |
| 44 |
try { |
| 45 |
const repo = await client.createRepository({ owner, name, description, visibility: vis.value }); |
| 46 |
if (description.trim()) { |
| 47 |
await offerStarter(client, repo, description.trim()); |
| 48 |
} |
| 49 |
const pick = await vscode.window.showInformationMessage(`OxygenHub: created ${owner}/${name}.`, "Clone", "Open on Hub"); |
| 50 |
if (pick === "Open on Hub") { |
| 51 |
void vscode.env.openExternal(vscode.Uri.parse(`${base}/${owner}/${name}`)); |
| 52 |
} else if (pick === "Clone") { |
| 53 |
const parent = await vscode.window.showOpenDialog({ canSelectFolders: true, canSelectFiles: false, openLabel: "Clone Here" }); |
| 54 |
if (parent?.[0]) { |
| 55 |
const t = vscode.window.createTerminal({ name: "OxygenHub", cwd: parent[0].fsPath }); |
| 56 |
t.show(); |
| 57 |
t.sendText(`oxy clone ${repo.clone_url} ${name}`); |
| 58 |
} |
| 59 |
} |
| 60 |
} catch (e) { |
| 61 |
void vscode.window.showErrorMessage(`OxygenHub: ${(e as Error).message}`); |
| 62 |
} |
| 63 |
} |
| 64 |
|
| 65 |
/** When the owner has an assistant, offer its starting files, and wait for them. */ |
| 66 |
async function offerStarter(client: ReturnType<HubAuth["clientFor"]>, repo: api.Repository, description: string): Promise<void> { |
| 67 |
const { assistants } = await client.starterAssistants(repo.owner).catch(() => ({ assistants: [] as api.StarterAssistant[] })); |
| 68 |
if (!assistants.length) { |
| 69 |
return; |
| 70 |
} |
| 71 |
const pick = await vscode.window.showQuickPick( |
| 72 |
[{ label: "$(close) No, start empty" }, ...assistants.map((a) => ({ label: `$(sparkle) Ask ${a.name}`, description: `${a.model}, from ${a.from}`, assistant: a.name }))], |
| 73 |
{ placeHolder: "Ask an assistant for its starting files (a README, build files, a first test)?" }, |
| 74 |
); |
| 75 |
if (!pick || !("assistant" in pick)) { |
| 76 |
return; |
| 77 |
} |
| 78 |
await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: `OxygenHub: ${pick.assistant} is drafting the starting files…`, cancellable: true }, async (_p, token) => { |
| 79 |
let s = await client.startStarter(repo.owner, repo.name, { description, assistant: pick.assistant }); |
| 80 |
// A model may take a few minutes; the hub commits the files when it is done. |
| 81 |
for (let i = 0; i < 120 && s.status === "running" && !token.isCancellationRequested; i++) { |
| 82 |
await new Promise((r) => setTimeout(r, 3000)); |
| 83 |
s = await client.starterStatus(repo.owner, repo.name); |
| 84 |
} |
| 85 |
if (s.status === "done") { |
| 86 |
void vscode.window.showInformationMessage(`OxygenHub: ${s.assistant} committed ${s.files?.length ?? 0} starting file(s).`); |
| 87 |
} else if (s.status === "failed") { |
| 88 |
void vscode.window.showWarningMessage(`OxygenHub: the starter failed: ${s.error}`); |
| 89 |
} else if (s.status === "running") { |
| 90 |
void vscode.window.showInformationMessage("OxygenHub: the starter is still being drafted; the hub commits it when done."); |
| 91 |
} |
| 92 |
}); |
| 93 |
} |