Test quak login and backup-metadata --exif (closes #110)
check / check (push) Successful in 42s

loginCommand now takes its login function and prompts from CliContext;
bin/quak.ts passes Client.login and the terminal prompts, so behaviour is
unchanged. Tests cover a login from QUAK_EMAIL/QUAK_PASSWORD with no
prompt, the TOTP prompt, a failed login, and the saved session's modes,
and show that --exif and --all each turn on EXIF extraction. Also rewraps
the header comment of src/cli-commands.ts.

Model: opus-5-5
This commit was merged in pull request #125.
This commit is contained in:
2026-09-23 09:27:40 +02:00
parent ae76eb3f74
commit 7740ebfd4d
4 changed files with 174 additions and 16 deletions
+17 -14
View File
@@ -4,10 +4,8 @@
// code; a thrown error is left to the caller. Nothing here calls
// `process.exit`: `bin/quak.ts` wires these to the command line, and `run` in
// `cli-run.ts` prints a thrown error as one line and exits once output has
// drained. Output must stay byte-identical
// (see `cli-output.ts`).
// drained. Output must stay byte-identical (see `cli-output.ts`).
import { input, password as passwordPrompt } from "@inquirer/prompts";
import {
copyFileSync,
existsSync,
@@ -16,7 +14,11 @@ import {
writeFileSync,
} from "node:fs";
import { join } from "node:path";
import { Client, type ClientSnapshot } from "./client.js";
import {
type Client,
type ClientSnapshot,
type LoginOptions,
} from "./client.js";
import { init } from "./crypto/index.js";
import {
defaultCacheDirectory,
@@ -44,6 +46,12 @@ export interface CliContext {
// Reads the session file into a client, or null when there is none. The
// CLI passes `loadSession` from `cli-session.ts`; tests pass a fake client.
loadSession: (path: string) => Client | null;
// Used by `login` only. The CLI passes `Client.login` and terminal
// prompts; tests pass fakes.
login: (opts: LoginOptions) => Promise<Client>;
prompt: (message: string) => Promise<string>;
// Like `prompt`, but the answer is masked as it is typed.
promptSecret: (message: string) => Promise<string>;
}
const sessionPath = (ctx: CliContext): string =>
@@ -109,24 +117,19 @@ const openReadLibrary = (ctx: CliContext, client: Client): Promise<Library> =>
precacheOriginals: false,
});
const prompt = async (message: string): Promise<string> => input({ message });
const promptSecret = async (message: string): Promise<string> =>
passwordPrompt({ message, mask: true });
export const loginCommand = async (ctx: CliContext): Promise<number> => {
await init();
const email = process.env.QUAK_EMAIL ?? (await prompt("Email"));
const email = process.env.QUAK_EMAIL ?? (await ctx.prompt("Email"));
const password =
process.env.QUAK_PASSWORD ?? (await promptSecret("Password"));
process.env.QUAK_PASSWORD ?? (await ctx.promptSecret("Password"));
ctx.stderr.write("Authenticating...\n");
try {
const client = await Client.login({
const client = await ctx.login({
email,
password,
totp: async () => prompt("TOTP code: "),
emailOTP: async () => prompt("Email verification code: "),
totp: async () => ctx.prompt("TOTP code: "),
emailOTP: async () => ctx.prompt("Email verification code: "),
});
saveSession(ctx.sessionDir, client.toJSON());