Skip to content

feat: add @evlog/cli observability package#360

Open
HugoRCD wants to merge 4 commits into
mainfrom
feat/cli-observability
Open

feat: add @evlog/cli observability package#360
HugoRCD wants to merge 4 commits into
mainfrom
feat/cli-observability

Conversation

@HugoRCD
Copy link
Copy Markdown
Owner

@HugoRCD HugoRCD commented May 30, 2026

Summary

  • Add @evlog/cli — wide events per command, setupEvlog() + useLogger(), citty (runMain) and ofetch (createOutboundHooks) entrypoints
  • Demo CLI (examples/cli, pnpm example:cli) with fs/Axiom/Datadog/OTLP drain routing via env
  • Docs at /integrate/frameworks/cli (drain setup, catalogs, progressive adoption)
  • CI: pkg-pr-new + npm publish --dry-run for @evlog/cli

Test plan

  • pnpm --filter @evlog/cli run test (14 tests)
  • pnpm --filter @evlog/cli run typecheck
  • pnpm --filter @evlog/cli run lint
  • npm publish --dry-run on packages/cli
  • pnpm example:cli doctor — wide event in .evlog/logs/
  • pkg-pr-new comment installs @evlog/cli on this PR

Summary by CodeRabbit

  • New Features

    • Introduces @evlog/cli for CLI observability (per-command telemetry, optional console logging via --log, error/audit helpers).
    • Built-in drain routing with defaults for local NDJSON and adapters for Axiom, Datadog, and OTLP.
  • Documentation

    • New comprehensive CLI integration guide, API reference, and installation docs.
  • Chores

    • Example multi-command CLI demo added.
    • CI updated to include CLI package in publish dry-run and publish steps.

Review Change Stack

@vercel
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
evlog-docs Ready Ready Preview, Comment, Open in v0 May 30, 2026 7:45pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
just-use-evlog Skipped Skipped May 30, 2026 7:45pm

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

Thank you for following the naming conventions! 🙏

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 30, 2026

📝 Walkthrough

Walkthrough

This PR introduces @evlog/cli, a new package providing observability for TypeScript CLIs built with citty. It delivers CLI-specific type contracts, a setupEvlog() bootstrap function with per-command useLogger() hooks, drain configuration with environment-based selection, error/audit catalog support, and a complete example CLI with multiple commands demonstrating integration patterns.

Changes

@evlog/cli Library and Integration

Layer / File(s) Summary
Type contracts and configuration
packages/cli/src/types.ts, packages/cli/src/errors.ts
CliContext, InvokeOptions, EvlogSetupConfig, ParsedCliError, and EvlogSetup interfaces define configuration shapes, per-command context, and the returned handle API for CLI observability.
Core library implementation
packages/cli/src/index.ts, packages/cli/src/redact.ts, packages/cli/src/presentation.ts, packages/cli/src/enrichers.ts, packages/cli/src/flush.ts
setupEvlog() bootstrap wires redaction presets, drain/catalog selection, and CLI context enrichment; console logging behavior controlled via --log/logToConsole; process shutdown flushing via signal handlers.
Framework integrations and adapters
packages/cli/src/integration.ts, packages/cli/src/citty.ts, packages/cli/src/http.ts
CLI framework integration with logger storage and useLogger() hook; citty command tree wrapper that injects setup.invoke() and --log flag; HTTP outbound hooks for fetch/ofetch request/response logging.
Package configuration
packages/cli/package.json, packages/cli/tsconfig.json, packages/cli/tsdown.config.ts, packages/cli/vitest.config.ts
ESM package manifest with conditional ./citty and ./http exports, peer dependencies, and build/test configuration.
Library test suites
packages/cli/test/helpers/drain.ts, packages/cli/test/cli.test.ts, packages/cli/test/citty.test.ts, packages/cli/test/http.test.ts
Drain spying utilities and comprehensive tests covering setupEvlog().invoke() wide event emission, redaction, console logging modes, error handling, citty wrapper lifecycle, and HTTP hook recording.

Demo CLI Application and Documentation

Layer / File(s) Summary
Complete demo CLI
examples/cli/package.json, examples/cli/src/..., examples/cli/.env.example
Multi-command example (doctor, pull, sync, deploy) with error/audit catalogs demonstrating per-command telemetry via useLogger(), environment-based drain selection, actor resolution, and both JSON and human-readable output modes.
Integration guides and API reference
apps/docs/content/3.integrate/frameworks/17.cli.md, apps/docs/content/3.integrate/frameworks/00.overview.md, packages/cli/README.md, apps/docs/content/1.start/3.installation.md
Full CLI integration guide with drain configuration walkthrough, bootstrap/invoke patterns, command implementation examples, catalog setup, and API reference; framework overview table and installation instructions updated.
Monorepo configuration
.changeset/cli-package-v1.md, .github/workflows/ci.yml, .github/workflows/semantic-pull-request.yml, AGENTS.md, package.json (script), packages/evlog/README.md
Changeset entry describing minor release; CI publish verification step added for three packages; semantic PR scope validation updated; example CLI runnable script added; monorepo structure and parent package README documented.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as CLI Binary
  participant Setup as setupEvlog()
  participant Invoke as setup.invoke()
  participant Command as Command Handler
  participant Logger as useLogger()
  participant Drain as Drain Pipeline
  
  CLI->>Setup: config: { service, version, drain, catalogs, redact }
  Setup-->>CLI: EvlogSetup handle
  
  CLI->>Invoke: command: 'doctor', fn: handlerFn
  activate Invoke
  Invoke->>Invoke: bootstrap logger per invocation
  Invoke->>Command: executes handler
  activate Command
  Command->>Logger: useLogger() for telemetry
  Logger->>Logger: log.set({ field: value })
  Logger->>Logger: log.audit({ type: 'CHECKED' })
  Command-->>Invoke: returns/throws result
  deactivate Command
  Invoke->>Invoke: finish({ status: 0 }) or finish({ error: err })
  Invoke->>Drain: emit wide event with context/status/duration
  Invoke-->>CLI: event persisted
  deactivate Invoke
  
  CLI->>Setup: setup.flush()
  Setup->>Drain: drain.flush()
  Drain-->>CLI: buffered events flushed
  CLI->>CLI: exit(code)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • HugoRCD/evlog#356: Audit metadata enhancements (severity, requiresChanges, redactPaths) that enable the demo CLI's audit catalog definitions.

Suggested labels

feature

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding a new @evlog/cli package for CLI observability.
Description check ✅ Passed The description provides a comprehensive summary with clear sections (Summary, Test plan) and includes implementation details, demo info, and documentation references, though it does not include a linked issue section.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cli-observability

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 30, 2026

npm i https://pkg.pr.new/HugoRCD/evlog/@evlog/cli@360
npm i https://pkg.pr.new/HugoRCD/evlog@360
npm i https://pkg.pr.new/HugoRCD/evlog/@evlog/nuxthub@360

commit: 42b0433

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/cli-package-v1.md:
- Line 5: Add a top-level H1 heading at the very start of the changeset file so
the file begins with a Markdown heading (for example add a first line like "#
Add `@evlog/cli`" or "# CLI package v1") before the existing content "Add
`@evlog/cli` — CLI observability for evlog."; update the file named
cli-package-v1.md in the .changeset set so the first line is a single "#"
heading followed by the rest of the changeset content.

In `@apps/docs/content/3.integrate/frameworks/17.cli.md`:
- Around line 97-103: The example misses importing exitWithError used in the
final catch; add an import for exitWithError from '`@evlog/cli`' alongside the
existing imports so the snippet that calls runMain(main, setup).then(() =>
setup.flush()).catch(err => exitWithError(err)) compiles; update the top of the
file to import exitWithError so symbols exitWithError, runMain, setup and main
are all available.

In `@examples/cli/src/commands/sync.ts`:
- Line 27: Replace the current coercion for count with explicit parsing and
validation: parse args.records once (e.g. using parseInt/Number), ensure the
result is an integer and within the allowed range (reject negative values and
non-integers), and if invalid, exit or throw a clear error; update the variable
referenced as count and validate args.records before it flows into any
loop/payload logic so that 0, negatives, and fractional values are handled
correctly.

In `@examples/cli/src/index.ts`:
- Around line 7-14: The catch handler currently calls exitWithError immediately
and skips flushing; make the .catch callback async and perform a best-effort
await setup.flush() before calling exitWithError(error) (e.g. change
.catch((error: unknown) => { ... }) to .catch(async (error: unknown) => { if
(process.env.DEBUG) console.error(parseCliError(error)); await
setup.flush().catch(() => {}); exitWithError(error); }), referencing runMain,
setup, parseCliError and exitWithError so the error path flushes before exit.

In `@packages/cli/package.json`:
- Around line 18-36: Add a "typesVersions" field to packages/cli package.json
mapping the public exports to their .d.mts type files: create "typesVersions": {
"*": { ".": ["./dist/index.d.mts"], "citty": ["./dist/citty.d.mts"], "http":
["./dist/http.d.mts"] } } so the existing exports for ".", "./citty" and
"./http" (and the package-level "types") correctly resolve to dist/*.d.mts for
TypeScript consumers.

In `@packages/cli/README.md`:
- Around line 167-171: The diff adds a .catch(exitWithError) to the runMain call
but exitWithError is not imported, causing a compile error; fix it by adding an
import for exitWithError (or exporting it if missing) and use that symbol in the
top imports alongside runMain and setup so the final file imports runMain,
exitWithError and setup before calling runMain(main, setup).then(() =>
setup.flush()).catch(exitWithError).

In `@packages/cli/src/enrichers.ts`:
- Around line 16-20: The CLI enrichers currently emit the raw argv in the `cli`
payload (the `argv` field built from `options.argv`), which can leak secrets;
remove the raw `argv` from the emitted `cli` object or replace it with a
sanitized version (e.g., produce `sanitizedArgv` by parsing
`options.argv`/`options.flags` and redacting values for sensitive flags like
token, password, secret, key, or any positional args matched by a sensitive flag
list), and only include `command`, `flags` (sanitized), and `version`; update
the `cli` construction that references `options.argv` so events never contain
unredacted `argv`.
- Around line 34-35: applyCliContext is using an unsafe cast "as never" when
calling RequestLogger.set; instead adjust buildCliContext's return type to match
RequestLogger.set's expected FieldContext<T> (e.g., change buildCliContext(...)
to return FieldContext<Record<string, unknown>> or the precise FieldContext<T>
matching RequestLogger) so you can call logger.set(buildCliContext(options))
directly; update the buildCliContext signature (and any related types like
CliContextFields) so the types align and remove the "as never" escape hatch in
applyCliContext.

In `@packages/cli/src/flush.ts`:
- Around line 49-51: resetFlushOnExitForTests currently only flips the flags but
leaves process listeners attached; change it to remove the installed listeners
before clearing state: call process.removeListener('beforeExit', flushHandler)
and remove the same flushHandler for any signal names the module installs (e.g.
'SIGINT', 'SIGTERM', 'SIGHUP' — match the exact list used where listeners are
added), then set flushHandler = undefined and listenersInstalled = false; ensure
you reference the module's flushHandler and the same signal list used by the
install logic so the exact handler functions are removed.
- Around line 31-40: The current signal handler (handleSignal) re-emits the same
signal with process.kill(process.pid, signal) while the listener remains
registered, causing an endless loop; fix it by removing/unregistering the
installed listener before re-emitting or restoring default behavior: inside the
process.on callback (both in the !handler branch and the finally of the handler
promise) call process.removeListener(signal, /* the same callback reference used
in handleSignal */) or process.off(signal, /* callback */) (reference the
handleSignal installation code and the flushHandler variable) and only then call
process.kill(process.pid, signal) so the signal will run with default behavior
instead of re-entering the handler.
- Around line 7-12: resolveFlushFn currently rejects any non-function drain
early so objects exposing a .flush() are ignored; change the logic in
resolveFlushFn to accept either a function drain or an object with a flush
method: if drain is a function return a wrapper that calls drain(), otherwise
treat drain as candidate = drain as { flush?: () => Promise<void> } and only
return a wrapper that calls candidate.flush() when typeof candidate.flush ===
'function'; keep the existing return type and null/undefined behavior otherwise.

In `@packages/cli/src/http.ts`:
- Around line 19-22: resolveUrl can throw when constructing new URL(request,
baseURL) if baseURL is invalid; wrap the URL construction in a guard (try/catch)
inside the resolveUrl function and, on failure, fall back to returning the
original request string (or a safe default) instead of letting the exception
propagate; ensure you only attempt new URL when baseURL is truthy and valid, and
reference resolveUrl to locate the change.

In `@packages/cli/src/index.ts`:
- Around line 43-47: The env object currently spreads config.env last which
allows config.env to overwrite explicit top-level fields; change the spread
order so config.env is spread first and then set service, environment, and
version using the existing expressions (service: config.service ??
config.env?.service ?? 'cli', environment: config.environment ??
config.env?.environment, version: config.version ?? config.env?.version) so
top-level config.* values take precedence while still falling back to
config.env.* and defaults; update the env object near the config usage in
packages/cli/src/index.ts accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5a6c0896-e59b-41e8-b0dc-a150afd0eefb

📥 Commits

Reviewing files that changed from the base of the PR and between 1b17ff1 and 6cb193d.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (45)
  • .changeset/cli-package-v1.md
  • .github/pull_request_template.md
  • .github/workflows/ci.yml
  • .github/workflows/semantic-pull-request.yml
  • AGENTS.md
  • apps/docs/content/1.start/3.installation.md
  • apps/docs/content/3.integrate/frameworks/00.overview.md
  • apps/docs/content/3.integrate/frameworks/17.cli.md
  • examples/cli/.env.example
  • examples/cli/README.md
  • examples/cli/package.json
  • examples/cli/src/catalogs/actor.ts
  • examples/cli/src/catalogs/audit.ts
  • examples/cli/src/catalogs/errors.ts
  • examples/cli/src/commands/deploy.ts
  • examples/cli/src/commands/doctor.ts
  • examples/cli/src/commands/index.ts
  • examples/cli/src/commands/pull.ts
  • examples/cli/src/commands/sync.ts
  • examples/cli/src/drain.ts
  • examples/cli/src/evlog.ts
  • examples/cli/src/index.ts
  • examples/cli/tsconfig.json
  • package.json
  • packages/cli/README.md
  • packages/cli/package.json
  • packages/cli/src/citty.ts
  • packages/cli/src/enrichers.ts
  • packages/cli/src/errors.ts
  • packages/cli/src/flush.ts
  • packages/cli/src/http.ts
  • packages/cli/src/index.ts
  • packages/cli/src/integration.ts
  • packages/cli/src/presentation.ts
  • packages/cli/src/redact.ts
  • packages/cli/src/types.ts
  • packages/cli/test/citty.test.ts
  • packages/cli/test/cli.test.ts
  • packages/cli/test/helpers/drain.ts
  • packages/cli/test/http.test.ts
  • packages/cli/tsconfig.json
  • packages/cli/tsdown.config.ts
  • packages/cli/vitest.config.ts
  • packages/evlog/README.md
  • packages/evlog/test/README.md

Comment thread .changeset/cli-package-v1.md
Comment thread apps/docs/content/3.integrate/frameworks/17.cli.md Outdated
Comment thread examples/cli/src/commands/sync.ts Outdated
Comment thread examples/cli/src/index.ts
Comment thread packages/cli/package.json
Comment thread packages/cli/src/flush.ts
Comment thread packages/cli/src/flush.ts Outdated
Comment thread packages/cli/src/flush.ts
Comment thread packages/cli/src/http.ts
Comment thread packages/cli/src/index.ts Outdated
@HugoRCD HugoRCD changed the title feat(cli): add @evlog/cli observability package feat: add @evlog/cli observability package May 30, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/cli/src/flush.ts (1)

37-40: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Refresh the stored flush function on every install.

Line 38 returns before flushHandler is updated, so the first installFlushOnExit() call wins permanently. If the process bootstraps @evlog/cli more than once, shutdown hooks will flush the wrong pipeline and can drop the later setup's buffered events.

Proposed fix
 export function installFlushOnExit(flush: () => Promise<void>): void {
-  if (listenersInstalled) return
-  listenersInstalled = true
   flushHandler = flush
+  if (listenersInstalled) return
+  listenersInstalled = true
 
   const runFlush = () => {
     if (!flushHandler) return
     void flushHandler().catch(err => console.error('[evlog/cli] flush failed:', err))
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/flush.ts` around lines 37 - 40, The installFlushOnExit
function currently returns early when listenersInstalled is true and therefore
never updates the stored flush function; change it so that flushHandler is
always updated on every call to installFlushOnExit (assign the incoming flush to
the module-level flushHandler unconditionally), and then only short-circuit
returning when listenersInstalled is true; keep the listenersInstalled flag and
existing listener installation logic intact so listeners are still only
installed once. Ensure you reference/modify the existing installFlushOnExit
function and the module-level flushHandler and listenersInstalled variables.
packages/cli/README.md (1)

99-101: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Flush on the error path too.

Both snippets only await setup.flush() after a successful runMain(). If the command rejects, copied integrations will jump straight to exitWithError() and can lose the final wide event for the failing invocation.

Suggested doc fix
-runMain(main, setup)          // was: runMain(main)
-  .then(() => setup.flush())  // flush batched drain writes
-  .catch(error => exitWithError(error))
+runMain(main, setup)          // was: runMain(main)
+  .then(() => setup.flush())  // flush batched drain writes
+  .catch(async (error) => {
+    await setup.flush().catch(() => {})
+    exitWithError(error)
+  })

Also applies to: 168-172

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/README.md` around lines 99 - 101, The current flow calls
setup.flush() only on the success path after runMain(main, setup) and skips it
when runMain rejects, causing lost final events; change the control flow to
always await setup.flush() regardless of success or failure (e.g., use a
try/catch/finally or Promise.finally) so that setup.flush() runs before calling
exitWithError(error) or before resolving; locate the call site using
runMain(main, setup), setup.flush, and exitWithError to implement the guaranteed
flush in the error path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/docs/content/3.integrate/frameworks/17.cli.md`:
- Around line 355-360: The example currently calls useLogger() at module load
time which binds a logger outside the command-scoped context; move the call to
useLogger() (and the ofetch.create(createOutboundHooks(...)) call that uses it)
inside the command handler or runMain()/invoke() callback so the logger is
created within the active command context; specifically instantiate api by
calling ofetch.create(createOutboundHooks(useLogger())) inside the handler where
the command is executed (rather than at top-level module evaluation) so
createOutboundHooks and useLogger() are invoked with the correct command-scoped
logger.

In `@packages/cli/test/flush.test.ts`:
- Around line 35-45: Add a regression test that installs two different flush
handlers via installFlushOnExit and verifies the second (latest) handler is the
one invoked and that resetFlushOnExitForTests cleans up both listeners: create
two spies (e.g., flushA and flushB), call installFlushOnExit(flushA) then
installFlushOnExit(flushB), emit a 'beforeExit' (or otherwise trigger the flush
behavior) and assert only flushB was called (and flushA not called), then call
resetFlushOnExitForTests() and assert process.listenerCount('beforeExit')
returns to beforeCount; reference installFlushOnExit and
resetFlushOnExitForTests to locate code.

---

Outside diff comments:
In `@packages/cli/README.md`:
- Around line 99-101: The current flow calls setup.flush() only on the success
path after runMain(main, setup) and skips it when runMain rejects, causing lost
final events; change the control flow to always await setup.flush() regardless
of success or failure (e.g., use a try/catch/finally or Promise.finally) so that
setup.flush() runs before calling exitWithError(error) or before resolving;
locate the call site using runMain(main, setup), setup.flush, and exitWithError
to implement the guaranteed flush in the error path.

In `@packages/cli/src/flush.ts`:
- Around line 37-40: The installFlushOnExit function currently returns early
when listenersInstalled is true and therefore never updates the stored flush
function; change it so that flushHandler is always updated on every call to
installFlushOnExit (assign the incoming flush to the module-level flushHandler
unconditionally), and then only short-circuit returning when listenersInstalled
is true; keep the listenersInstalled flag and existing listener installation
logic intact so listeners are still only installed once. Ensure you
reference/modify the existing installFlushOnExit function and the module-level
flushHandler and listenersInstalled variables.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 78682747-cb49-4750-9bb5-029959a73bfd

📥 Commits

Reviewing files that changed from the base of the PR and between 6cb193d and 42b0433.

📒 Files selected for processing (13)
  • .changeset/cli-package-v1.md
  • apps/docs/content/3.integrate/frameworks/17.cli.md
  • examples/cli/src/commands/sync.ts
  • examples/cli/src/index.ts
  • packages/cli/README.md
  • packages/cli/package.json
  • packages/cli/src/enrichers.ts
  • packages/cli/src/flush.ts
  • packages/cli/src/http.ts
  • packages/cli/src/index.ts
  • packages/cli/test/cli.test.ts
  • packages/cli/test/flush.test.ts
  • packages/cli/test/http.test.ts

Comment on lines +355 to +360
import { ofetch } from 'ofetch'
import { useLogger } from '@evlog/cli'
import { createOutboundHooks } from '@evlog/cli/http'

const api = ofetch.create(createOutboundHooks(useLogger()))
await api('https://api.example.com/v1/records')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move useLogger() inside the command handler.

This example calls useLogger() at module evaluation time, before runMain()/invoke() establishes the command-scoped context. Copied as-is, it will either bind the wrong logger or throw outside an active command.

Suggested doc fix
-const api = ofetch.create(createOutboundHooks(useLogger()))
-await api('https://api.example.com/v1/records')
+async run() {
+  const api = ofetch.create(createOutboundHooks(useLogger()))
+  await api('https://api.example.com/v1/records')
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/content/3.integrate/frameworks/17.cli.md` around lines 355 - 360,
The example currently calls useLogger() at module load time which binds a logger
outside the command-scoped context; move the call to useLogger() (and the
ofetch.create(createOutboundHooks(...)) call that uses it) inside the command
handler or runMain()/invoke() callback so the logger is created within the
active command context; specifically instantiate api by calling
ofetch.create(createOutboundHooks(useLogger())) inside the handler where the
command is executed (rather than at top-level module evaluation) so
createOutboundHooks and useLogger() are invoked with the correct command-scoped
logger.

Comment on lines +35 to +45
describe('resetFlushOnExitForTests', () => {
it('removes beforeExit listeners installed by installFlushOnExit', () => {
const flush = vi.fn(async () => {})
const beforeCount = process.listenerCount('beforeExit')

installFlushOnExit(flush)
expect(process.listenerCount('beforeExit')).toBe(beforeCount + 1)

resetFlushOnExitForTests()
expect(process.listenerCount('beforeExit')).toBe(beforeCount)
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add a double-install regression case.

This suite verifies extraction and cleanup, but it still misses the case where installFlushOnExit() is called twice and should flush the latest handler. That gap would let the stale-handler bug in packages/cli/src/flush.ts regress unnoticed.

Based on learnings: Every code change must have a matching test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/test/flush.test.ts` around lines 35 - 45, Add a regression test
that installs two different flush handlers via installFlushOnExit and verifies
the second (latest) handler is the one invoked and that resetFlushOnExitForTests
cleans up both listeners: create two spies (e.g., flushA and flushB), call
installFlushOnExit(flushA) then installFlushOnExit(flushB), emit a 'beforeExit'
(or otherwise trigger the flush behavior) and assert only flushB was called (and
flushA not called), then call resetFlushOnExitForTests() and assert
process.listenerCount('beforeExit') returns to beforeCount; reference
installFlushOnExit and resetFlushOnExitForTests to locate code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant