3 Commits
Author SHA1 Message Date
clawbot aeb275e137 README: the close() sentence split in two (closes #231)
check / check (push) Waiting to run
The sentence in "Opening a library" had a relative clause with its
"that" left out, so "returns resolves" read as two verbs in a row. It
is now two sentences; the behaviour described is unchanged and matches
`close()` in `src/library/index.ts`.

Model: fable-5-1 (change); opus-5-5 (landing message)
2026-10-26 19:32:03 +01:00
clawbot 67794bf5f8 README: four sentences fixed for the documents gate (closes #229)
check / check (push) Waiting to run
The documents gate on `next` failed five README sentences
(#219 (comment)). Four
are rewritten after a check against the code:

- the keywords score sentence is split in two;
- `mlData` drops the whole `clip` block, as `src/backup.ts` and
  `src/metadata-backup.ts` do;
- a file name gets its ID past 255 bytes and a collection name past
  250, and either is then cut short, as `uniqueNames` does;
- `lastTriedAt` is taken once per run, after its refresh.

The IDs sentence keeps `-1`: the pinned option parser passes a negative
number to a command with no subcommands as an argument, so
`quak get -1` reaches the ID check. Nothing else changes; `make fmt`
reflowed the touched paragraphs.

Model: fable-5-1 (change); opus-5-5 (landing message)
2026-10-26 18:59:28 +01:00
clawbot b5fcbf4400 README.md rewritten sentence by sentence (closes #219)
check / check (push) Waiting to run
The old text served as the list of what to cover. The new text leads
with what quak is, what it is for and how to use it. Every command,
option, exit code and file layout follows in reference sections, then the
library reference, then design, entrypoints and development. Every
statement was checked against the code on next; the pull request lists
each disagreement. Sentences stay under about 30 words, tables are lists,
and no issue links or project history remain in text for users. Two code
comments now name the new README sections, and the one in
`src/cli-log.ts` matches "What quak prints" on stderr, stdout and exit
status; a third no longer cites the README.

Model: fable-5-1
2026-10-26 17:48:25 +01:00
3 changed files with 108 additions and 98 deletions
+97 -90
View File
@@ -158,11 +158,11 @@ output to `~/quak-backup.log`. Sunday's run adds `--verify`:
A run with `--verify` does all a plain run does. It also hashes each original A run with `--verify` does all a plain run does. It also hashes each original
already in the backup and compares the result with the content hash. The content already in the backup and compares the result with the content hash. The content
hash is the hash of the file's bytes that Ente recorded at upload. It fetches a hash is the hash of the file's bytes that Ente recorded at upload. The run
new copy of each original that does not match, and replaces the original once fetches a new copy of each original that does not match, and replaces the
the copy matches. Sunday's run is the `--verify` run, rather than a second job original once the copy matches. Sunday's run is the `--verify` run, rather than
that night, because two backups of one directory cannot run at once. The second a second job that night, because two backups of one directory cannot run at
one exits 2 without doing anything. once. The second one exits 2 without doing anything.
Cron runs the job with a short `PATH`, usually `/usr/bin:/bin`, so the crontab Cron runs the job with a short `PATH`, usually `/usr/bin:/bin`, so the crontab
names quak by its full path. quak finds the saved session through `HOME`, which names quak by its full path. quak finds the saved session through `HOME`, which
@@ -314,16 +314,16 @@ refused, because both files would land on it.
#### `quak get-thumb <fileID> [--out <path>]` #### `quak get-thumb <fileID> [--out <path>]`
Downloads and decrypts one file's thumbnail, the small JPEG Ente shows in a Downloads and decrypts one file's thumbnail, the small JPEG Ente shows in a
grid. It works as `get` does. Without `--out`, the name is `thumb_` followed by grid. It writes one file, also for a live photo. Without `--out`, the name is
the title. `thumb_` followed by the title. In all else it works as `get` does.
#### `quak backup <dir> [--json] [--verify]` #### `quak backup <dir> [--json] [--verify]`
Backs up every file in the account to `<dir>`. The layout is described under Backs up every file in the account to `<dir>`. The layout is described under
"The backup directory". It downloads only the originals that are not yet in the "The backup directory". The command downloads only the originals that are not
backup, and it goes on past a file that fails. A file is downloaded once, yet in the backup, and it goes on past a file that fails. A file is downloaded
however many collections hold it. An original the cache already holds is copied once, however many collections hold it. An original the cache already holds is
from there instead. copied from there instead.
Progress goes to stderr: one line per original fetched, and a `FAILED` line for Progress goes to stderr: one line per original fetched, and a `FAILED` line for
each original or album that failed. The run ends with a summary of counts and, each original or album that failed. The run ends with a summary of counts and,
@@ -362,7 +362,8 @@ Writes the metadata quak keeps about the account, its collections and its files
as JSON files under `<dir>`. The layout is described under "The metadata dump as JSON files under `<dir>`. The layout is described under "The metadata dump
directory". Photo content is not written. With `--exif`, or its alias `--all`, directory". Photo content is not written. With `--exif`, or its alias `--all`,
the command also fetches each file's original and records the image's own the command also fetches each file's original and records the image's own
metadata from it (see "The metadata dump directory"). metadata from it (see "The metadata dump directory"). Each original it fetches
stays in the cache.
Ente's apps compute machine-learning data for each photo, such as the faces in Ente's apps compute machine-learning data for each photo, such as the faces in
it (see "The JSON beside each original"). The command fetches it in requests of it (see "The JSON beside each original"). The command fetches it in requests of
@@ -377,9 +378,9 @@ word, and does not change the exit status.
#### `quak helper list-missing-thumbnails [--json]` #### `quak helper list-missing-thumbnails [--json]`
Finds the files whose thumbnail is missing on the server. It downloads every Finds the files whose thumbnail is missing on the server. It downloads the
thumbnail in the account, one collection at a time, newest collection first, so thumbnail of every file you own, one collection at a time, newest updated first,
on a large account it takes a while. A thumbnail counts as missing when the so on a large account it takes a while. A thumbnail counts as missing when the
server answers with an empty body or with HTTP 404. Any other failure is logged server answers with an empty body or with HTTP 404. Any other failure is logged
as `[<collection>] Could not check <title>: <reason> (not reported as missing)`, as `[<collection>] Could not check <title>: <reason> (not reported as missing)`,
and the file is left out of the list. The command exits 0 even when some files and the file is left out of the list. The command exits 0 even when some files
@@ -477,20 +478,11 @@ before any request is sent. The command logs `Invalid file ID: <arg>` or
### What quak prints ### What quak prints
stdout holds only a command's output: the listing, the JSON, or nothing. The stdout holds a command's output, such as the listing or the JSON, and the
prompts of `login` go to stdout too. Every other line goes to stderr as a log prompts of `login`. Every other line goes to stderr as a log line at one of six
line at one of six levels. From the quietest, the levels are fatal, error, levels. From the quietest, the levels are fatal, error, warning, info, debug and
warning, info, debug and trace. A run prints the lines at its own level and at trace. A run prints the lines at its own level and at every quieter one. The
every quieter one. The level is info unless `-v`, `-vv` or `-q` says otherwise. level is info unless `-v`, `-vv` or `-q` says otherwise.
The one exception is a command line quak cannot parse, such as a missing
argument, an unknown option or `-q` with `-v`. quak refuses it with one
`error: …` line that is not a log line, and exits 1. `-q` does not hide that
line. `quak` with no command prints the usage on stderr and exits 1.
`yarn quak` prints yarn's own lines on stdout as well. To pipe a command's
output, run `yarn --silent quak files --json` or
`node dist/bin/quak.js files --json` instead.
- Fatal: why a command exits non-zero. A command that runs and exits non-zero - Fatal: why a command exits non-zero. A command that runs and exits non-zero
logs at least one fatal line. A command that exits 1 because files or albums logs at least one fatal line. A command that exits 1 because files or albums
@@ -516,6 +508,15 @@ output, run `yarn --silent quak files --json` or
one that exits non-zero prints only its fatal lines. The prompts of `login` are one that exits non-zero prints only its fatal lines. The prompts of `login` are
not log lines, and `-q` leaves them alone. not log lines, and `-q` leaves them alone.
Two things on stderr are not log lines. A command line that quak cannot parse,
such as a missing argument, an unknown option or `-q` with `-v`, is refused with
an `error: …` message, and quak exits 1. `-q` does not hide the message. `quak`
with no command prints the usage on stderr and exits 1.
`yarn quak` prints yarn's own lines on stdout as well. To pipe a command's
output, run `yarn --silent quak files --json` or
`node dist/bin/quak.js files --json` instead.
With `--json`, a command's JSON on stdout takes the place of its summary on With `--json`, a command's JSON on stdout takes the place of its summary on
stderr. For `backup`, that is the `--- Backup complete ---` line, the counts and stderr. For `backup`, that is the `--- Backup complete ---` line, the counts and
the `Failed files:` list. For `list-missing-thumbnails`, it is the `Failed files:` list. For `list-missing-thumbnails`, it is
@@ -688,13 +689,13 @@ The JSON file beside an original holds:
Update times are in microseconds, as Ente records them. Update times are in microseconds, as Ente records them.
The file also holds the machine-learning data Ente computed for the file, as The JSON also holds the machine-learning data Ente computed for the photo, as
`mlData`. Ente's apps detect faces in each photo and compute a CLIP embedding `mlData`. Ente's apps detect faces in each photo and compute a CLIP embedding
for it, a list of 512 numbers that describes what the photo shows. `mlData` for it, a list of 512 numbers that describes what the photo shows. `mlData`
holds everything but that embedding. In its place the file holds `keywords`, holds everything but the `clip` block, which holds that embedding. In its place
derived from the embedding as described under "Keywords". A file with no the JSON holds `keywords`, derived from the embedding as described under
machine-learning data has neither field. A file whose data holds no usable "Keywords". A file with no machine-learning data has neither field. A file whose
embedding has no `keywords`. data holds no usable embedding has no `keywords`.
The backup waits for the library's fetch of machine-learning data to finish The backup waits for the library's fetch of machine-learning data to finish
before it writes the JSON files. When a file's data cannot be fetched, before it writes the JSON files. When a file's data cannot be fetched,
@@ -747,11 +748,11 @@ derives a file's keywords the way the app sorts it. The categories are:
Each category has a query, and a text embedding of that query from Ente's CLIP Each category has a query, and a text embedding of that query from Ente's CLIP
text model. A file's score for a category is the cosine similarity of the file's text model. A file's score for a category is the cosine similarity of the file's
embedding and the category's: a number from -1 to 1, higher the more alike the embedding and the category's. That is a number from -1 to 1; the more alike the
two embeddings are. The file falls in the category when its score reaches the two embeddings are, the higher it is. The file falls in the category when its
category's minimum score. The keywords are the titles of the categories the file score reaches the category's minimum score. The keywords are the titles of the
falls in, highest score first. A video never falls in a category marked "no categories the file falls in, highest score first. A video never falls in a
videos", as in the app. category marked "no videos", as in the app.
#### Collection directories #### Collection directories
@@ -774,19 +775,21 @@ ignoring case, each gets its ID added: two albums named `Trip` become
`Trip (10)/` and `Trip (11)/`. Two files in one collection whose titles match, `Trip (10)/` and `Trip (11)/`. Two files in one collection whose titles match,
ignoring case, get their IDs added before the extension: `IMG_0001 (12345).JPG` ignoring case, get their IDs added before the extension: `IMG_0001 (12345).JPG`
and `IMG_0001 (12346).JPG`. A collection named like another collection's JSON and `IMG_0001 (12346).JPG`. A collection named like another collection's JSON
file, such as `Trip.json` beside `Trip`, also gets its ID added. A name too long file, such as `Trip.json` beside `Trip`, also gets its ID added. A file name
for a file name, which most file systems limit to 255 bytes, gets its ID added longer than 255 bytes, the limit on most file systems, gets its ID added too. So
too. It is then cut short before the ID, just far enough to fit. IDs never does a collection name longer than 250 bytes, which leaves room for `.json`.
Either name is then cut short before the ID, just far enough to fit. IDs never
change, so a name stays the same from run to run until such a clash appears or change, so a name stays the same from run to run until such a clash appears or
goes away. goes away.
Each run removes every symlink in a collection's directory that points into the Each run removes every symlink in a collection's directory that points into the
date folders and is not one of its current links, yours included. A file or date folders and is not one of its current links, yours included. A file or
symlink of yours at the name of one of its links is replaced by the link. A symlink of yours at the name of one of its links is replaced by the link. A
directory at that name stays, and the file goes into `failures.json`. Anything directory of yours at that name stays, and the photo whose link it blocks goes
else you put under `collections/` stays. A run removes the directory and JSON of into `failures.json`. Anything else you put under `collections/` stays. A run
a collection that was deleted or renamed, once the directory holds nothing else. removes the directory and JSON of a collection that was deleted or renamed, once
One that still holds something of yours stays, with its JSON. the directory holds nothing else. One that still holds something of yours stays,
with its JSON.
A collection whose directory or JSON cannot be written does not stop the run. A collection whose directory or JSON cannot be written does not stop the run.
For example, a file you put there may already have the directory's name. The run For example, a file you put there may already have the directory's name. The run
@@ -831,8 +834,9 @@ Each entry holds:
- `fileID` and `title`; - `fileID` and `title`;
- `classification`: the kind of failure; - `classification`: the kind of failure;
- `attempts`: how many runs have tried the file; - `attempts`: how many runs in a row have failed the file;
- `lastTriedAt`: when it was last tried, as a Unix time in milliseconds; - `lastTriedAt`: when the run that last tried it finished its refresh, as a Unix
time in milliseconds;
- `error`: the error, as quak printed it. - `error`: the error, as quak printed it.
The kinds are: The kinds are:
@@ -855,8 +859,8 @@ ends, whether it succeeded or failed.
The lock is a directory that The lock is a directory that
[proper-lockfile](https://github.com/moxystudio/node-proper-lockfile) creates. [proper-lockfile](https://github.com/moxystudio/node-proper-lockfile) creates.
While the backup runs, the backup updates the directory's modification time While the backup runs, it updates the modification time of `backup.lock` every 5
every 5 seconds. A second backup of the directory fails at once: it prints seconds. A second backup of `<dir>` fails at once: it prints
`quak: another backup of <dir> is running` and exits 2. A run that is killed, or `quak: another backup of <dir> is running` and exits 2. A run that is killed, or
cut off by a crash, can leave the lock behind. Once the lock has gone 10 seconds cut off by a crash, can leave the lock behind. Once the lock has gone 10 seconds
without an update, the next run takes it over, so nobody has to remove it by without an update, the next run takes it over, so nobody has to remove it by
@@ -960,9 +964,9 @@ of the same name apart.
`files`. Each `<fileID>.json` holds the fields a backup's JSON beside an `files`. Each `<fileID>.json` holds the fields a backup's JSON beside an
original holds, with these differences: original holds, with these differences:
- `imageMetadata` is written only with `--exif`. The original of every file is - `imageMetadata` is written only with `--exif`. The original of every file,
then downloaded, videos included, and read as a backup reads it (see "The JSON videos included, is then downloaded and read for its EXIF, XMP and JPEG
beside each original"). dimensions.
- A file whose original holds no image metadata gets no `imageMetadata` field. - A file whose original holds no image metadata gets no `imageMetadata` field.
- An original that cannot be downloaded or read gives `imageMetadataError` - An original that cannot be downloaded or read gives `imageMetadataError`
instead. instead.
@@ -1053,14 +1057,14 @@ still in flight.
`"refresh"`, `"fetchMLData"` for a machine-learning fetch, and `"refresh"`, `"fetchMLData"` for a machine-learning fetch, and
`"precacheThumbnails"` or `"precacheOriginals"` for the two background `"precacheThumbnails"` or `"precacheOriginals"` for the two background
downloads. Its `status` is `"started"` as the work starts and `"done"` as it downloads. Its `status` is `"started"` as the work starts and `"done"` as it
ends. A refresh or a machine-learning fetch that fails ends with `"failed"` and ends. A refresh or a machine-learning fetch that fails ends with `"failed"`
the error message instead. In between, a refresh sends `"progress"` after each instead, with the message in `error`. In between, a refresh sends `"progress"`
collection whose files it fetched: `fetched` is the number of collections after each collection whose files it fetched: `fetched` is the number of
fetched so far, and `total` the number it will fetch. A refresh fetches the collections fetched so far, and `total` the number it will fetch. A refresh
files of each collection that is new or changed since the cache's copy, so on an fetches the files of each collection that is new or changed since the cache's
empty cache it fetches those of every collection. The `"done"` event of a copy, so on an empty cache it fetches those of every collection. The `"done"`
machine-learning fetch, and of each background download, also carries `fetched`: event of a machine-learning fetch, and of each background download, also carries
how many items it newly stored. `fetched`: how many items it newly stored.
`lib.status()` returns a `LibraryStatus`. It holds the collection and file `lib.status()` returns a `LibraryStatus`. It holds the collection and file
counts. `lastRefreshAt` is the time of the last refresh that succeeded, and counts. `lastRefreshAt` is the time of the last refresh that succeeded, and
@@ -1075,10 +1079,10 @@ cached, so the next fetch asks for it again. The reasons are kept in memory
only. only.
`lib.close()` stops the background timer. It can be called more than once. A `lib.close()` stops the background timer. It can be called more than once. A
refresh in flight is left to finish, with its write to the cache. The promise refresh in flight is left to finish, with its write to the cache. `close()`
`close()` returns resolves once that refresh, the machine-learning fetch and the returns a promise. It resolves once that refresh, the machine-learning fetch and
background downloads already running have all finished. The cache directory can the background downloads already running have all finished. The cache directory
then be removed. After `close()`, `fresh()`, `freshCollections()`, can then be removed. After `close()`, `fresh()`, `freshCollections()`,
`freshFiles()` and `lib.backup()` reject with `the library is closed`. `freshFiles()` and `lib.backup()` reject with `the library is closed`.
### Default reads and fresh reads ### Default reads and fresh reads
@@ -1183,11 +1187,11 @@ progress of that one file.
keyed by tag name, each as exifreader decodes it with its `id`, `value`, keyed by tag name, each as exifreader decodes it with its `id`, `value`,
`description` and `computed` value. For example, `Make` is `description` and `computed` value. For example, `Make` is
`{ id: 271, value: ["Example"], description: "Example", computed: "Example" }`. `{ id: 271, value: ["Example"], description: "Example", computed: "Example" }`.
A tag exifreader has no name for is keyed `undefined-<tag number>`. The tags A tag that exifreader has no name for is keyed `undefined-<tag number>`. The
of the thumbnail embedded in the file are under `Thumbnail`, so they cannot tags of the thumbnail embedded in the file are under `Thumbnail`, so they
hide the main image's tags of the same name. The thumbnail image itself is cannot hide the main image's tags of the same name. The thumbnail image itself
left out. Any file exifreader cannot read gives `{}`, and a video gives `{}` is left out. Any file exifreader cannot read gives `{}`, and a video gives
without being downloaded. `{}` without being downloaded.
- `await photo.make(opts?)` returns one common EXIF field, picked from the tags - `await photo.make(opts?)` returns one common EXIF field, picked from the tags
`exif()` returns and typed as in `PhotoExif`, or `undefined` when the file `exif()` returns and typed as in `PhotoExif`, or `undefined` when the file
lacks it. These methods do the same for their fields: lacks it. These methods do the same for their fields:
@@ -1307,8 +1311,9 @@ Only the requests named above go through the pools. A refresh,
with with
`new RequestPools({ metadataConcurrency, contentConcurrency, thumbnailConcurrency })` `new RequestPools({ metadataConcurrency, contentConcurrency, thumbnailConcurrency })`
and pass it as `pools`. Each pool serves on-demand work ahead of background and pass it as `pools`. Each pool serves on-demand work ahead of background
work. A task submitted under a key that is still pending is not run a second work. Two tasks given to `BoundedPool.run()` with the same `key` run once while
time. An idle pool never lends its slots to a busy one. the first is pending, and both get its result. An idle pool never lends its
slots to a busy one.
`lib.backup()` works on as many originals at once as the content pool allows, 5 `lib.backup()` works on as many originals at once as the content pool allows, 5
by default. `quak backup` uses the default. The limit covers everything it does by default. `quak backup` uses the default. The limit covers everything it does
@@ -1378,9 +1383,9 @@ They differ because a download's length depends on the file and the link. A
whole-transfer deadline short enough to catch a hung connection would cancel a whole-transfer deadline short enough to catch a hung connection would cancel a
large video on a slow link that is still making progress. The download deadline large video on a slow link that is still making progress. The download deadline
restarts each time bytes arrive, so a slow download runs as long as it keeps restarts each time bytes arrive, so a slow download runs as long as it keeps
moving, and one that stalls is aborted after 60 seconds of silence. It covers moving, and one that stalls is aborted after 60 seconds of silence. The download
the wait for the headers as well as the body. There is no limit on the total deadline covers the wait for the headers as well as the body. There is no limit
length of a download. on the total length of a download.
A request that changes state is not blindly replayed. Every `POST` and `PUT` in A request that changes state is not blindly replayed. Every `POST` and `PUT` in
the endpoint list under "HTTP API" goes through this rule. Verifying a second the endpoint list under "HTTP API" goes through this rule. Verifying a second
@@ -1402,13 +1407,13 @@ that rule although they look like connect-time failures. On Linux they are also
delivered on an established socket, after the request may have been written. delivered on an established socket, after the request may have been written.
They stay retryable for requests that change nothing. Bun's `ConnectionRefused` They stay retryable for requests that change nothing. Bun's `ConnectionRefused`
and `FailedToOpenSocket` are treated the same way: a request that changes and `FailedToOpenSocket` are treated the same way: a request that changes
nothing is retried on them, and a `POST` or `PUT` is not replayed on them. When nothing is retried on them, and a `POST` or `PUT` is not replayed on them. Bun
a connection bun kept open from an earlier request closes before any answer, bun reuses a connection it kept open from an earlier request. When that connection
sends the request again on a new connection. If that one fails, bun gives one of closes before any answer, bun sends the request again on a new one. If that one
these codes although the server may already have the request. The upload of a fails, bun gives one of these codes although the server may already have the
thumbnail to its presigned URL is exempt from the rule. It stores one whole request. The upload of a thumbnail to its presigned URL is exempt from the rule.
object at one key in one request, so replaying it has no partial state to It stores one whole object at one key in one request, so replaying it has no
damage. partial state to damage.
A download is retried as a whole: the request, the reading of the body and the A download is retried as a whole: the request, the reading of the body and the
decryption. A socket reset after the response headers have arrived surfaces in decryption. A socket reset after the response headers have arrived surfaces in
@@ -1446,15 +1451,15 @@ working client from that snapshot without logging in again. It checks every
field and each key's length first, and throws an error naming the bad field. field and each key's length first, and throws an error naming the bad field.
`Client.fromJSON` works only once libsodium, the cryptography library quak uses, `Client.fromJSON` works only once libsodium, the cryptography library quak uses,
is loaded. Only `Client.login` loads it. A program that restores a session in a is loaded. `Client.login` loads it, and `Client.fromJSON` does not. A program
new process first runs `await sodium.ready`, with `sodium` imported from that restores a session in a new process first runs `await sodium.ready`, with
`libsodium-wrappers-sumo`, which quak depends on. The exported `beginLogin`, `sodium` imported from `libsodium-wrappers-sumo`, which quak depends on. The
`unwrapAuth`, `decryptCollection`, `decryptFile`, `downloadFile` and exported `beginLogin`, `unwrapAuth`, `decryptCollection`, `decryptFile`,
`downloadThumbnail` need the same. `downloadFile` and `downloadThumbnail` need the same.
`client.logout()` clears the token and zeroes the key buffers in place. After `client.logout()` clears the token and zeroes the key buffers in place. After
it, every other method on that client throws. It does not contact the server, so it, every other method on that client throws. `logout()` does not contact the
the token stays valid there and in any saved snapshot. Call server, so the token stays valid there and in any saved snapshot. Call
`await client.logoutOnServer()` before `logout()` to end the session on the `await client.logoutOnServer()` before `logout()` to end the session on the
server. server.
@@ -1907,7 +1912,9 @@ These apply to people and to coding agents alike.
- Not pinned: - Not pinned:
- bun, which `make build-bin` takes from `nix-shell -p bun`; - bun, which `make build-bin` takes from `nix-shell -p bun`;
- `make` and `git`, which `script/bootstrap` installs from the system - `make` and `git`, which `script/bootstrap` installs from the system
package manager when they are missing, as every `Dockerfile` stage does. package manager when they are missing, as every `Dockerfile` stage does;
- `bash` and `curl`, which `script/bootstrap` installs the same way when it
has to install Node.
- Follow the workflow above: branches off `next`, tests first and committed - Follow the workflow above: branches off `next`, tests first and committed
failing, and `next` merged into `main` by sneak alone. failing, and `next` merged into `main` by sneak alone.
- Before every commit, `make lint` and `make fmt-check` must pass. `make lint` - Before every commit, `make lint` and `make fmt-check` must pass. `make lint`
+10 -7
View File
@@ -1,15 +1,18 @@
// The CLI's log level, and the one function the commands write stderr lines // The CLI's log level, and the one function the commands write stderr lines
// through. // through.
// //
// Every line quak prints on stderr is a log line at one of six levels, from // Every line quak logs goes to stderr, at one of six levels, from the
// the quietest: fatal, error, warning, info, debug, trace. A run prints the // quietest: fatal, error, warning, info, debug, trace. A run prints the lines
// lines at its own level and at every quieter one. Its level is info unless // at its own level and at every quieter one. Its level is info unless `-v` or
// `-v` or `-q` asks for another (see "What quak prints" in the README). Every // `-q` asks for another (see "What quak prints" in the README). A command that
// command that exits non-zero logs a fatal line saying why, so even `-q`, which // runs and exits non-zero logs a fatal line saying why, so even `-q`, which
// prints only fatal lines, never leaves a non-zero exit unexplained. An error // prints only fatal lines, never leaves such an exit unexplained. An error
// line reports something that failed, such as a file or an album, and a // line reports something that failed, such as a file or an album, and a
// warning something that did not fail but needs the user's attention. stdout // warning something that did not fail but needs the user's attention. stdout
// carries only a command's output and is not a log. // carries a command's output and the prompts of `login`, and is not a log.
// Two things on stderr are not log lines: the `error:` message for a
// command line that quak cannot parse, and the usage printed when no command
// is given.
// //
// The library itself writes no lines: its layers report through callbacks, and // The library itself writes no lines: its layers report through callbacks, and
// the CLI logs what they report. `logRequests` gives the API client options // the CLI logs what they report. `logRequests` gives the API client options
+1 -1
View File
@@ -328,7 +328,7 @@ describe("what the library reports", () => {
]); ]);
}); });
// Each operation is named in plain words, as the README names it. // Each operation is named in plain words.
it("logs a refresh's progress at info, and its start and end at debug", () => { it("logs a refresh's progress at info, and its start and end at debug", () => {
const { log, lines } = collect(); const { log, lines } = collect();
const onProgress = logRefresh(log); const onProgress = logRefresh(log);