feat: release 2.7.0-rc3
This commit is contained in:
+111
-52
@@ -1,6 +1,6 @@
|
||||
import { defineCommand } from "citty";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { stdin, stdout } from "node:process";
|
||||
import Enquirer from "enquirer";
|
||||
import { stdin } from "node:process";
|
||||
import { apiRequest } from "../lib/api";
|
||||
import {
|
||||
readSession,
|
||||
@@ -11,50 +11,108 @@ import {
|
||||
|
||||
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();
|
||||
}
|
||||
type Credentials = { username: string; password: string };
|
||||
type AuthChoice = {
|
||||
name: string;
|
||||
input: string;
|
||||
value: string;
|
||||
initial?: string;
|
||||
cursor?: number;
|
||||
};
|
||||
export interface LoginPrompt {
|
||||
choices: AuthChoice[];
|
||||
values: Credentials;
|
||||
state: { submitted: boolean; cancelled: boolean; closed: boolean };
|
||||
initialize(): Promise<void>;
|
||||
render(): Promise<void>;
|
||||
submit(): Promise<void>;
|
||||
cancel(): Promise<void>;
|
||||
run(): Promise<LoginResponse>;
|
||||
}
|
||||
|
||||
async function promptPassword(): Promise<string> {
|
||||
if (!stdin.isTTY || !stdin.setRawMode) {
|
||||
throw new Error("Password input requires an interactive terminal");
|
||||
// Enquirer's declarations omit its runtime BasicAuth factory.
|
||||
const BasicAuth = (
|
||||
Enquirer as typeof Enquirer & {
|
||||
BasicAuth: {
|
||||
create(
|
||||
authenticate: (values: Credentials) => Promise<LoginResponse | false>,
|
||||
): new (options: Record<string, unknown>) => LoginPrompt;
|
||||
};
|
||||
}
|
||||
).BasicAuth;
|
||||
|
||||
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;
|
||||
/** Actual BasicAuth prompt; injectable authentication keeps tests offline. */
|
||||
export function createLoginPrompt(
|
||||
username = "",
|
||||
persistent = false,
|
||||
authenticate: typeof loginUser = loginUser,
|
||||
options: { show?: boolean; stdout?: NodeJS.WriteStream } = {},
|
||||
): LoginPrompt {
|
||||
let failure: unknown;
|
||||
let authentication: Promise<LoginResponse | false> | undefined;
|
||||
const Auth = BasicAuth.create((values) => {
|
||||
// Repeated Enter presses must never issue a second login or session write.
|
||||
authentication ??= (async () => {
|
||||
try {
|
||||
const name = values.username.trim();
|
||||
if (!name) throw new Error("Username is required");
|
||||
return await authenticate(name, values.password, persistent);
|
||||
} catch (error) {
|
||||
// BasicAuth doesn't forward rejected authenticate callbacks to run().
|
||||
failure = error;
|
||||
return false as const;
|
||||
}
|
||||
if (value === "\r" || value === "\n") {
|
||||
cleanup();
|
||||
resolve(password);
|
||||
return;
|
||||
})();
|
||||
return authentication;
|
||||
});
|
||||
class KuberBasicAuth extends Auth {
|
||||
private submitting = false;
|
||||
|
||||
override async initialize() {
|
||||
await super.initialize();
|
||||
const choice = this.choices.find((item) => item.name === "username");
|
||||
if (choice) {
|
||||
choice.input = choice.value = username;
|
||||
choice.cursor = username.length;
|
||||
}
|
||||
if (value === "\u007f" || value === "\b") {
|
||||
password = password.slice(0, -1);
|
||||
return;
|
||||
await this.render();
|
||||
}
|
||||
|
||||
override async submit() {
|
||||
if (this.submitting || this.state.closed) return;
|
||||
this.submitting = true;
|
||||
await super.submit();
|
||||
}
|
||||
|
||||
override async render() {
|
||||
if (this.state.submitted) {
|
||||
// FormPrompt skips choice.format on submission and cancellation.
|
||||
const password = this.choices.find(
|
||||
(choice) => choice.name === "password",
|
||||
);
|
||||
if (password) password.input = password.value = password.initial = "";
|
||||
this.values.password = "";
|
||||
}
|
||||
password += value;
|
||||
};
|
||||
stdin.on("data", onData);
|
||||
await super.render();
|
||||
}
|
||||
|
||||
override async run(): Promise<LoginResponse> {
|
||||
let session: LoginResponse;
|
||||
try {
|
||||
session = await super.run();
|
||||
} catch {
|
||||
throw new Error("Login cancelled");
|
||||
}
|
||||
if (failure !== undefined) throw failure;
|
||||
return session;
|
||||
}
|
||||
}
|
||||
return new KuberBasicAuth({
|
||||
name: "login",
|
||||
message: "Log in to kuber.astrxl.dev",
|
||||
initial: { username },
|
||||
showPassword: false,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,14 +134,22 @@ export async function loginUser(
|
||||
}
|
||||
|
||||
/** Share the CLI's credential prompt with onboarding without exposing passwords. */
|
||||
export async function interactiveLogin(): Promise<LoginResponse> {
|
||||
if (!stdin.isTTY)
|
||||
export async function interactiveLogin(
|
||||
username = "",
|
||||
persistent = false,
|
||||
dependencies: {
|
||||
isTTY?: boolean;
|
||||
prompt?: typeof createLoginPrompt;
|
||||
} = {},
|
||||
): Promise<LoginResponse> {
|
||||
if (!(dependencies.isTTY ?? 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);
|
||||
const session = await (dependencies.prompt ?? createLoginPrompt)(
|
||||
username.trim(),
|
||||
persistent,
|
||||
).run();
|
||||
console.log(`Logged in as ${session.user.username}`);
|
||||
return session;
|
||||
}
|
||||
@@ -100,14 +166,7 @@ export const login = defineCommand({
|
||||
},
|
||||
},
|
||||
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}`);
|
||||
await interactiveLogin(String(args._[0] ?? ""), Boolean(args.persist));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user