#!/usr/bin/env node import { stdout, stderr } from "node:process"; import { input, password } from "@inquirer/prompts"; import { Command } from "commander"; import envPaths from "env-paths"; import { init } from "../src/crypto/index.js"; import { type CliContext, loginCommand, whoamiCommand, logoutCommand, collectionsCommand, filesCommand, getCommand, getThumbCommand, backupMetadataCommand, backupCommand, listMissingThumbnailsCommand, fixMissingThumbnailsCommand, } from "../src/cli-commands.js"; import { run as runCommand } from "../src/cli-run.js"; import { loadSession } from "../src/cli-session.js"; import { Client } from "../src/client.js"; import { VERSION } from "../src/index.js"; const paths = envPaths("quak", { suffix: "" }); const program = new Command(); program .name("quak") .description("CLI for the Ente end-to-end encrypted photo service") .version(VERSION) .option( "--cache-dir ", "Directory for the local metadata/content cache " + "(default: the per-user cache directory)", ); const context = (): CliContext => ({ stdout, stderr, sessionDir: paths.data, cacheDir: program.opts<{ cacheDir?: string }>().cacheDir, loadSession, login: (opts) => Client.login(opts), prompt: (message) => input({ message }), promptSecret: (message) => password({ message, mask: true }), }); const run = (command: Promise): Promise => runCommand(command, stdout, stderr, (code) => process.exit(code)); program .command("login") .description("Log in to an Ente account and save the session") .action(() => run(loginCommand(context()))); program .command("whoami") .description("Print the logged-in account") .action(() => run(whoamiCommand(context()))); program .command("logout") .description("End the session on the server and delete the saved session") .action(() => run(logoutCommand(context()))); program .command("collections") .description("List all collections (albums)") .option("--json", "Output as JSON array") .action((opts: { json?: boolean }) => run(collectionsCommand(context(), opts)), ); program .command("files") .description("List files in a collection") .requiredOption( "--collection ", "Collection ID (from `quak collections`)", ) .option("--json", "Output as JSON array") .action((opts: { collection: string; json?: boolean }) => run(filesCommand(context(), opts)), ); program .command("get") .description("Download and decrypt a single file") .argument("", "File ID (from `quak files`)") .option("--out ", "Output file path") .option("--collection ", "Accepted for compatibility; ignored") .action((fileID: string, opts: { out?: string }) => run(getCommand(context(), fileID, opts)), ); program .command("get-thumb") .description("Download and decrypt a thumbnail") .argument("", "File ID (from `quak files`)") .option("--out ", "Output file path") .option("--collection ", "Accepted for compatibility; ignored") .action((fileID: string, opts: { out?: string }) => run(getThumbCommand(context(), fileID, opts)), ); program .command("backup-metadata") .description( "Dump all decrypted account metadata to a directory of JSON files", ) .argument("", "Output directory") .option( "--exif", "Download each file and extract full EXIF/IPTC/XMP metadata (slow)", ) .option("--all", "Alias for --exif") .action((dir: string, opts: { exif?: boolean; all?: boolean }) => run(backupMetadataCommand(context(), dir, opts)), ); program .command("backup") .description( "Download all photos to a local directory, organized by collection", ) .argument("", "Output directory") .option("--json", "Print result as JSON instead of human-readable summary") .action((dir: string, opts: { json?: boolean }) => run(backupCommand(context(), dir, opts)), ); const helper = program .command("helper") .description("Maintenance and repair utilities"); helper .command("list-missing-thumbnails") .description("List files whose thumbnails are missing or empty") .option("--json", "Output as JSON array") .action((opts: { json?: boolean }) => run(listMissingThumbnailsCommand(context(), opts)), ); helper .command("fix-missing-thumbnails") .description( "Generate and upload thumbnails for files that are missing them", ) .option( "--file ", "Specific file IDs to fix (default: fix all missing)", ) .option("--json", "Output as JSON") .action((opts: { file?: string[]; json?: boolean }) => run(fixMissingThumbnailsCommand(context(), opts)), ); await init(); await program.parseAsync();