import type { Command } from "commander";import { resolveOperationAccount, type OperationAccount,} from "#/lib/account-selection";import { maybeAutoSyncBackup, maybeAutoUpdateBackup } from "#/lib/backup";import { getDefaultAccountSelector } from "#/lib/config"; export interface CliCommandContext { program: Command; print: (data: unknown, asJson: boolean) => void; asJson: () => boolean; autoSyncAfterWrite: () => Promise<void>; autoUpdateBeforeRead: () => Promise<void>; parseNonNegativeIntegerOption: ( value: string | undefined, option: string, ) => number | undefined; parsePositiveIntegerOption: ( value: string | undefined, option: string, ) => number | undefined;} let previousXurlUsername: | { existed: boolean; value: string | undefined } | undefined; function commandHasAccountOption(command: Command) { return command.options.some((option) => option.attributeName() === "account");} function selectOperationAccount( command: Command,): OperationAccount | undefined { if (!commandHasAccountOption(command)) return undefined; const source = command.getOptionValueSource("account"); const current = command.getOptionValue("account"); const explicit = source === "cli" && typeof current === "string" ? current : undefined; const configSelector = getDefaultAccountSelector(); const selector = explicit || configSelector; if (!selector) return undefined; const account = resolveOperationAccount(selector); command.setOptionValueWithSource( "account", account.id, explicit ? "cli" : "config", ); return account;} export function resetOperationAccountSelection() { if (!previousXurlUsername) return; if (previousXurlUsername.existed) { process.env.BIRDCLAW_XURL_OAUTH2_USERNAME = previousXurlUsername.value; } else { delete process.env.BIRDCLAW_XURL_OAUTH2_USERNAME; } previousXurlUsername = undefined;} export function configureOperationAccountSelection(program: Command) { program.hook("preAction", (_root, actionCommand) => { const account = selectOperationAccount(actionCommand); if (!account) return; previousXurlUsername ??= { existed: Object.hasOwn(process.env, "BIRDCLAW_XURL_OAUTH2_USERNAME"), value: process.env.BIRDCLAW_XURL_OAUTH2_USERNAME, }; process.env.BIRDCLAW_XURL_OAUTH2_USERNAME = account.username; }); program.hook("postAction", resetOperationAccountSelection);} export function print(data: unknown, asJson: boolean) { if (asJson) { console.log(JSON.stringify(data, null, 2)); return; } console.log(data);} export function printError(error: string) { console.error(JSON.stringify({ error }));} export function errorMessage(error: unknown) { return error instanceof Error ? error.message : String(error);} export function parseNonNegativeIntegerOption( value: string | undefined, option: string,) { if (value === undefined) return undefined; const trimmed = value.trim(); if (!/^\d+$/.test(trimmed)) { printError(`${option} must be a non-negative integer`); process.exitCode = 1; return undefined; } const parsed = Number.parseInt(trimmed, 10); if (!Number.isSafeInteger(parsed)) { printError(`${option} must be a non-negative integer`); process.exitCode = 1; return undefined; } return parsed;} export function parsePositiveIntegerOption( value: string | undefined, option: string,) { const parsed = parseNonNegativeIntegerOption(value, option); if (parsed === undefined) return undefined; if (parsed < 1) { printError(`${option} must be at least 1`); process.exitCode = 1; return undefined; } return parsed;} async function autoUpdateBeforeRead() { try { const result = await maybeAutoUpdateBackup(); if (!result.ok) { console.error(`birdclaw backup auto-sync failed: ${result.error}`); } } catch (error) { console.error(`birdclaw backup auto-sync failed: ${errorMessage(error)}`); }} async function autoSyncAfterWrite() { try { const result = await maybeAutoSyncBackup(); if (!result.ok) { console.error(`birdclaw backup sync failed: ${result.error}`); } } catch (error) { console.error(`birdclaw backup sync failed: ${errorMessage(error)}`); }} export function createCommandContext(program: Command): CliCommandContext { return { program, print, asJson: () => program.opts().json ?? false, autoSyncAfterWrite, autoUpdateBeforeRead, parseNonNegativeIntegerOption, parsePositiveIntegerOption, };}
import type { Command } from "commander";
import {
resolveOperationAccount,
type OperationAccount,
} from "#/lib/account-selection";
import { maybeAutoSyncBackup, maybeAutoUpdateBackup } from "#/lib/backup";
import { getDefaultAccountSelector } from "#/lib/config";
export interface CliCommandContext {
program: Command;
print: (data: unknown, asJson: boolean) => void;
asJson: () => boolean;
autoSyncAfterWrite: () => Promise<void>;
autoUpdateBeforeRead: () => Promise<void>;
parseNonNegativeIntegerOption: (
value: string | undefined,
option: string,
) => number | undefined;
parsePositiveIntegerOption: (
value: string | undefined,
option: string,
) => number | undefined;
}
let previousXurlUsername:
| { existed: boolean; value: string | undefined }
| undefined;
function commandHasAccountOption(command: Command) {
return command.options.some((option) => option.attributeName() === "account");
}
function selectOperationAccount(
command: Command,
): OperationAccount | undefined {
if (!commandHasAccountOption(command)) return undefined;
const source = command.getOptionValueSource("account");
const current = command.getOptionValue("account");
const explicit =
source === "cli" && typeof current === "string" ? current : undefined;
const configSelector = getDefaultAccountSelector();
const selector = explicit || configSelector;
if (!selector) return undefined;
const account = resolveOperationAccount(selector);
command.setOptionValueWithSource(
"account",
account.id,
explicit ? "cli" : "config",
);
return account;
}
export function resetOperationAccountSelection() {
if (!previousXurlUsername) return;
if (previousXurlUsername.existed) {
process.env.BIRDCLAW_XURL_OAUTH2_USERNAME = previousXurlUsername.value;
} else {
delete process.env.BIRDCLAW_XURL_OAUTH2_USERNAME;
}
previousXurlUsername = undefined;
}
export function configureOperationAccountSelection(program: Command) {
program.hook("preAction", (_root, actionCommand) => {
const account = selectOperationAccount(actionCommand);
if (!account) return;
previousXurlUsername ??= {
existed: Object.hasOwn(process.env, "BIRDCLAW_XURL_OAUTH2_USERNAME"),
value: process.env.BIRDCLAW_XURL_OAUTH2_USERNAME,
};
process.env.BIRDCLAW_XURL_OAUTH2_USERNAME = account.username;
});
program.hook("postAction", resetOperationAccountSelection);
}
export function print(data: unknown, asJson: boolean) {
if (asJson) {
console.log(JSON.stringify(data, null, 2));
return;
}
console.log(data);
}
export function printError(error: string) {
console.error(JSON.stringify({ error }));
}
export function errorMessage(error: unknown) {
return error instanceof Error ? error.message : String(error);
}
export function parseNonNegativeIntegerOption(
value: string | undefined,
option: string,
) {
if (value === undefined) return undefined;
const trimmed = value.trim();
if (!/^\d+$/.test(trimmed)) {
printError(`${option} must be a non-negative integer`);
process.exitCode = 1;
return undefined;
}
const parsed = Number.parseInt(trimmed, 10);
if (!Number.isSafeInteger(parsed)) {
printError(`${option} must be a non-negative integer`);
process.exitCode = 1;
return undefined;
}
return parsed;
}
export function parsePositiveIntegerOption(
value: string | undefined,
option: string,
) {
const parsed = parseNonNegativeIntegerOption(value, option);
if (parsed === undefined) return undefined;
if (parsed < 1) {
printError(`${option} must be at least 1`);
process.exitCode = 1;
return undefined;
}
return parsed;
}
async function autoUpdateBeforeRead() {
try {
const result = await maybeAutoUpdateBackup();
if (!result.ok) {
console.error(`birdclaw backup auto-sync failed: ${result.error}`);
}
} catch (error) {
console.error(`birdclaw backup auto-sync failed: ${errorMessage(error)}`);
}
}
async function autoSyncAfterWrite() {
try {
const result = await maybeAutoSyncBackup();
if (!result.ok) {
console.error(`birdclaw backup sync failed: ${result.error}`);
}
} catch (error) {
console.error(`birdclaw backup sync failed: ${errorMessage(error)}`);
}
}
export function createCommandContext(program: Command): CliCommandContext {
return {
program,
print,
asJson: () => program.opts().json ?? false,
autoSyncAfterWrite,
autoUpdateBeforeRead,
parseNonNegativeIntegerOption,
parsePositiveIntegerOption,
};
}