verifika 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses [Semantic Versioning](https://semver.org/). The CLI flags, exit codes, configuration format (`version: 1`) and JSON output (`schemaVersion: 1`) are the public API.
4
4
 
5
+ ## [0.0.3] - 2026-10-13
6
+
7
+ ### Added
8
+
9
+ - Live status block in `human` mode on an interactive terminal: progress and elapsed time, each running check with a spinner, its elapsed time and latest output line, and what queued checks wait on. Finished checks print above it as in line mode. Line output remains the fallback in CI, when piped, with `TERM=dumb`, and with the new `--no-live` flag or `VERIFIKA_LIVE=0`.
10
+ - `human` output: a `verifika · <selection> · <n> checks · <j> parallel` header, aligned durations, the command and a rerun hint (`vfk -c <check>`) on failure lines, a `○ not run: build failed` marker for checks blocked by a failed dependency, and an estimate of the time saved by skipped checks from their last recorded durations.
11
+ - In `human` mode, the first interrupt prints `stopping… (Ctrl-C again to force)`.
12
+ - `vfk logs` on a terminal: local and relative start time (`today 01:12, 3m ago`), clickable OSC 8 log paths (opt out with `VERIFIKA_HYPERLINKS=0`), and paging through `$PAGER` or `less -R -F -X` (`--no-pager` to opt out; never with `--tail`, when piped, or in `agent` mode).
13
+ - `vfk plan` in `human` mode colours `run` and dims skipped checks.
14
+ - `vfk init` adds `"verify": "vfk"` and `"verify:ci": "vfk --all"` to `package.json` when they are absent, keeping its indentation, line endings, key order and trailing newline; existing scripts are never overwritten.
15
+ - JSON (additive, `schemaVersion: 1`): `checks[].blockedBy` on blocked checks in run results, naming the dependency that did not pass; `checks[].requiredBy` in plan output on checks selected only as dependencies.
16
+
17
+ ### Changed
18
+
19
+ - **`npm run verify` is now selective.** The recommended scripts are `"verify": "vfk"` for local work and `"verify:ci": "vfk --all"` for the full gate; use `npm run verify:ci` (or `vfk --all`) in CI and before delivery. The README setup prompt, docs and agent skill say so, and the prompt only replaces an existing `verify` script when the configured checks cover what it ran. If you followed the 0.0.2 setup, your `"verify": "vfk --all"` script still runs the full gate; to adopt the split, rename it to `verify:ci` and add `"verify": "vfk"` by hand (`vfk init` only adds scripts when it creates the config).
20
+ - A check pulled in by several dependents now names all of them (`required by test, pack`), in plan output and reasons.
21
+ - Failure previews leave out package-manager script banners (`> app@1.0.0 lint`, `> eslint .`, yarn's `$ …`).
22
+
23
+ ### Fixed
24
+
25
+ - ESLint failure previews showed only the `✖ N problems` summary; they now keep each `line:col error` line under its file heading, plus the summary.
26
+ - A failed check whose preview was hidden by the three-preview limit looked as if nothing was found; its line now says `(preview omitted: vfk logs <check>)`, in `human` and `agent` output.
27
+
28
+ ## [0.0.2] - 2026-10-12
29
+
30
+ ### Changed
31
+
32
+ - `vfk` is now the advertised command: `--help`, run hints (`Run: vfk logs <check>`), error hints, the README, the docs and the agent skill use it. `verifika` keeps working and is identical.
33
+
34
+ ### Added
35
+
36
+ - README: a copy-paste prompt that has a coding agent install Verifika, write and validate a config, and install the skill.
37
+
5
38
  ## [0.0.1] - 2026-10-12
6
39
 
7
40
  First public release.
@@ -27,4 +60,6 @@ First public release.
27
60
  - A process that deliberately detaches from the check (on Linux and macOS: leaves the process group, for example via `setsid`; on Windows: breaks away from the job with an exited intermediate parent) is not tracked unless it keeps the check's output open, in which case the check is reported as `error`.
28
61
  - On Windows, confirming cleanup queries the process table with PowerShell after each check, which costs roughly 0.5–3 s per check.
29
62
 
63
+ [0.0.3]: https://github.com/alexportelli12/verifika/compare/v0.0.2...v0.0.3
64
+ [0.0.2]: https://github.com/alexportelli12/verifika/releases/tag/v0.0.2
30
65
  [0.0.1]: https://github.com/alexportelli12/verifika/releases/tag/v0.0.1
package/README.md CHANGED
@@ -4,56 +4,101 @@
4
4
  [![Release](https://img.shields.io/github/v/release/alexportelli12/verifika)](https://github.com/alexportelli12/verifika/releases/latest)
5
5
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
6
 
7
- Run the checks your changes need, concurrently, with minimal output.
7
+ **Run only the checks your change needs. Get one line back when they pass.**
8
8
 
9
- Verifika is a small CLI for repository verification. You declare your checks (lint, typecheck, tests, build, ...) in `verifika.config.json`. Locally, Verifika runs only the checks affected by your changes; in CI it runs everything. Successful runs print one line, failures print exactly what failed and how to see the full logs. It works the same for developers, CI pipelines and AI coding agents (Claude Code, Codex, OpenCode, and others).
9
+ Every repository has a verification gate: format, lint, typecheck, tests, build. Using it well is harder than it looks:
10
+
11
+ - **Running everything is slow.** A one-line fix waits minutes for checks it cannot affect, so people (and agents) skip the gate or batch changes until the end.
12
+ - **Picking checks by hand is unreliable.** It is easy to forget the one that breaks, and "it passed" stops meaning much.
13
+ - **Failures are buried in noise.** A test runner can print tens of thousands of lines to report one failed assertion. A coding agent pays for every line in tokens and context, and often never reaches the line that matters.
14
+
15
+ Verifika fixes this with one config file and one command. You list your checks in `verifika.config.json`, with the files each one covers. Then `vfk` runs only the checks your changes affect, CI runs all of them, and the output says exactly what failed:
10
16
 
11
17
  ```text
12
- $ verifika
18
+ $ vfk
13
19
  PASS 3/3 (4.1s) selective: 2 files changed since HEAD (a1b2c3d); 2 skipped
14
20
 
15
- $ verifika --all
21
+ $ vfk --all
16
22
  FAIL 4/5 (12s) full run (--all)
17
23
  typecheck: exit 2
18
24
  src/cart.ts(14,7): error TS2322: Type 'string' is not assignable to type 'number'.
19
- Run: verifika logs typecheck
25
+ Run: vfk logs typecheck
20
26
  ```
21
27
 
22
- _Verifika_ is Maltese for "verification".
28
+ ## Why Verifika
23
29
 
24
- ## Why
30
+ - **Small output, especially for agents.** On a real Angular project, one failing test produced 94,900 bytes of output with the project's previous verify script (~23,700 tokens). Verifika reported the same failure in 369 bytes (~90 tokens): the test name, the assertion and the totals. Full logs stay on disk until you ask for them.
31
+ - **Less waiting.** Locally, only affected checks run: in a 25-check monorepo, a typical edit ran 1–13 of them. Checks that share no build outputs can also run in parallel; on that Angular project, four at a time cut the full gate from 95.5 s to 61.7 s. [Measurements](docs/benchmarks.md).
32
+ - **Deterministic and conservative.** Selection comes from your config, file globs, dependencies and git; no heuristics, no LLM. When the scope of a change is unclear, Verifika runs everything or fails loudly. It never quietly skips a check, and in CI it runs the full suite unless you explicitly ask for a selective run.
33
+ - **Trustworthy results.** Timeouts, fail-fast, correct exit codes and signal handling. A check only passes once its whole process tree has exited. Tested on Linux, macOS and Windows.
34
+ - **Built for humans and coding agents alike.** Live progress on a terminal, one line in agent sessions, JSON for scripts. It ships an [agent skill](skills/verifika/SKILL.md) for Claude Code, Codex, OpenCode and others.
35
+ - **Small.** One JSON file, one runtime dependency, no daemon, no cache to invalidate.
25
36
 
26
- - **Deterministic.** Check selection comes from your config, file globs, dependencies and git. No heuristics, no LLM. When the change scope is unclear, Verifika runs everything or fails loudly; it never quietly skips a check.
27
- - **Token-efficient.** Agents get one line on success and a bounded failure preview on failure. Full logs stay on disk until asked for.
28
- - **Fast.** Selective runs skip unaffected checks, and independent checks can run in parallel once you opt in (`defaults.concurrency`). One small runtime dependency.
29
- - **Reliable.** Timeouts, fail-fast, process cleanup, correct exit codes and signal handling, tested on Linux, macOS and Windows.
37
+ _Verifika_ is Maltese for "verification". The command is `vfk`, short for `verifika`; both are installed and identical.
30
38
 
31
- ## Install
39
+ ## Get started
32
40
 
33
41
  Requires Node.js 22.13 or newer.
34
42
 
35
- ```sh
36
- npm install --save-dev verifika
37
- ```
43
+ ### With a coding agent
38
44
 
39
- The package installs two commands: `verifika` and its shorthand `vfk`. They are the same program, so use whichever you prefer:
45
+ Paste this prompt into Claude Code, Codex, OpenCode or another coding agent, from your repository root. It installs Verifika, writes and validates a config for your project, and reports what it found.
40
46
 
41
- ```sh
42
- npx verifika plan # same as: npx vfk plan
43
- npx vfk --all # same as: npx verifika --all
47
+ ```text
48
+ Set up Verifika (https://github.com/alexportelli12/verifika) in this repository.
49
+
50
+ 1. Check that Node.js is 22.13 or newer, and install the project's dependencies if
51
+ they are missing. Install Verifika as a dev dependency with this repository's
52
+ package manager (npm install --save-dev verifika, or the pnpm/yarn equivalent).
53
+ It provides the `vfk` command.
54
+ 2. Run `npx vfk init` to create verifika.config.json, then refine it by reading
55
+ package.json, the CI workflows and the tool configs:
56
+ - One check per quality gate the project actually has (such as format, lint,
57
+ typecheck, unit tests, build). Do not invent checks for tools it does not use,
58
+ or add a check that another one already fully covers. Every command must be
59
+ non-interactive and exit on its own: no watch mode, no prompts, no dev servers.
60
+ - Give each check `paths` globs for the files that can affect it, so local runs
61
+ select only what a change needs. If unsure, leave `paths` out: the check then
62
+ runs on every change.
63
+ - Add `dependsOn` where one check needs another's output (for example e2e after build).
64
+ - Keep concurrency at 1 unless the checks clearly share no build outputs.
65
+ - Make sure package.json has two scripts: "verify": "vfk" (selective, for local
66
+ work) and "verify:ci": "vfk --all" (the full gate). `vfk init` adds them when
67
+ they are missing. If a "verify" or "verify:ci" script already exists, replace
68
+ it only when the configured checks cover everything it ran; otherwise leave it
69
+ and report the conflict. This is the one exception to the rule below about
70
+ existing scripts.
71
+ - If the CI workflow runs the gate some other way, report that switching it to
72
+ `npm run verify:ci` is recommended. Do not edit the workflow.
73
+ Reference: https://github.com/alexportelli12/verifika/blob/main/docs/configuration.md
74
+ 3. Validate the config:
75
+ - Run `npx vfk --all`. If a check fails, decide whether the config is wrong (fix it)
76
+ or the project already fails that check (report it; do not fix it).
77
+ - Run `npx vfk plan`. Until this setup is committed, the changed package.json and
78
+ lockfile select every check; that is expected. Explain which checks a typical
79
+ source edit and a documentation-only edit would select once it is committed.
80
+ 4. Install the agent skill: copy node_modules/verifika/skills/verifika into this
81
+ agent's skills directory (see
82
+ https://github.com/alexportelli12/verifika/blob/main/docs/agents.md).
83
+
84
+ Do not change source code or existing scripts to make checks pass (except "verify"
85
+ and "verify:ci", as step 2 describes), and do not commit.
86
+ Finish with: the checks you configured and why, the --all result, the expected
87
+ selection for those two edits, the scripts you added, replaced or left (with any
88
+ conflicts), any CI recommendation, and any existing failures you found.
44
89
  ```
45
90
 
46
- The docs use `verifika` throughout. To install it globally instead, run `npm install --global verifika`; to try it without installing, run `npx verifika --help`.
91
+ Review the config it writes before committing it: the commands in it run on every verification.
47
92
 
48
- ## Quick start
49
-
50
- Generate a starter config from your `package.json` scripts:
93
+ ### By hand
51
94
 
52
95
  ```sh
53
- npx verifika init
96
+ npm install --save-dev verifika
97
+ npx vfk init # creates verifika.config.json from your package.json scripts,
98
+ # and adds "verify" / "verify:ci" scripts if they are missing
54
99
  ```
55
100
 
56
- Or write `verifika.config.json` yourself:
101
+ `init` only maps conventional script names (`lint`, `test`, `build`, ...) and leaves every check unfiltered, so open the file and give each check `paths`:
57
102
 
58
103
  ```json
59
104
  {
@@ -72,46 +117,50 @@ Or write `verifika.config.json` yourself:
72
117
  }
73
118
  ```
74
119
 
75
- If your repository checks the formatting of every file (for example `prettier --check .`), format the new `verifika.config.json` too.
76
-
77
- Then:
120
+ If your repository checks the formatting of every file (for example `prettier --check .`), format the new `verifika.config.json` too. Then:
78
121
 
79
122
  ```sh
80
- npx verifika plan # which checks would run for your changes, and why
81
- npx verifika # run them
82
- npx verifika --all # run everything (do this before you push)
123
+ npx vfk plan # which checks would run for your changes, and why
124
+ npx vfk # run them
125
+ npx vfk --all # run everything (do this before you push)
83
126
  ```
84
127
 
85
- Add a script so everyone runs the same full gate:
128
+ `init` adds two scripts unless package.json already has scripts with those names (it never overwrites them):
86
129
 
87
130
  ```json
88
- { "scripts": { "verify": "verifika --all" } }
131
+ { "scripts": { "verify": "vfk", "verify:ci": "vfk --all" } }
89
132
  ```
90
133
 
134
+ `npm run verify` runs the checks your changes affect, for quick feedback while you work. `npm run verify:ci` runs every check: it is the full gate, so use it in CI and before you push or open a pull request.
135
+
136
+ Run logs go to `.verifika/`, which ignores itself in git, so you do not need to edit `.gitignore`. To install globally instead, run `npm install --global verifika`.
137
+
91
138
  ## How checks are selected
92
139
 
93
- | Situation | What runs |
94
- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
95
- | `verifika` locally | Checks whose `paths` match files changed since `changes.base` (default `HEAD`: your uncommitted and untracked work) |
96
- | `verifika` in CI (`CI` or a known CI variable) | Everything; only `--changed`/`--changed-since` on the command line can narrow it |
97
- | `verifika --all` | Everything |
98
- | `verifika --check lint` | `lint` and its dependencies |
99
- | `verifika --changed-since origin/main` | Checks matching files changed since `merge-base(origin/main, HEAD)`, plus uncommitted and untracked files |
100
- | A changed file matches `invalidateAll` | Everything |
101
- | Git unavailable, no commits, unknown base | Everything, with the reason printed (or an error, when you asked for `--changed`/`--changed-since` explicitly) |
140
+ | Situation | What runs |
141
+ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
142
+ | `vfk` locally | Checks whose `paths` match files changed since `changes.base` (default `HEAD`: your uncommitted and untracked work) |
143
+ | `vfk` in CI (`CI` or a known CI variable) | Everything; only `--changed`/`--changed-since` on the command line can narrow it |
144
+ | `vfk --all` | Everything |
145
+ | `vfk --check lint` | `lint` and its dependencies |
146
+ | `vfk --changed-since origin/main` | Checks matching files changed since `merge-base(origin/main, HEAD)`, plus uncommitted and untracked files |
147
+ | A changed file matches `invalidateAll` | Everything |
148
+ | Git unavailable, no commits, unknown base | Everything, with the reason printed (or an error, when you asked for `--changed`/`--changed-since` explicitly) |
102
149
 
103
- A check without `paths` runs whenever any file changed; `"always": true` runs it even when nothing changed. Selecting a check also selects its `dependsOn` checks. `verifika plan` shows every decision.
150
+ A check without `paths` runs whenever any file changed; `"always": true` runs it even when nothing changed. Selecting a check also selects its `dependsOn` checks. `vfk plan` shows every decision.
104
151
 
105
152
  ## Output modes
106
153
 
107
154
  The default is `human` on an interactive terminal and `agent` otherwise. Override with `--mode` or `VERIFIKA_MODE`.
108
155
 
109
- | Mode | Success | Failure |
110
- | ------- | ----------------------------- | --------------------------------------------------- |
111
- | `human` | Live progress, summary | Failing checks, previews, logs hint |
112
- | `agent` | One line: `PASS 4/4 (4.3s) …` | One line per problem, bounded preview, logs command |
113
- | `json` | Versioned result document | Same document, `"status": "failed"` |
114
- | `quiet` | Nothing | One line on stderr |
156
+ | Mode | Success | Failure |
157
+ | ------- | ----------------------------- | --------------------------------------------------------------- |
158
+ | `human` | Live status block, summary | Failing checks with command and rerun hint, previews, logs hint |
159
+ | `agent` | One line: `PASS 4/4 (4.3s) …` | One line per problem, bounded preview, logs command |
160
+ | `json` | Versioned result document | Same document, `"status": "failed"` |
161
+ | `quiet` | Nothing | One line on stderr |
162
+
163
+ In `human` mode on an interactive terminal, a live status block shows progress, each running check with its latest output line, and what the queued checks wait on; finished checks print above it. In CI, when piped, with `TERM=dumb`, or with `--no-live` / `VERIFIKA_LIVE=0`, `human` mode prints plain lines instead (one per check start and end, plus a note every 30 seconds for long checks), which read well in logs. `NO_COLOR` turns colour off in both.
115
164
 
116
165
  Exit codes: `0` passed (or nothing to verify), `1` verification failed, `2` usage or configuration error, `128+n` interrupted by a signal (`130` SIGINT, `143` SIGTERM).
117
166
 
@@ -120,10 +169,10 @@ Exit codes: `0` passed (or nothing to verify), `1` verification failed, `2` usag
120
169
  Every run stores each check's combined stdout/stderr under `.verifika/runs/<run-id>/`. The directory ignores itself in git, and only the last 10 runs are kept (`defaults.keepRuns`).
121
170
 
122
171
  ```sh
123
- verifika logs # latest run: statuses and log paths
124
- verifika logs typecheck # full log of one check
125
- verifika logs --failed # logs of every failed check
126
- verifika logs test --tail 50 # last 50 lines
172
+ vfk logs # latest run: statuses and log paths
173
+ vfk logs typecheck # full log of one check
174
+ vfk logs --failed # logs of every failed check
175
+ vfk logs test --tail 50 # last 50 lines
127
176
  ```
128
177
 
129
178
  Logs contain whatever your commands print, which can include secrets. They never leave your machine; keep `.verifika/` out of anything you publish.
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { ExitCode } from "./errors.js";
3
3
  import { main } from "./main.js";
4
+ import { systemTerminal } from "./reporters/live.js";
4
5
  const write = (stream) => (text) => {
5
6
  stream.write(text);
6
7
  };
@@ -17,6 +18,9 @@ main(process.argv.slice(2), {
17
18
  isTTY: Boolean(process.stdout.isTTY),
18
19
  out: write(process.stdout),
19
20
  err: write(process.stderr),
21
+ ...(process.stdout.isTTY
22
+ ? { terminal: systemTerminal(write(process.stdout), write(process.stderr), process.stdout) }
23
+ : {}),
20
24
  }).then((code) => {
21
25
  process.exitCode = code;
22
26
  }, (error) => {
package/dist/config.js CHANGED
@@ -30,7 +30,7 @@ export function findConfigFile(cwd, explicit) {
30
30
  return candidate;
31
31
  const parent = path.dirname(directory);
32
32
  if (parent === directory) {
33
- throw new VerifikaError(`no ${CONFIG_FILE} found in ${cwd} or any parent directory`, "Create one with: verifika init");
33
+ throw new VerifikaError(`no ${CONFIG_FILE} found in ${cwd} or any parent directory`, "Create one with: vfk init");
34
34
  }
35
35
  directory = parent;
36
36
  }
package/dist/init.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { writeFile } from "node:fs/promises";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { CONFIG_FILE } from "./config.js";
5
5
  import { VerifikaError } from "./errors.js";
@@ -14,6 +14,44 @@ const CANDIDATES = [
14
14
  ["build", ["build"]],
15
15
  ];
16
16
  const NPM_PLACEHOLDER_TEST = /no test specified/;
17
+ /** npm scripts `vfk init` adds: selective verification while working, the full gate for CI. */
18
+ export const VERIFY_SCRIPTS = {
19
+ verify: "vfk",
20
+ "verify:ci": "vfk --all",
21
+ };
22
+ /**
23
+ * Adds the `verify` and `verify:ci` scripts to package.json where they are
24
+ * absent; existing scripts are never overwritten. Keeps the file's indentation,
25
+ * line endings, key order and trailing newline.
26
+ */
27
+ export async function addVerifyScripts(root) {
28
+ const file = path.join(root, "package.json");
29
+ const raw = await readFile(file, "utf8");
30
+ const bom = raw.startsWith("\uFEFF") ? "\uFEFF" : "";
31
+ const text = raw.slice(bom.length);
32
+ const manifest = JSON.parse(text);
33
+ const scripts = (manifest.scripts ??= {});
34
+ const result = { added: [], kept: [] };
35
+ for (const [name, command] of Object.entries(VERIFY_SCRIPTS)) {
36
+ const existing = scripts[name];
37
+ if (existing === undefined) {
38
+ scripts[name] = command;
39
+ result.added.push(name);
40
+ }
41
+ else {
42
+ result.kept.push({ name, command: existing });
43
+ }
44
+ }
45
+ if (result.added.length === 0)
46
+ return result;
47
+ // A one-line manifest stays on one line.
48
+ const indent = text.trim().includes("\n") ? (/^[ \t]+(?=")/m.exec(text)?.[0] ?? " ") : "";
49
+ const newline = text.includes("\r\n") ? "\r\n" : "\n";
50
+ const trailing = /\r?\n$/.test(text) ? newline : "";
51
+ const json = JSON.stringify(manifest, null, indent).replace(/\n/g, newline);
52
+ await writeFile(file, `${bom}${json}${trailing}`);
53
+ return result;
54
+ }
17
55
  /**
18
56
  * Writes a starter verifika.config.json from the package.json scripts in `root`.
19
57
  * Checks are created without path filters (they run on any change); narrowing
package/dist/logs.js CHANGED
@@ -2,7 +2,9 @@ import { createReadStream } from "node:fs";
2
2
  import { createInterface } from "node:readline";
3
3
  import { open } from "node:fs/promises";
4
4
  import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
5
6
  import { VerifikaError } from "./errors.js";
7
+ import { withPager } from "./pager.js";
6
8
  import { duration, isFailureStatus, selectionLabel } from "./reporters/reporter.js";
7
9
  import { plain, symbol } from "./reporters/human.js";
8
10
  import { stripAnsi } from "./preview.js";
@@ -15,7 +17,7 @@ import { readRun } from "./runs.js";
15
17
  export async function showLogs(request, output) {
16
18
  const run = await readRun(request.root, request.runId);
17
19
  if (request.checks.length === 0 && !request.failed) {
18
- output.out(renderRunSummary(run));
20
+ output.out(renderRunSummary(run, request.display));
19
21
  return 0;
20
22
  }
21
23
  let targets;
@@ -35,20 +37,29 @@ export async function showLogs(request, output) {
35
37
  targets = request.checks.map((name) => byName.get(name));
36
38
  }
37
39
  const headers = request.failed || targets.length > 1;
38
- for (const check of targets) {
39
- if (headers)
40
- output.out(`── ${check.name}: ${check.status} (${check.reason}) ──\n`);
41
- if (!check.log) {
42
- output.out(`${check.name} has no log: ${check.status} (${check.reason})\n`);
43
- continue;
40
+ const print = async (write) => {
41
+ for (const check of targets) {
42
+ if (headers)
43
+ await write(`── ${check.name}: ${check.status} (${check.reason}) ──\n`);
44
+ if (!check.log) {
45
+ await write(`${check.name} has no log: ${check.status} (${check.reason})\n`);
46
+ continue;
47
+ }
48
+ await printLog(path.join(request.root, check.log), request, write);
44
49
  }
45
- await printLog(path.join(request.root, check.log), request, output);
50
+ };
51
+ if (request.pager !== undefined && request.tail === undefined) {
52
+ await withPager(request.pager, request.env ?? process.env, print);
53
+ }
54
+ else {
55
+ await print((text) => output.out(text));
46
56
  }
47
57
  return 0;
48
58
  }
49
- export function renderRunSummary(run) {
59
+ export function renderRunSummary(run, display) {
60
+ const started = display ? describeTime(new Date(run.startedAt), display.now) : run.startedAt;
50
61
  const lines = [
51
- `Run ${run.runId} · ${run.status} · ${duration(run.durationMs)} · ${run.startedAt}`,
62
+ `Run ${run.runId} · ${run.status} · ${duration(run.durationMs)} · ${started}`,
52
63
  selectionLabel(run.selection, run.summary.skipped),
53
64
  ];
54
65
  if (run.status === "running")
@@ -60,8 +71,9 @@ export function renderRunSummary(run) {
60
71
  parts.push(duration(check.durationMs).padStart(6));
61
72
  if (check.status !== "passed")
62
73
  parts.push(check.reason);
63
- if (check.log)
64
- parts.push(check.log);
74
+ if (check.log) {
75
+ parts.push(display?.hyperlinks ? hyperlink(check.log, path.join(run.root, check.log)) : check.log);
76
+ }
65
77
  lines.push(parts.join(" "));
66
78
  }
67
79
  // One line for everything not selected; large configs would otherwise list dozens.
@@ -72,12 +84,41 @@ export function renderRunSummary(run) {
72
84
  }
73
85
  return `${lines.join("\n")}\n`;
74
86
  }
75
- async function printLog(file, { tail, stripAnsi: strip = false }, output) {
87
+ /**
88
+ * `today 01:12, 3m ago`; `yesterday 23:40, 2h ago`; otherwise the local date.
89
+ * Local time, because that is what the user's clock shows.
90
+ */
91
+ export function describeTime(time, now) {
92
+ const pad = (value) => String(value).padStart(2, "0");
93
+ const clock = `${pad(time.getHours())}:${pad(time.getMinutes())}`;
94
+ const day = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
95
+ const days = Math.round((day(now) - day(time)) / 86_400_000);
96
+ const date = days === 0
97
+ ? "today"
98
+ : days === 1
99
+ ? "yesterday"
100
+ : `${time.getFullYear()}-${pad(time.getMonth() + 1)}-${pad(time.getDate())}`;
101
+ return `${date} ${clock}, ${ago(now.getTime() - time.getTime())}`;
102
+ }
103
+ function ago(ms) {
104
+ if (ms < 60_000)
105
+ return ms < 1_000 ? "just now" : `${Math.floor(ms / 1000)}s ago`;
106
+ if (ms < 3_600_000)
107
+ return `${Math.floor(ms / 60_000)}m ago`;
108
+ if (ms < 86_400_000)
109
+ return `${Math.floor(ms / 3_600_000)}h ago`;
110
+ return `${Math.floor(ms / 86_400_000)}d ago`;
111
+ }
112
+ /** An OSC 8 terminal hyperlink to a local file; terminals without support show just the text. */
113
+ export function hyperlink(text, file) {
114
+ return `\u001b]8;;${pathToFileURL(file).href}\u001b\\${text}\u001b]8;;\u001b\\`;
115
+ }
116
+ async function printLog(file, { tail, stripAnsi: strip = false }, write) {
76
117
  try {
77
118
  if (tail !== undefined) {
78
119
  const text = await readTail(file, tail);
79
120
  const shown = strip ? stripAnsi(text) : text;
80
- output.out(shown.endsWith("\n") || shown === "" ? shown : `${shown}\n`);
121
+ await write(shown.endsWith("\n") || shown === "" ? shown : `${shown}\n`);
81
122
  return;
82
123
  }
83
124
  if (strip) {
@@ -87,16 +128,16 @@ async function printLog(file, { tail, stripAnsi: strip = false }, output) {
87
128
  crlfDelay: Infinity,
88
129
  });
89
130
  for await (const line of lines)
90
- output.out(`${stripAnsi(line)}\n`);
131
+ await write(`${stripAnsi(line)}\n`);
91
132
  return;
92
133
  }
93
134
  let last = "";
94
135
  for await (const chunk of createReadStream(file, { encoding: "utf8" })) {
95
- output.out(chunk);
136
+ await write(chunk);
96
137
  last = chunk;
97
138
  }
98
139
  if (last !== "" && !last.endsWith("\n"))
99
- output.out("\n");
140
+ await write("\n");
100
141
  }
101
142
  catch (error) {
102
143
  throw new VerifikaError(`cannot read log ${file}: ${error.message}`);