# Workflow - branch (from `main`) - do the work in Next Step - move Next Step to the top of Completed Steps - move the top item of Future Steps into Next Step - commit (`TODO.md` changes in the same commit as the work) - merge to `main` if the branch is not protected, otherwise open a PR - push # Status pre-1.0 # Next Step Tag v1.0.0. # Completed Steps - 2026-09-23: Made the CLI testable and tested it (issue 12). The command bodies moved from `bin/quak.ts` into `src/cli-commands.ts` as functions that take their options and a context (output streams, session directory, cache directory, session loader) and return an exit code; `bin/quak.ts` only wires them to commander and exits with the code once stdout and stderr have drained, so nothing below it calls `process.exit`. `test/cli/commands.test.ts` drives them with a fake client: session file modes, logout, the missing and corrupt session paths, and the output and exit code of `whoami`, `collections`, `files`, `get`, `get-thumb`, `backup` and `helper list-missing-thumbnails`. - 2026-09-22: Hardened the client session lifecycle (issue 10). `Client.fromJSON` checks every snapshot field and each key's decoded length and names the bad field; `toJSON` reads the token through `ApiClient.getAuthToken` and throws when there is none; `logout` zeroes the key buffers, and `collectionsSince` re-checks for logout after its request so it never decrypts with zeroed keys. The CLI reports a corrupt session file separately from a missing one (`src/cli-session.ts`). - 2026-09-22: Sanitized file names taken from server metadata (issue 9). A new `src/filename.ts` holds the one sanitizer, used by `quak get`/`get-thumb` without `--out`, `downloadFile`/`downloadThumbnail` without `outPath`, and the backup and metadata backup trees; it removes separators, control characters, leading dots and Windows device names, and falls back to a name built from the ID for an empty title. Originals-cache extensions are letters and digits only, else `.bin`. A user-supplied path is used as is. `decryptFile` reads a missing or non-string title as "" and rejects metadata that is not a JSON object. - 2026-09-22: Rewrote the README API reference (and the Getting Started / usage snippets) to match the shipped cache/API library on `next` (issue 53, issue 13). Documented `Library.open` and its options, the default-read vs `fresh()` distinction (and that the CLI's read commands are fresh), the record types and `snapshot()`/`subscribe()`, the `albums`/`photos`/`timeline` read surface, `Photo` content methods, `thumbnails.ensure`, the `mldata` search surface, `backup()`, the three request pools (10/5/25), and the on-disk cache layout; noted the deferred content-hash integrity check (issue 68). Docs-only; no code changed. - 2026-09-22: Added resumable, deletion-aware enumeration to `Client` (issue 38, closes issue 7). `collectionsSince`/`filesSince` take a starting cursor, decrypt live records, surface tombstoned ids in a separate `deleted` list (a tombstone has nothing to decrypt, so it is a bare id, not a hollow record), and return the max `updationTime` seen as the cursor to resume from. `filesSince` refuses to loop when the diff reports `hasMore` without advancing the cursor (issue 7). `listCollections`/`listFiles` are now thin wrappers that enumerate from `sinceTime: 0` and drop deletions, so existing callers are unaffected. - 2026-09-22: Carried file size, thumbnail size, and the deletion flag through `decryptFile` (issue 37, foundation for the cache/API design). Live files now populate `file.size`/`thumbnail.size` from the server's `info` (left `undefined` when the server omits it), and `isDeleted` is carried from the diff row onto `EnteFile`. No caller change: `listFiles` still filters deleted rows before decrypting. Surfacing a tombstone through decryption belongs to the enumeration unit (issue 38). - 2026-08-10: Made `lint-once.test.ts` enforce what its header claims. It walked `make check` only, so it never read `Dockerfile` — the image CI builds through `script/cibuild` — and a second `prettier --check .` could be added there with the suite staying green. The walk now also starts at `.gitea/workflows/check.yml` and follows its `run:` steps, so the graph under test is the one CI executes rather than the one someone assumed it executes. The lockfile assertion was a substring check against the whole of `script/bootstrap`, which has two install sites and so reported the branch the containers never take; the two branches are now resolved separately and every `yarn install` in each is required to be `--frozen-lockfile`. Prettier is counted per occurrence instead of per line, so two invocations chained with `&&` no longer read as one, and edges are followed on counted lines instead of being skipped. Every way for the walk to reach nothing — an unknown target, an unknown script, a missing file, a node with no commands, an unknown node kind — is a thrown error rather than a quiet zero. Every assertion in the file was mutation-tested individually. - 2026-08-10: Stopped `make check` running `prettier --check .` twice. Since linting moved into Docker, the duplicate was one container pass and one host pass of the same check: `script/lint` builds `Dockerfile.lint`, which runs prettier as a build step, and `script/check` then called `script/fmt-check` as well. The host call is gone from `script/check` and from `script/precommit`; the container keeps checking formatting, because a successful `Dockerfile.lint` build is what CI treats as proof of a clean tree, and it is also what still fails the pre-commit hook on a badly formatted tree. `script/fmt-check` survives as a standalone entrypoint, whose verdict cannot drift from the container's. A test walks the invocation graph from each entrypoint — through the Makefile shims, the `script/` calls and the `docker build` — and asserts the prettier count, so the duplication cannot come back unnoticed. - 2026-08-10: Moved all linting into Docker. `script/lint` builds a new root `Dockerfile.lint`, which copies the repo into the digest-pinned node image and runs eslint and prettier as build steps, so a successful build is a clean lint; no host lint path remains and `yarn lint` is gone from `package.json`. A fail-closed `LINT_EPOCH` guard stops Docker serving the linter layers from cache, which is how a lint build returns success in under a second having linted nothing. The lint stage inside `Dockerfile` and its `COPY --from=lint` ordering hack are gone: that image now runs `make test` and `make build` only, because `script/check` calls `script/lint` and running it in a container would mean docker inside docker. `script/cibuild` builds the lint image first, then the test and build image. - 2026-08-09: Made `make docker` green and policy-conformant. Multi-stage Dockerfile: a lint stage runs `make fmt-check` and `make lint`, and the check stage takes a `COPY --from=lint` dependency on it before running `make check` and `make build`. `CHECK_EPOCH` and a fail-closed guard stop Docker serving those two layers from cache, which is what let a build report success without running the suite. `script/projectname` says `quak`, so the image is tagged `quak`; `script/bootstrap` updates apt lists before installing, so a Debian base works; `.dockerignore` no longer ships the compiled binary, the caches or agent worktrees into the build context, and keeps `.gitignore` in it for prettier. - 2026-08-09: Fixed the TypeScript build. `rootDir` is the repo root, so `bin/` compiles alongside `src/` instead of failing with TS6059; output is `dist/src/` and `dist/bin/`, which is where `main`, `types` and `bin.quak` now point. `script/build` verifies the declared entrypoints exist after the compiler runs and makes the CLI executable, the Dockerfile runs `make build` as well as `make check`, and a `quak` script makes the README's `yarn quak ` examples work. - 2026-08-09: Retry policy: no retry on 4xx (except `408` and `429`), exponential backoff with full jitter on 5xx, transport failures and truncated transfers, under per-attempt deadlines that cover the response body as well as the request. Downloads retry request, stream consumption and decryption as one unit; `postJSON` and `putJSON` are replayed only when the connection was never established. - 2026-08-09: Downloads verify the secretstream terminated on `TAG_FINAL` and write output atomically: a truncated body is rejected instead of landing on disk as a short file, and plaintext is staged in a sibling temp file and renamed into place, so a failed download leaves the destination untouched. - 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints, Makefile shims, README Entrypoints section - 2026-06-10: Decrypted collections shared by other users (sealed-box keys); listCollections drops deleted-collection tombstones. - 2026-06-10: Login hardening: dual-2FA empty-string fields handled, TOTP preferred when a passkey is also enrolled, interactive input via @inquirer/prompts. - 2026-06-10: Replaced sharp with pure JS (jpeg-js + exif-reader); added single-binary bun build and make install. - 2026-06-09: Added backup-metadata command (ML data always included, --exif opt-in); rewrote README to match the implementation; added thumbnail helper tests. - 2026-05-13: Full CLI surface: login, backup with dedup symlink layout, collections, files, get, get-thumb, thumbnail repair helpers. - 2026-05-13: Client OO API with literate usage tests; file download and decryption; all three metadata layers decrypted and persisted; renamed quack to quak. - 2026-05-11: SRP login flow (email OTP + TOTP) and ApiClient. # Future Steps - Future desktop client, separate repo: - Electron app skeleton consuming this library. - Local SQLite cache keyed on (collectionID, fileID, updationTime). - Background sync worker streaming new files into the cache. - Gallery UI: thumbnails, full-image view, basic search. - Upload, delete, and share operations in the library.