Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@evlog/cli": minor
"@evlog/telemetry": patch
---

Introduce the evlog CLI (`@evlog/cli`, binary `evlog`). First release ships `evlog doctor` — diagnoses your setup (Node version, evlog install, local `.evlog/logs` sink) with a branded terminal report or `--json` output — plus `evlog telemetry status|enable|disable`. Opt into debug with `--debug` / `EVLOG_CLI_DEBUG=1`. Commands use `defineEvlogCommand` → `{ cli, log, ui }`: `log.step` / `log.finding(cliErrors.X)` for diagnostics, `ui.done` for human/json/exit. Compact case-file on stderr; raw event with `--json --debug`. Workspace detection covers pnpm, bun (`bun.lock` / `bun.lockb`), npm, and yarn.

`--json`, `--debug`, and telemetry all include an `environment` stage (`development` | `preview` | `production`). Packaged installs (`npx` / `node_modules`) report `production`; workspace builds report `development`. Override with `EVLOG_CLI_ENV` / `EVLOG_TELEMETRY_ENV`, or inherit `VERCEL_ENV`.

`withTelemetry()` is now generic over citty `ArgsDef`, so root commands with typed flags (e.g. `--debug`) type-check cleanly. `evlog telemetry status` (and any tool using `defineTelemetryCommands`) prints the local data directory path. Telemetry `env.environment` is part of the standard envelope; authors may pass `environment` in `TelemetryOptions`.
5 changes: 5 additions & 0 deletions .changeset/evlog-package-json-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"evlog": patch
---

Export `evlog/package.json` so tooling (e.g. `evlog doctor`) can resolve the installed version via `require.resolve('evlog/package.json')` under Node's `exports` map.
1 change: 1 addition & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ scope.
- bench (benchmarks)
- better-auth (Better Auth integration)
- better-stack (Better Stack drain adapter)
- cli (`@evlog/cli` package)
- core (logger, pipeline, error, redact, catalog internals)
- datadog (Datadog drain adapter)
- deps (the dependencies)
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/semantic-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
bench
better-auth
better-stack
cli
core
datadog
deps
Expand Down
69 changes: 69 additions & 0 deletions packages/cli/DEBUG-DX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# CLI debug DX — frictions & wishlist

Notes from wiring `--debug` on `@evlog/cli` (dogfooding `evlog` + the error catalog). Keep this file for maintainers; not user-facing docs.

## Command author contract

`defineEvlogCommand` injects **`{ args, cli, log, ui }`** plus shared flags (`json`, `debug`, `noHeader`).

| Object | Role | API |
| --- | --- | --- |
| `cli` | Inputs (cwd, env, color, …) | read-only context |
| `log` | Debug / diagnostics | `step`, `finding`, `set`, `raw` |
| `ui` | Terminal output + exit | `human`, `json`, `exit`, `done` |

```ts
export default defineEvlogCommand('audit', {
meta: { description: '…' },
args: { since: { type: 'string' } },
async run({ args, cli, log, ui }) {
const data = await log.step('load', () => load(cli.cwd))
if (!data) {
log.finding(cliErrors.LOGS_SINK_MISSING, { id: 'logs' })
ui.done({ human: 'No sink.', summary: { ok: 0, warn: 1, fail: 0 } })
return
}
// unexpected throw inside step → steps trail + cli.COMMAND_FAILED when --debug
ui.done({
jsonMode: args.json,
json: { data },
human: format(data),
summary: { ok: 1, warn: 0, fail: 0 },
})
},
})
```

Rules:

1. **Filet** (header, debug event, catch throw) = zero lines in the command
2. **Récit** = `log.step` / `log.finding(cliErrors.X)` only where useful
3. **Pixels / JSON / exit** = `ui.*` only — never touch `process.stdout` / `exitCode` in commands

## Target flow

```bash
evlog <cmd> --debug # compact case-file report on stderr
evlog <cmd> --json --debug 2>e.json # stdout = contract, stderr = raw wide event
```

## What works today

- `defineEvlogCommand` → `{ cli, log, ui }` + `COMMON_ARGS`
- `log.step('name', fn)` / `log.finding(cliErrors.X, { id, status })`
- `ui.done({ human, json, summary, jsonMode })`
- Human `--debug` → `formatDebugReport`; `--json --debug` → raw event on stderr
- `environment` on `--json` / debug / telemetry: packaged install → `production`, workspace → `development` (`EVLOG_CLI_ENV` / `VERCEL_ENV` override)

## Friction / wishlist

- Soft findings still mapped in doctor via `findingsForChecks` — ideal: checks carry a catalog ref
- `DefinedError.toFinding()` on evlog catalog would remove `toCliFinding` glue
- Pretty → stderr / isolated logger still useful upstream in `evlog`
- Live breadcrumbs (`--debug -v`) for long commands later

## Publish note — `workspace:*` deps

`package.json` keeps `"evlog": "workspace:*"` and `"@evlog/telemetry": "workspace:*"` for local linking. **pnpm / `changeset publish` rewrite `workspace:` to real semver** on the tarball.

Doctor resolves the install via `require.resolve('evlog/package.json')` — that subpath is exported on `evlog` (`"./package.json": "./package.json"`).
110 changes: 110 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<p align="center">
<img src="https://raw.githubusercontent.com/HugoRCD/evlog/main/assets/evlog-banner.gif" width="100%" alt="evlog — Digging through logs is not observability. It's hope" />
</p>

# @evlog/cli

[![npm version](https://img.shields.io/npm/v/@evlog/cli?color=black)](https://npmjs.com/package/@evlog/cli)
[![npm downloads](https://img.shields.io/npm/dm/@evlog/cli?color=black)](https://npm.chart.dev/@evlog/cli)
[![CI](https://img.shields.io/github/actions/workflow/status/HugoRCD/evlog/ci.yml?branch=main&color=black)](https://github.com/HugoRCD/evlog/actions/workflows/ci.yml)
[![TypeScript](https://img.shields.io/badge/TypeScript-black?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Documentation](https://img.shields.io/badge/Documentation-black?logo=readme&logoColor=white)](https://evlog.dev)
[![license](https://img.shields.io/github/license/HugoRCD/evlog?color=black)](https://github.com/HugoRCD/evlog/blob/main/LICENSE)

**Digging through logs is not observability. It's hope.**

The official command line for [evlog](https://evlog.dev).

Diagnose your install. Inspect wide events. Audit and map what your app emits.

## Usage

```bash
pnpm add -D @evlog/cli
pnpm evlog doctor
```

Or without installing:

```bash
npx @evlog/cli doctor
npx @evlog/cli doctor --json
npx @evlog/cli doctor --cwd apps/web
```

## Commands

| Command | What it does |
| --- | --- |
| `evlog doctor` | Monorepo-aware diagnosis: Node, project/workspace, stack, evlog install, `.evlog/logs` |
| `evlog doctor --cwd <dir>` | Run against another directory |
| `evlog doctor --debug` | Same, plus a debug wide event (see Debug) |
| `evlog telemetry status` | Show telemetry status and disclosure |
| `evlog telemetry enable` / `disable` | Change telemetry preference (disable purges buffered data) |

## Exit codes

| Code | Meaning |
| --- | --- |
| `0` | All checks passed (warnings allowed) |
| `1` | At least one check failed |
| `2` | Usage error (unknown command or flags) |

## `--json` output

With `--json`, the payload is the **only** thing written to stdout — everything human goes to stderr. The shape is a contract:

```jsonc
{
"schemaVersion": 1,
"checks": [{ "id": "node", "status": "ok", "message": "Node v22.1.0" }],
"summary": { "ok": 4, "warn": 0, "fail": 0 }
}
```

Breaking this shape requires a `schemaVersion` bump.

## Telemetry

The CLI records **one anonymous wide event per command** via [`@evlog/telemetry`](https://npmjs.com/package/@evlog/telemetry) (tool name `evlog-cli`): command name, duration, outcome, sanitized flags. No arguments, paths, or file contents. Opt out anytime:

```bash
evlog telemetry disable # or DO_NOT_TRACK=1 / EVLOG_TELEMETRY=0
```

Full policy: [evlog.dev — telemetry](https://evlog.dev/use-cases/telemetry/overview)

## Quieter output

Commands print a short branded header by default. Skip it with `--no-header`, `EVLOG_CLI_NO_HEADER=1`, or `--json`.

## Debug

Emit one debug case file per command with `--debug` or `EVLOG_CLI_DEBUG=1` (dogfoods `evlog`). Human mode prints a compact summary on stderr (`steps`, `findings`, resolve probes). With `--json`, the raw wide event goes to stderr so stdout stays a clean JSON contract. Separate from product telemetry (`@evlog/telemetry`).

```bash
evlog doctor --debug
evlog doctor --json --debug # JSON result on stdout, full debug event on stderr
```

Maintainer notes on frictions / wishlist: [`DEBUG-DX.md`](./DEBUG-DX.md).

## Adding a command

1. Create `src/commands/<name>.ts` with `defineEvlogCommand('name', { run({ args, cli, log, ui }) { … } })` — header, `--json` / `--debug` / `--no-header`, and debug filet are automatic. Use `log.step` / `log.finding` for diagnostics; `ui.done` / `ui.human` / `ui.json` for output.
2. Register it with one import + one line in [`src/commands/index.ts`](src/commands/index.ts).

`src/index.ts` stays a thin shell (meta + `withTelemetry`). Do not embed command bodies there.

```
src/
cli.ts # bin entry (runMain)
index.ts # main command tree
commands/ # one file per command + registry
core/ # context, output, brand, usage
lib/ # shared constants / helpers
```

## Docs

Full guide: [evlog.dev](https://evlog.dev)
2 changes: 2 additions & 0 deletions packages/cli/bin/evlog.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env node
import '../dist/cli.mjs'
70 changes: 70 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"name": "@evlog/cli",
"version": "0.0.0",
"description": "The official command line for evlog",
"author": "HugoRCD <contact@hrcd.fr>",
"homepage": "https://evlog.dev",
"repository": {
"type": "git",
"url": "git+https://github.com/HugoRCD/evlog.git",
"directory": "packages/cli"
},
"bugs": {
"url": "https://github.com/HugoRCD/evlog/issues"
},
"license": "MIT",
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"bin": {
"evlog": "./bin/evlog.mjs"
},
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"default": "./dist/index.mjs"
}
},
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"files": [
"bin",
"dist",
"README.md"
],
"keywords": [
"evlog",
"cli",
"wide-events",
"doctor",
"citty",
"structured-logging",
"telemetry"
],
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"dev:prepare": "tsdown",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "vitest run",
"test:watch": "vitest watch",
"typecheck": "echo 'Typecheck handled by build'"
},
"dependencies": {
"@evlog/telemetry": "workspace:*",
"citty": "^0.2.2",
"evlog": "workspace:*"
},
"devDependencies": {
"tsdown": "^0.22.8",
"tsx": "^4.23.1",
"typescript": "^6.0.3",
"vitest": "^4.1.10"
},
"publishConfig": {
"access": "public"
}
}
5 changes: 5 additions & 0 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { runMain } from 'citty'
import { showUsage } from './core/usage'
import { main } from './index'

runMain(main, { showUsage })
Loading
Loading