A flat-YAML task tracker that is just files. No daemon, no database, no git hooks. diarie.dev
issue-tracker cli nodejs agent-friendly
0

Configure Feed

Select the types of activity you want to include in your feed.

diarie / cli.js
5.0 kB 94 lines
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}