150 lines
3.7 KiB
TypeScript
150 lines
3.7 KiB
TypeScript
import { defineCommand } from "citty";
|
|
import { createInterface } from "node:readline/promises";
|
|
import { stdin, stdout } from "node:process";
|
|
import { apiRequest } from "../lib/api";
|
|
import {
|
|
readSession,
|
|
removeSessions,
|
|
writeSession,
|
|
type KuberSession,
|
|
} from "../lib/session";
|
|
|
|
type LoginResponse = KuberSession;
|
|
|
|
async function promptUsername(): Promise<string> {
|
|
const readline = createInterface({ input: stdin, output: stdout });
|
|
try {
|
|
return (await readline.question("Username: ")).trim();
|
|
} finally {
|
|
readline.close();
|
|
}
|
|
}
|
|
|
|
async function promptPassword(): Promise<string> {
|
|
if (!stdin.isTTY || !stdin.setRawMode) {
|
|
throw new Error("Password input requires an interactive terminal");
|
|
}
|
|
|
|
stdout.write("Password: ");
|
|
stdin.setRawMode(true);
|
|
stdin.resume();
|
|
return new Promise((resolve, reject) => {
|
|
let password = "";
|
|
const cleanup = () => {
|
|
stdin.off("data", onData);
|
|
stdin.setRawMode(false);
|
|
stdin.pause();
|
|
stdout.write("\n");
|
|
};
|
|
const onData = (chunk: Buffer) => {
|
|
const value = chunk.toString("utf8");
|
|
if (value === "\u0003") {
|
|
cleanup();
|
|
reject(new Error("Login cancelled"));
|
|
return;
|
|
}
|
|
if (value === "\r" || value === "\n") {
|
|
cleanup();
|
|
resolve(password);
|
|
return;
|
|
}
|
|
if (value === "\u007f" || value === "\b") {
|
|
password = password.slice(0, -1);
|
|
return;
|
|
}
|
|
password += value;
|
|
};
|
|
stdin.on("data", onData);
|
|
});
|
|
}
|
|
|
|
export async function loginUser(
|
|
username: string,
|
|
password: string,
|
|
persistent: boolean,
|
|
): Promise<LoginResponse> {
|
|
const session = await apiRequest<LoginResponse>(
|
|
"/login",
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({ username, password, persistent }),
|
|
},
|
|
{ authenticated: false },
|
|
);
|
|
await writeSession(session, persistent);
|
|
return session;
|
|
}
|
|
|
|
/** Share the CLI's credential prompt with onboarding without exposing passwords. */
|
|
export async function interactiveLogin(): Promise<LoginResponse> {
|
|
if (!stdin.isTTY)
|
|
throw new Error(
|
|
"Login requires an interactive terminal; run kuber login first",
|
|
);
|
|
const username = await promptUsername();
|
|
if (!username) throw new Error("Username is required");
|
|
const session = await loginUser(username, await promptPassword(), false);
|
|
console.log(`Logged in as ${session.user.username}`);
|
|
return session;
|
|
}
|
|
|
|
export const login = defineCommand({
|
|
meta: {
|
|
name: "login",
|
|
description: "Log in to kuber.astrxl.dev",
|
|
},
|
|
args: {
|
|
persist: {
|
|
type: "boolean",
|
|
description: "Keep the login across reboots",
|
|
},
|
|
},
|
|
async run({ args }) {
|
|
const username = String(args._[0] ?? "").trim() || (await promptUsername());
|
|
if (!username) throw new Error("Username is required");
|
|
const session = await loginUser(
|
|
username,
|
|
await promptPassword(),
|
|
Boolean(args.persist),
|
|
);
|
|
console.log(`Logged in as ${session.user.username}`);
|
|
},
|
|
});
|
|
|
|
export const logout = defineCommand({
|
|
meta: {
|
|
name: "logout",
|
|
description: "Log out of kuber.astrxl.dev",
|
|
},
|
|
async run() {
|
|
const session = await readSession();
|
|
if (session) {
|
|
try {
|
|
await apiRequest<void>(
|
|
"/logout",
|
|
{
|
|
method: "POST",
|
|
},
|
|
{ session },
|
|
);
|
|
} finally {
|
|
await removeSessions();
|
|
}
|
|
} else {
|
|
await removeSessions();
|
|
}
|
|
console.log("Logged out");
|
|
},
|
|
});
|
|
|
|
export const whoami = defineCommand({
|
|
meta: {
|
|
name: "whoami",
|
|
description: "Show the current kuber user",
|
|
},
|
|
async run() {
|
|
const result = await apiRequest<KuberSession["user"]>("/me");
|
|
console.log(`${result.username} (${result.roles.join(", ")})`);
|
|
},
|
|
});
|