feat: release 2.7.0-rc3

This commit is contained in:
2026-10-07 05:10:59 +00:00 Unverified
parent c8de9ddcba
commit 2a24b2ff56
14 changed files with 1302 additions and 249 deletions
+111 -52
View File
@@ -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));
},
});