An expired session exits 3 with one line saying to run quak login (closes #164)
check / check (push) Waiting to run

A 401 from the server that ends a command reaches run in src/cli-run.ts as
an ApiError; run prints one line saying to run "quak login" and exits 3.
quak backup meets it on its first refresh, before it touches any file. For
the commands that load the saved session, a missing or corrupt session file
keeps its message and also exits 3.

Judgement call: quak logout is unchanged; it handles its own errors and
deletes the session file whatever the server answers.
Judgement call: quak backup still prints its two progress lines before the
error line.

Model: opus-5-5
This commit is contained in:
2026-10-06 10:48:13 +00:00
parent ad0e11407f
commit b8c5a26552
6 changed files with 142 additions and 37 deletions
+12 -10
View File
@@ -73,7 +73,9 @@ export const saveSession = (
);
};
// The saved client, or undefined after telling the user why there is none.
// The saved client, or undefined after telling the user why there is none. The
// command then exits 3, the code for "log in again", as `run` in `cli-run.ts`
// does when the server no longer accepts the saved session.
const requireSession = (ctx: CliContext): Client | undefined => {
let client: Client | null;
try {
@@ -150,7 +152,7 @@ export const loginCommand = async (ctx: CliContext): Promise<number> => {
export const whoamiCommand = async (ctx: CliContext): Promise<number> => {
await init();
const client = requireSession(ctx);
if (!client) return 1;
if (!client) return 3;
const info = client.whoami();
ctx.stdout.write(JSON.stringify(info) + "\n");
return 0;
@@ -201,7 +203,7 @@ export const collectionsCommand = async (
): Promise<number> => {
await init();
const client = requireSession(ctx);
if (!client) return 1;
if (!client) return 3;
const lib = await openReadLibrary(ctx, client);
try {
// Force a server round-trip and list in enumeration order (issue #36
@@ -243,7 +245,7 @@ export const filesCommand = async (
): Promise<number> => {
await init();
const client = requireSession(ctx);
if (!client) return 1;
if (!client) return 3;
const collectionID = Number(opts.collection);
if (!Number.isFinite(collectionID)) {
ctx.stderr.write("Invalid collection ID\n");
@@ -285,7 +287,7 @@ export const getCommand = async (
): Promise<number> => {
await init();
const client = requireSession(ctx);
if (!client) return 1;
if (!client) return 3;
const fileID = Number(fileIDStr);
if (!Number.isFinite(fileID)) {
ctx.stderr.write("Invalid file ID\n");
@@ -343,7 +345,7 @@ export const getThumbCommand = async (
): Promise<number> => {
await init();
const client = requireSession(ctx);
if (!client) return 1;
if (!client) return 3;
const fileID = Number(fileIDStr);
if (!Number.isFinite(fileID)) {
ctx.stderr.write("Invalid file ID\n");
@@ -380,7 +382,7 @@ export const backupMetadataCommand = async (
): Promise<number> => {
await init();
const client = requireSession(ctx);
if (!client) return 1;
if (!client) return 3;
const lib = await openReadLibrary(ctx, client);
try {
// Refresh first so the dump holds current account state, not what the
@@ -403,7 +405,7 @@ export const backupCommand = async (
): Promise<number> => {
await init();
const client = requireSession(ctx);
if (!client) return 1;
if (!client) return 3;
ctx.stderr.write("Starting backup...\n");
// The precache is off: the backup fetches what it needs, and must not
@@ -453,7 +455,7 @@ export const listMissingThumbnailsCommand = async (
): Promise<number> => {
await init();
const client = requireSession(ctx);
if (!client) return 1;
if (!client) return 3;
const lib = await openReadLibrary(ctx, client);
try {
// Refresh first so files added since the cache was written are
@@ -491,7 +493,7 @@ export const fixMissingThumbnailsCommand = async (
): Promise<number> => {
await init();
const client = requireSession(ctx);
if (!client) return 1;
if (!client) return 3;
const lib = await openReadLibrary(ctx, client);
try {
// Refresh first so files added since the cache was written are found;
+15 -5
View File
@@ -1,12 +1,15 @@
// Runs one CLI command for `bin/quak.ts` and exits with its code.
import type { Writable } from "node:stream";
import { ApiError } from "./api/client.js";
// Run a command and exit with its code once stdout/stderr have drained.
// Exiting before the drain can truncate piped output, and the library can keep
// the event loop alive after a command returns, so a plain return could hang.
// An error the command throws is printed as one `quak: MESSAGE` line, without
// the stack trace, and exits 1.
// the stack trace, and exits 1. A 401 from the server means it no longer
// accepts the saved session: that prints one line saying to log in again and
// exits 3, as a missing or corrupt session file does.
export const run = async (
command: Promise<number>,
stdout: Writable,
@@ -17,10 +20,17 @@ export const run = async (
try {
code = await command;
} catch (err) {
stderr.write(
`quak: ${err instanceof Error ? err.message : String(err)}\n`,
);
code = 1;
if (err instanceof ApiError && err.status === 401) {
stderr.write(
`quak: the saved session is no longer valid; run "quak login"\n`,
);
code = 3;
} else {
stderr.write(
`quak: ${err instanceof Error ? err.message : String(err)}\n`,
);
code = 1;
}
}
const pending = [stdout, stderr].filter((s) => s.writableLength > 0);
if (pending.length === 0) {