A flat-YAML task tracker that is just files. No daemon, no database, no git hooks.
diarie.dev
issue-tracker
cli
nodejs
agent-friendly
1#!/usr/bin/env node
2
3/**
4 * The error boundary. All logic lives in lib/.
5 *
6 * Exit codes carry meaning, and one of them is the whole point of this tool:
7 *
8 * 0 the answer is on stdout
9 * 1 InputError — you got it wrong. Most importantly: THERE IS NO STORE HERE.
10 * 2 ResultError — it ran, and the answer is "no" (invalid store, --strict)
11 *
12 * NOTHING ELSE MAY USE 2. peowly's `showHelp()` defaults to `exit(2)` for "incorrect
13 * usage", which would collide head-on with ResultError and leave a CI job unable to tell
14 * a dependency cycle from a typo. `main.js` therefore intercepts the bare invocation, the
15 * unknown command, and the unknown flag, and re-throws each as an InputError carrying the
16 * code `EUSAGE` — so a machine consumer can tell "you typed it wrong" (EUSAGE) from
17 * "there is no store here" (ENOSTORE) without regexing a human sentence.
18 *
19 * Under `--json`, an InputError is emitted as JSON on STDOUT, not as prose on
20 * stderr. That is deliberate and it is the fix for the defect this CLI was built
21 * around: the old readers printed `{"ready": []}` to stdout and their only
22 * complaint to stderr — a stream that ten call sites pipe to /dev/null — so a
23 * tracker that could not find its store was indistinguishable from one reporting
24 * an empty backlog. Anything a caller must not miss goes to stdout, and shows up
25 * in the exit code. Nothing important is whispered.
26 */
27
28import process, { argv, stderr } from 'node:process';
29
30import { messageWithCauses, stackWithCauses } from 'pony-cause';
31
32import { cli } from './lib/main.js';
33import { InputError, ResultError } from './lib/utils/errors.js';
34import { exitResultError } from './lib/utils/exit.js';
35
36/** True if the user asked for machine-readable output. */
37const wantsJson = argv.includes('--json') || argv.includes('-j');
38
39try {
40 await cli(argv.slice(2));
41} catch (err) {
42 // AN IF/ELSE CHAIN, AND `process.exitCode` — NOT `process.exit()`. Both halves are load-bearing.
43 //
44 // `process.exit()` does not flush a pending write to a PIPE (stdout to a pipe is async, and the
45 // kernel buffer is 64 KB). So writing the answer and then exiting truncated any payload over
46 // ~64 KB into unparseable JSON — a `--json` consumer's `jq` fails, it falls back to "no data",
47 // and a broken store reads as an empty one. The founding defect, re-entered through the exit code
48 // that reports it. Setting `exitCode` lets Node drain stdout and exit naturally.
49 //
50 // Which means these handlers no longer TERMINATE — so they must not fall through. They used to be
51 // three sequential `if`s that each ended in `exit()`; drop the exit and a ResultError would sail
52 // on into the "genuinely unexpected" branch and be answered with a stack trace.
53 if (err instanceof ResultError) {
54 // The command has already said its piece on stdout (validate printed the errors).
55 // Adding a second, vaguer complaint here would just be noise.
56 //
57 // `exitResultError()` rather than a bare `exit(2)`: the code is reserved, and the name is what
58 // reserves it. See lib/utils/exit.js — it is the only file allowed to write the number.
59 exitResultError();
60 } else if (err instanceof InputError) {
61 if (wantsJson) {
62 // On stdout, WITH a code, so a machine consumer can branch without regexing a human
63 // sentence: ENOSTORE (no store here), EUSAGE (you typed it wrong), EEXIST (init, refusing
64 // an existing store).
65 //
66 // `err.code` DIRECTLY — not through `isErrorWithCode`, which used to guard this line.
67 // `err` is already narrowed to InputError, whose `code` is OUR field and is correctly typed
68 // `string|undefined`. Reaching for a foreign predicate to read it was pure downside: that
69 // predicate is `value instanceof Error && 'code' in value` — a PRESENCE check whose type
70 // signature nonetheless promises `code: string` — so it narrowed nothing here (an InputError
71 // always has the key) while licensing a non-string `code` straight into the JSON contract.
72 //
73 // The same unsound guard crashed main.js on three user-error paths. This was its sibling
74 // site, and hardening only the one that had already blown up would have been fixing the
75 // instance and leaving the class.
76 const { code } = err;
77 process.stdout.write(JSON.stringify({ error: err.message, ...(code ? { code } : {}) }, undefined, 2) + '\n');
78 } else {
79 stderr.write(`diarie: ${err.message}\n`);
80 if (err.body) stderr.write('\n' + err.body + '\n');
81 }
82 process.exitCode = 1;
83 } else {
84 // Genuinely unexpected: a bug, not a user mistake. Show the whole cause chain —
85 // this is the one place a stack trace is the honest answer.
86 if (err instanceof Error) {
87 stderr.write(`diarie: unexpected error: ${messageWithCauses(err)}\n\n`);
88 stderr.write(stackWithCauses(err) + '\n');
89 } else {
90 stderr.write('diarie: unexpected error with no details\n');
91 }
92 process.exitCode = 1;
93 }
94}