// deno-lint-ignore-file no-explicit-any import { GlobalOptions } from "./types.ts"; import { getRootStore } from "./store.ts"; import { loginInteractive, tryGetLoginInfo } from "./login.ts"; import { colors, Command, DelimiterStream, Input, log, setClient, Table, UserService, WorkspaceService, } from "./deps.ts"; import { requireLogin } from "./context.ts"; export interface Workspace { remote: string; workspaceId: string; name: string; token: string; } function makeWorkspaceStream( readable: ReadableStream ): ReadableStream { return readable .pipeThrough(new DelimiterStream(new TextEncoder().encode("\n"))) .pipeThrough(new TextDecoderStream()) .pipeThrough( new TransformStream({ transform(line, controller) { try { if (line.length <= 2) { return; } const workspace = JSON.parse(line) as Workspace; workspace.remote = new URL(workspace.remote).toString(); // add trailing slash in all cases! controller.enqueue(workspace); } catch { /* ignore */ } }, }) ); } export async function getWorkspaceStream() { const file = await Deno.open((await getRootStore()) + "remotes.ndjson", { write: false, read: true, }); return makeWorkspaceStream(file.readable); } export async function allWorkspaces(): Promise { try { const workspaceStream = await getWorkspaceStream(); const workspaces: Workspace[] = []; for await (const workspace of workspaceStream) { workspaces.push(workspace); } return workspaces; } catch (_) { return []; } } async function getActiveWorkspaceName( opts: GlobalOptions ): Promise { if (opts.workspace) { return opts.workspace; } try { return await Deno.readTextFile((await getRootStore()) + "/activeWorkspace"); } catch { return undefined; } } export async function getActiveWorkspace( opts: GlobalOptions ): Promise { const name = await getActiveWorkspaceName(opts); if (!name) { return undefined; } return await getWorkspaceByName(name); } export async function getWorkspaceByName( workspaceName: string ): Promise { const workspaceStream = await getWorkspaceStream(); for await (const workspace of workspaceStream) { if (workspace.name === workspaceName) { return workspace; } } return undefined; } async function list(opts: GlobalOptions) { const workspaces = await allWorkspaces(); const activeName = await getActiveWorkspaceName(opts); new Table() .header(["name", "remote", "workspace id"]) .padding(2) .border(true) .body( workspaces.map((x) => { const a = [x.name, x.remote, x.workspaceId]; if (x.name === activeName) { return a.map((x) => colors.underline(x)); } else { return a; } }) ) .render(); log.info("Active: " + colors.green.bold(activeName || "none")); } async function switchC(opts: GlobalOptions, workspaceName: string) { if (opts.workspace) { log.info( colors.red.bold( "! Workspace needs to be specified as positional argument, not as option." ) ); return; } const all = await allWorkspaces(); if (all.findIndex((x) => x.name === workspaceName) === -1) { log.info( colors.red.bold( `! This workspace profile ${workspaceName} does not exist locally.` ) ); log.info("available workspace profiles:"); for (const w of all) { log.info(" - " + w.name); } return; } return await Deno.writeTextFile( (await getRootStore()) + "/activeWorkspace", workspaceName ); } export async function add( opts: GlobalOptions & { create: boolean; createWorkspaceName: string | undefined; createUsername: string; }, workspaceName: string | undefined, workspaceId: string | undefined, remote: string | undefined ) { if (opts.workspace) { log.info( colors.red.bold( "! Workspace needs to be specified as positional argument, not as option." ) ); return; } if (!workspaceName) { workspaceName = await Input.prompt("Name this workspace:"); } if (!workspaceId) { workspaceId = await Input.prompt({ message: "Enter the ID of this workspace", default: workspaceName, suggestions: [workspaceName], }); } if (!remote) { // first check whether workspaceId is actually a URL try { const url = new URL(workspaceId); workspaceId = url.username; url.username = ""; remote = url.toString(); } catch { // not a url remote = new URL( await Input.prompt({ message: "Enter the Remote URL", suggestions: ["https://app.windmill.dev/"], default: "https://app.windmill.dev/", }) ).toString(); } } remote = new URL(remote).toString(); // add trailing slash in all cases! let token = await tryGetLoginInfo(opts); if (!token && !Deno.isatty(Deno.stdin.rid)) { log.info("Not a TTY, can't login interactively. Pass the token in --token"); return; } while (!token) { token = await loginInteractive(remote); } setClient( token, remote.endsWith("/") ? remote.substring(0, remote.length - 1) : remote ); let alreadyExists = false; try { alreadyExists = await WorkspaceService.existsWorkspace({ requestBody: { id: workspaceId }, }); } catch (e) { log.info( colors.red.bold("! Credentials or instance is invalid. Aborting.") ); throw e; } if (opts.create) { if (!alreadyExists) { log.info( colors.yellow( `Workspace at id ${workspaceId} on ${remote} does not exist. Creating...` ) ); await WorkspaceService.createWorkspace({ requestBody: { id: workspaceId, name: opts.createWorkspaceName ?? workspaceId, username: opts.createUsername, }, }); } } else if (!alreadyExists) { log.info( colors.red.bold( `! Workspace at id ${workspaceId} on ${remote} does not exist. Re-run with --create to create it. Aborting.` ) ); log.info( "On that instance and with those credentials, the workspaces that you can access are:" ); const workspaces = await WorkspaceService.listWorkspaces(); for (const workspace of workspaces) { log.info(`- ${workspace.id} (name: ${workspace.name})`); } Deno.exit(1); } await addWorkspace( { name: workspaceName, remote: remote, workspaceId: workspaceId, token: token, }, opts ); await Deno.writeTextFile( (await getRootStore()) + "/activeWorkspace", workspaceName ); log.info( colors.green.underline( `Added workspace ${workspaceName} for ${workspaceId} on ${remote}!` ) ); } export async function addWorkspace(workspace: Workspace, opts: any) { workspace.remote = new URL(workspace.remote).toString(); // add trailing slash in all cases! await removeWorkspace(workspace.name, true, opts); const file = await Deno.open((await getRootStore()) + "remotes.ndjson", { append: true, write: true, read: true, create: true, }); await file.write(new TextEncoder().encode(JSON.stringify(workspace) + "\n")); file.close(); } export async function removeWorkspace( name: string, silent: boolean, opts: any ) { const orgWorkspaces = await allWorkspaces(); if (orgWorkspaces.findIndex((x) => x.name === name) === -1) { if (!silent) { log.info( colors.red.bold(`! Workspace profile ${name} does not exist locally`) ); log.info("available workspace profiles:"); await list(opts); } return; } if (silent) { log.info(colors.yellow(`Replacing existing workspace ${name}`)); } await Deno.writeTextFile( (await getRootStore()) + "remotes.ndjson", orgWorkspaces .filter((x) => x.name !== name) .map((x) => JSON.stringify(x)) .join("\n") + "\n" ); if (!silent) { log.info(colors.green.underline(`Succesfully removed workspace ${name}!`)); } } async function remove(_opts: GlobalOptions, name: string) { await removeWorkspace(name, false, _opts); } async function whoami(_opts: GlobalOptions) { await requireLogin(_opts); log.info(await UserService.globalWhoami()); const activeName = await getActiveWorkspaceName(_opts); log.info("Active: " + colors.green.bold(activeName || "none")); } const command = new Command() .description("workspace related commands") .action(list as any) .command("switch") .complete("workspace", async () => (await allWorkspaces()).map((x) => x.name)) .description("Switch to another workspace") .arguments("") .action(switchC as any) .command("add") .description("Add a workspace") .arguments("[workspace_name:string] [workspace_id:string] [remote:string]") .option("-c --create", "Create the workspace if it does not exist") .option( "--create-workspace-name ", "Specify the workspace name. Ignored if --create is not specified or the workspace already exists. Will default to the workspace id." ) .option( "--create-username ", "Specify your own username in the newly created workspace. Ignored if --create is not specified or the workspace already exists.", { default: "admin", } ) .action(add as any) .command("remove") .description("Remove a workspace") .arguments("") .action(remove as any) .command("whoami") .description("Show the currently active user") .action(whoami as any); export default command;