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
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ Temporary endpoints are for short-term, non-sensitive debugging. Private endpoin
| `barestash events list` | List recent events (`--endpoint`, `--limit`, `--json`) |
| `barestash events latest` | Show the most recent event (`--endpoint`, `--json`) |
| `barestash events show <event-id>` | Show event details (`--json`) |
| `barestash events tail` | Follow new events (`--endpoint`, `--last`, `--headers`, `--body`, `--poll-interval`) |
| `barestash events tail` | Follow new events (`--endpoint`, `--last`, `--headers`, `--body`, `--view`, `--poll-interval`) |
| `barestash events stream` | Stream events as JSON Lines (`--endpoint`) |

Sensitive headers such as `Authorization` and `stripe-signature` are shown as `[REDACTED]` in CLI output.
Expand Down Expand Up @@ -261,12 +261,30 @@ Treat it like a secret-handling surface:

## Output formats

- Human-readable output is the default for interactive use.
- Human-readable output is the default. Interactive terminals use color,
Unicode symbols, and tables sized to the terminal width. Redirected and
piped output, and terminals with `TERM=dumb`, remain plain text.
- Pass `--json` for structured output suitable for scripts.
- `barestash events stream` always writes JSON Lines (NDJSON) to stdout for machine consumers.
- Set `NO_COLOR` to disable ANSI color without disabling the interactive
layout.

Keep stdout reserved for structured data. Diagnostic messages go to stderr.

Use the optional live dashboard when following events in an interactive
terminal:

```bash
barestash events tail --view
```

The dashboard redraws the terminal with the endpoint status, received count,
latest event time, and recent events. It uses a compact table on narrow
terminals, tracks terminal resizes, and limits visible events to the detected
terminal height. Stop it with `Ctrl+C`. Because the dashboard requires
terminal control, `--view` is unavailable when stdout is redirected or piped,
or when `TERM=dumb`, and cannot be combined with `--headers` or `--body`.

## Links

- Repository: [github.com/barestashhq/cli](https://github.com/barestashhq/cli)
Expand Down
28 changes: 28 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,31 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

## String width utilities

This distribution includes `string-width`, `get-east-asian-width`,
`strip-ansi`, and `ansi-regex` under the following license.

MIT License

Copyright (c) Sindre Sorhus <sindresorhus@gmail.com>
(<https://sindresorhus.com>)

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
46 changes: 43 additions & 3 deletions docs/cli-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,23 @@ Short aliases may be considered later, but the documented command surface should

Commands should clearly distinguish between:

- Human-readable terminal output: `--format table`
- Machine-readable JSON / JSONL (NDJSON) output: `--format json`
- Human-readable terminal output: the default
- Machine-readable JSON output: explicit `--json`
- Machine-readable JSONL (NDJSON) output: `events stream`

Default output should be optimized for humans.

Machine-readable output should be explicitly requested with flags such as the above. For continuous streams, JSONL is preferred.
When stdout is an interactive terminal, human output uses ANSI color, Unicode
symbols, syntax highlighting, and tables fitted to the detected terminal
width. Narrow terminals truncate flexible columns and fall back to vertical
key/value rows when a table cannot fit. When stdout is redirected or piped, or
when `TERM=dumb`, human output remains undecorated plain text. The `NO_COLOR`
environment variable disables ANSI color while preserving the interactive
layout.

Machine-readable output must be explicitly requested. Structured JSON and
JSONL output must never contain ANSI escapes. For continuous machine-readable
streams, JSONL is preferred.

### CLI and AI agent friendliness

Expand Down Expand Up @@ -791,16 +802,45 @@ Flags:
- `--last`: show the last N events before watching begins
- `--headers`: include headers in output
- `--body`: include body in output
- `--view`: show a screen-updating dashboard in an interactive terminal
- `--poll-interval`: specify polling interval (default `2s`; supports `ms` / `s` / `m`; unitless values are not allowed)

```bash
barestash events tail --endpoint ep_abc123
barestash events tail --last 10
barestash events tail --headers
barestash events tail --body
barestash events tail --view
barestash events tail --poll-interval 2s
```

`--view` displays the selected endpoint, watch status, number of events
received by the dashboard, latest event time, and up to ten recent event rows.
Below the full table width it uses a compact table without size and content
type columns. It limits visible events further when needed so the rendered
frame fits the detected terminal height, and reads current terminal dimensions
again on every redraw so resizing takes effect. It redraws only stdout and
exits silently and successfully on `Ctrl+C`, like the regular tail display.
On exit it resets terminal styling and moves the cursor to a fresh line. The
option requires interactive stdout with terminal control support, rejects
`TERM=dumb`, and is incompatible with `--headers` and `--body`; invalid
combinations fail before making an API request. API host diagnostics and errors
continue to use stderr.

Example dashboard:

```text
BARESTASH ep_abc123

Status ● watching
Requests 2
Last event 2026-07-05T12:04:32.000Z

TIME METHOD PATH SIZE CONTENT-TYPE EVENT
12:04:32 POST /webhook/stripe 8.4 KB application/json evt_01JDEF
12:04:18 POST /webhook/github 2.1 KB application/json evt_01JABC
```

Example output for `barestash events tail --headers --body`:

```text
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"commander": "15.0.0",
"esbuild": "0.28.1",
"markdownlint-cli2": "^0.23.0",
"string-width": "8.2.2",
"tsx": "4.23.0",
"typescript": "6.0.3",
"vitest": "4.1.10"
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 11 additions & 4 deletions src/barestash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { realpathSync } from "node:fs";
import { fileURLToPath } from "node:url";

import type { CliIo } from "./domain/ports.js";
import { createTerminalOutput } from "./infrastructure/terminal-output.js";

const isMain =
process.argv[1] !== undefined &&
Expand All @@ -12,8 +13,14 @@ const isMain =
if (isMain) {
const { runCliProcess } = await import("./process.js");

process.exitCode = await runCliProcess(process.argv.slice(2), {
stdout: (line: string) => console.log(line),
stderr: (line: string) => console.error(line),
} satisfies CliIo);
process.exitCode = await runCliProcess(
process.argv.slice(2),
{
stdout: (line: string) => console.log(line),
stderr: (line: string) => console.error(line),
} satisfies CliIo,
{
terminal: createTerminalOutput(process.stdout, process.env),
},
);
}
16 changes: 16 additions & 0 deletions src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
CredentialWriteResult,
Sleeper,
StdinReader,
TerminalOutput,
} from "./domain/ports.js";
import {
formatApiHost,
Expand All @@ -34,6 +35,7 @@ import {
ReadlineConfirmer,
TimerSleeper,
} from "./infrastructure/terminal.js";
import { OutputRenderer } from "./presentation/output/renderer.js";

export const DEFAULT_API_URL = "http://localhost:8787";

Expand Down Expand Up @@ -64,6 +66,7 @@ export type CliOptions = {
) => Promise<CredentialWriteResult> | Promise<void>;
deleteCredential?: () => Promise<void>;
credentialLock?: CredentialLock;
terminal?: TerminalOutput;
};

export type CliState = {
Expand All @@ -82,6 +85,8 @@ export type AppDeps = {
browserOpener: SystemBrowserOpener;
now: () => Date;
deviceName: string;
output: OutputRenderer;
terminal: TerminalOutput;
};

function getEnv(options: CliOptions): Record<string, string | undefined> {
Expand Down Expand Up @@ -149,6 +154,15 @@ export function createAppDeps(
state: CliState,
): AppDeps {
const env = getEnv(options);
const terminal: TerminalOutput = options.terminal ?? {
capabilities: {
interactive: false,
color: false,
unicode: false,
width: 80,
},
write: () => {},
};
const configStore = new FileConfigStore({
env,
platformName: platform(),
Expand Down Expand Up @@ -222,6 +236,8 @@ export function createAppDeps(
browserOpener: new SystemBrowserOpener(platform(), options.openBrowser),
now: options.now ?? (() => new Date()),
deviceName: options.deviceName ?? hostname(),
output: new OutputRenderer(io, terminal.capabilities),
terminal,
};
}

Expand Down
15 changes: 15 additions & 0 deletions src/domain/ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ export type CliIo = {
stderr: (line: string) => void;
};

/** @public */
export type TerminalCapabilities = {
interactive: boolean;
color: boolean;
unicode: boolean;
width: number;
height?: number;
};

/** @public */
export type TerminalOutput = {
capabilities: TerminalCapabilities;
write: (value: string) => void;
};

/** @public */
export type CliFetch = (
input: string | URL | Request,
Expand Down
62 changes: 62 additions & 0 deletions src/infrastructure/terminal-output.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, expect, it, vi } from "vitest";

import { createTerminalOutput } from "./terminal-output.js";

describe("createTerminalOutput", () => {
it("detects interactive color terminals and their dimensions", () => {
const write = vi.fn(() => true);
const stdout = { isTTY: true, columns: 120, rows: 36, write };
const terminal = createTerminalOutput(stdout, {
TERM: "xterm-256color",
});

expect(terminal.capabilities).toEqual({
interactive: true,
color: true,
unicode: true,
width: 120,
height: 36,
});

terminal.write("frame");
expect(write).toHaveBeenCalledWith("frame");

stdout.columns = 60;
stdout.rows = 20;
expect(terminal.capabilities.width).toBe(60);
expect(terminal.capabilities.height).toBe(20);
});

it("disables decoration for pipes, dumb terminals, and NO_COLOR", () => {
const write = vi.fn(() => true);

expect(
createTerminalOutput({ isTTY: false, columns: undefined, write }, {})
.capabilities,
).toEqual({
interactive: false,
color: false,
unicode: false,
width: 80,
height: 24,
});
expect(
createTerminalOutput(
{ isTTY: true, columns: 80, write },
{ NO_COLOR: "", TERM: "xterm" },
).capabilities.color,
).toBe(false);
expect(
createTerminalOutput(
{ isTTY: true, columns: 120, rows: 36, write },
{ TERM: "dumb" },
).capabilities,
).toEqual({
interactive: false,
color: false,
unicode: false,
width: 80,
height: 24,
});
});
});
Loading