Skip to content

feat(cli): log to ~/.rawback/logs and add the logs command group - #81

Merged
AnnatarHe merged 4 commits into
mainfrom
claude/jolly-hamilton-h8r2a2
Sep 21, 2026
Merged

AnnatarHe merged 4 commits into
mainfrom
claude/jolly-hamilton-h8r2a2

Conversation

@AnnatarHe

@AnnatarHe AnnatarHe commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Important

Blocked on rawback-app/sdk#43. This builds on new @rawback/sdk APIs, and the pin here is still the exact 0.3.2, so CI cannot go green until that PR merges and release-please publishes. On release, bump the pin and regenerate bun.lock — nothing else in this diff should need to change. It was developed and validated against a local build of that branch.

Why

The CLI had no verbosity flag and no diagnostics at all. The only console.* calls in src/ were the two default sinks inside CommandOutput, so a user reporting a failure had nothing to send and no trace ID to quote.

What

Global flags

--log-level <level> and -v/-vv, declared beside --env and stashed in a new src/log-level.ts singleton, with the same middleware pattern.

That module's @rawback/sdk import is type-only and erases at compile time. It matters: cli.ts imports it and runs on every invocation, so a runtime import would drag the SDK barrel — ssh2 included — onto the rawback --help path, worth ~240 ms. This is the trap src/trace.ts already documents.

Lazy logger construction

src/logging.ts builds the one logger the process owns, memoized, behind the same boundary that keeps --help and --version from writing to a user's home directory. Neither path creates ~/.rawback/logs/ — asserted in a test rather than assumed, since a regression there would create files just for asking for help.

runCli records the command and its exit code and flushes in a finally: a failed command is exactly the one whose records need to reach the file, and pino's rolling destination is asynchronous, so a CLI process would otherwise exit before it drains. Only the leading non-flag words are recorded as the command name — an option value can be a search term or a path from the user's disk.

rawback logs path|show|purge

Modelled on the config group: nested .command(...), .demandCommand(1, …), .strict(), a no-op group handler, and the three per-handler invariants (exit-code guard, lazy await import, runCommand wrapper). Implementation flat in src/logs.ts with the house (options, dependencies = {}) signature; presenter in src/features/logs/view.ts.

  • show reads only the tail via a positioned read, so it stays fast on a ten-megabyte log, and shows a line it cannot parse as { raw } rather than dropping it — a torn record is evidence too. It asks the SDK for the active file rather than building a name.
  • purge defaults to --app all and so covers Desktop's log, because both clients share one directory. It matches only the filenames the SDK writes, for apps it knows, and never removes the directory itself: logging.directory is user-controlled and could point anywhere. A file it cannot delete is reported, not thrown — on Windows the Desktop app holds its log open — and the command exits 1 when any was left behind.

File naming

The SDK uses pino-roll, which numbers every file from 1. There is no un-numbered cli.log: files are cli.1.log, cli.2.log, … and the highest number is the one being written. Docs and fixtures reflect that, and the spawn test discovers the file rather than assuming a name.

Behaviour at the default level

info, at which a run records every failure and nothing else:

$ rawback photos list          # no credentials in a fresh home
✗ Authentication credentials are missing; run rawback auth

$ rawback logs show
Time                      Level  Event             Message
2026-09-21T05:10:34.707Z  warn   cli.command.done  photos list exited 1

-v adds the full request stream and headers, -vv adds bodies.

Logs never touch stdout, so raising verbosity cannot disturb a script parsing --json — covered by a test that runs -vv … --json and parses the result. The SDK pins the same invariant on its side, since pino's own default destination is stdout.

Docs

All four surfaces CLAUDE.md requires: yargs help in src/cli.ts, a troubleshooting block in README.md, the ## Global options table plus a ## rawback logs section in docs/commands.md, and a ## Logging section in docs/configuration.md covering the file layout, rotation, redaction, env vars and permissions.

Verification

bun run check passes — typecheck → test → lint → format:check → build. 533 tests (512 pre-existing, all green; 21 new across 3 files), split per CLAUDE.md: spawn-based for help and validation, injected dependencies and temp paths for filesystem behaviour. Re-run against the SDK branch's latest head.

Notes for reviewers

  • bun.lock is deliberately unchanged. My local bun install ran under a different Bun than the repo pins and rewrote lockfileVersion 2 → 1 plus a transitive bump; committing that would have fought the frozen install. It needs a real regeneration with the SDK bump.
  • --level filtering keeps its own small copy of pino's level numbers rather than importing a map the SDK does not export.
  • The log directory is rendered as a text block rather than a fields row. The shared fields renderer drops its label padding when a value is too long for the terminal (Directory/tmp/… with no gap), and a log directory is always a long absolute path. That looked like my bug but reproduces on any long value, so I worked around it here rather than changing shared UI in this PR — worth a separate fix.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HwoxuU7mv4yFQnecnKoaPY

The CLI had no verbosity flag and no diagnostics: the only console output
was the two sinks inside CommandOutput, so a user reporting a failure had
nothing to send. Build on the SDK's logging subsystem.

- Global `--log-level` and `-v`/`-vv`, declared beside `--env` and stashed
  in a new `src/log-level.ts` singleton. Its `@rawback/sdk` import is
  type-only and erases, so `cli.ts` stays off the SDK barrel and
  `rawback --help` keeps its startup cost — the trap `src/trace.ts`
  already documents.
- `src/logging.ts` builds the one logger this process owns, lazily and
  memoized, behind the same boundary that keeps `--help` and `--version`
  from writing to a user's home. Neither path creates `~/.rawback/logs/`,
  which is asserted rather than assumed.
- `runCli` records the command and its exit code, then flushes in a
  `finally` — a failed command is exactly the one whose records need to
  reach the file.
- `rawback logs path|show|purge`, modelled on the `config` group.
  `show` reads only the tail, so it stays fast on a ten-megabyte file, and
  shows a torn line rather than dropping it. `purge` covers Desktop's log
  too, since both clients share the directory; it matches only the names
  this tool writes and never removes the directory itself, because
  `logging.directory` is user-controlled.

Logs go to the file and never to stdout, so raising verbosity cannot
disturb a script parsing `--json`.

Docs updated across all four surfaces: yargs help, README, the global
options table and a `rawback logs` section in docs/commands.md, and a
Logging section in docs/configuration.md.

The directory path is rendered as a text block rather than a field: the
shared fields renderer drops its label padding when a value is long, and
a log directory is always a long absolute path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwoxuU7mv4yFQnecnKoaPY
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 21, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-21T06:17:18.707651Z d934668 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e33cf5ff96

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/logs.ts

import { commandOutput, type ReadCommandDependencies } from './command.ts'
import { environmentName } from './config.ts'
import { logFilesDocument, logLinesDocument, purgeResultDocument } from './features/logs/view.ts'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add the imported logs presenter

The commit does not contain src/features/logs/view.ts or any other definition of these three exports (checked the full commit tree), so every logs path, logs show, and logs purge handler fails while dynamically importing logs.ts; a normal dependency-enabled typecheck/build will also reject this unresolved module. Add the presenter that this import expects.

AGENTS.md reference: AGENTS.md:L69-L71

Useful? React with 👍 / 👎.

Comment on lines +35 to +37
for (const argument of args) {
if (argument.startsWith('-')) break
words.push(argument)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Exclude positional values from logged command names

This loop treats every positional argument before the first option as part of the command name. For example, rawback camera connect http://user:password@host records the URL, including its password, in the command field whenever the command fails at the default level (and on every run under -v); photo search prompts and local positional paths leak similarly. Determine the matched command independently of user-supplied positional values rather than copying this argv prefix.

Useful? React with 👍 / 👎.

Comment on lines +67 to +73
const { commandLogger, flushCommandLogger } = await import('../../logging.ts')
const command = commandName(parseArgs)
const logger = (await commandLogger()).child({ component: 'cli', command })
const startedAt = performance.now()
logger.debug(`running ${command}`, { event: 'cli.command.start' })
try {
await program.parseAsync(parseArgs)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Build the logger after parsing verbosity flags

commandLogger() runs before program.parseAsync(), but the yargs middleware that calls setSelectedLogLevel only runs during parsing. The memoized root logger is therefore created with no --log-level/-v override, and later calls from command clients return that existing root, so the newly documented verbosity flags have no effect; the same ordering also makes environment-specific logging configuration use the previously selected/default environment instead of --env.

Useful? React with 👍 / 👎.

Comment thread src/logs.ts Outdated
Comment on lines +184 to +186
const lines = (await readTail(path, count))
.map(parseLine)
.filter((line) => atLeastLevel(line, level))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply the level filter before the line limit

When --level is supplied, this reads only the last count unfiltered records and then removes lower-level entries. Thus logs show --lines 50 --level error returns no records if 50 later debug lines follow an error, even though the command promises the most recent records at the requested level. Scan enough of the tail to collect up to count matching records, then apply the limit.

Useful? React with 👍 / 👎.

Comment thread src/logs.ts
Comment on lines +216 to +218
if (!confirmed) {
output.info('Left the log files in place.')
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep purge cancellation machine-readable under --json

In an interactive rawback logs purge --json invocation without --yes, declining the prompt takes this branch and writes a styled human message to stdout while exiting successfully. A caller that selected --json will therefore fail to parse the successful result; emit a JSON cancellation result instead, as other mutating JSON commands do.

AGENTS.md reference: AGENTS.md:L83-L84

Useful? React with 👍 / 👎.

Comment thread src/logs.ts
}
}

const result = await purgeLogs({ directory, ...(app ? { app } : {}) })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Close the active CLI log before purging it on Windows

Every non-help invocation opens the memoized CLI logger before dispatch, so rawback logs purge --app cli --yes reaches purgeLogs while this process still holds cli.log open. On Windows—the platform this code already anticipates rejecting deletion of open Desktop logs—the CLI's own file is likewise left behind, causing the default --app all purge to report failure and exit 1 even when Desktop is closed. Flush and close/suspend the root logger before deleting its active file.

Useful? React with 👍 / 👎.

`~/.rawback/logs/` joins the files shared with Desktop, and `logs purge`
clears Desktop's log too. Records the two rules a future change would
otherwise trip over: diagnostics never go to stdout, and `src/log-level.ts`
must stay SDK-free at runtime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwoxuU7mv4yFQnecnKoaPY

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c8ea5800ea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/logs.ts
if (!handle) return []
try {
const { size } = await handle.stat()
const span = Math.min(size, Math.min(count * TAIL_BYTES_PER_LINE, MAX_TAIL_BYTES))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Read backward until the requested record count

When recent JSONL records average more than 1 KiB—for example, trace records containing request or response bodies—the fixed count * 1024 window contains fewer than count complete records; the 4 MiB cap also prevents --lines 10000 from returning 10,000 moderately sized records. The function then silently returns the undersized result and can expose a sampled fragment as { raw }; read backward in chunks until enough newline-delimited records or the beginning of the file is reached.

Useful? React with 👍 / 👎.

Comment thread src/logs.ts
* `rawback logs show` must not pull all of it into memory to print fifty lines.
*/
export async function readTail(path: string, count: number): Promise<string[]> {
const handle = await open(path, 'r').catch(() => undefined)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Report non-ENOENT failures when opening logs

If the log exists but cannot be opened—for example because of EACCES after permissions or ownership changed—this catch converts the failure into an empty successful result, so logs show misleadingly reports no records and exits zero. Suppress only the missing-file case and propagate other errors so the CLI emits an actionable failure and nonzero status.

AGENTS.md reference: AGENTS.md:L84-L86

Useful? React with 👍 / 👎.

Comment thread README.md

```bash
rawback logs show --level warn # what recently failed, and why
rawback -v photos upload ~/raw # re-run the failing command with more detail

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use --path in the troubleshooting upload example

The documented command cannot rerun an upload: photos upload defines the directory as the demanded --path option and has no positional path, so copying this example produces a validation error instead of collecting verbose upload diagnostics. Change it to rawback -v photos upload --path ~/raw.

AGENTS.md reference: AGENTS.md:L124-L125

Useful? React with 👍 / 👎.

The SDK's logging core is now pino, so this tracks its API and its file
naming.

- Records use pino's argument order, `logger.info({ event }, 'message')`.
- `pino-roll` numbers every file from `1`, so there is no `cli.log` to open
  by name. `logs show` asks the SDK for the active file, and reports
  cleanly when the app has not logged yet; `logs path` and `logs purge`
  needed no change, since both already worked off a pattern.
- `--level` filtering keeps its own copy of pino's level numbers rather
  than importing a map the SDK no longer exports.
- `flushCommandLogger` goes through the SDK's `flushLogger`: the rolling
  destination is asynchronous, and a CLI process exits long before it
  would drain on its own.

Docs updated for the numbering and to name pino, since the format is now
one `pino-pretty` reads directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwoxuU7mv4yFQnecnKoaPY

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d934668584

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/logs.ts
Comment on lines +83 to +86
const environment = await readEnvironment(
dependencies.configPath,
environmentName(dependencies),
).catch(() => undefined)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject invalid environments before touching logs

When --env names an unknown environment, this catch silently falls back to the default log directory instead of propagating readEnvironment's validation error. In the destructive case, rawback --env prodcution logs purge --yes can therefore delete the default environment's logs rather than rejecting the typo; malformed configuration is similarly treated as absent. Let configuration errors propagate before listing, reading, or purging files.

AGENTS.md reference: AGENTS.md:L91-L92

Useful? React with 👍 / 👎.

Comment thread src/logs.ts
Comment on lines +158 to +165
return {
...(typeof record.time === 'string' ? { time: record.time } : {}),
...(typeof record.level === 'string' ? { level: record.level } : {}),
...(typeof record.component === 'string' ? { component: record.component } : {}),
...(typeof record.event === 'string' ? { event: record.event } : {}),
...(typeof record.msg === 'string' ? { msg: record.msg } : {}),
...(ids?.traceId ? { traceId: ids.traceId } : {}),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve structured fields in displayed log records

For valid JSON records, this reconstruction discards every field outside the small allowlist, including the documented HTTP method, path, status, duration, cf-ray, x-request-id, error details, headers, and trace-level bodies. Consequently both the human presenter and logs show --json lose the diagnostic context users enabled -v/-vv to inspect; preserve the parsed record while deriving any presentation-specific fields instead of replacing it.

Useful? React with 👍 / 👎.

Comment thread docs/configuration.md
Comment on lines +449 to +450
tail -f ~/.rawback/logs/cli.log | jq -c '{time, level, event, ids}'
jq 'select(.levelValue >= 40)' ~/.rawback/logs/cli.log

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reference the numbered active log file

These troubleshooting commands target cli.log, but the configured pino-roll sink numbers every file and the implementation explicitly discovers the highest-numbered cli.N.log as the active file. A user copying either command therefore gets a missing-file error even while CLI logs exist; make the example discover the active numbered file or direct users through rawback logs path.

AGENTS.md reference: AGENTS.md:L75-L78

Useful? React with 👍 / 👎.

The SDK now rejects `maxFiles: 0` rather than silently rewriting it to 1;
a rolling log always has a file open, so `file: false` is how writing to
disk is switched off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwoxuU7mv4yFQnecnKoaPY
@AnnatarHe
AnnatarHe merged commit 454436f into main Sep 21, 2026
2 of 7 checks passed
@AnnatarHe
AnnatarHe deleted the claude/jolly-hamilton-h8r2a2 branch September 21, 2026 06:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants