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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/target
TODO.md

99 changes: 98 additions & 1 deletion Cargo.lock

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

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "tofu-cli"
version = "0.1.0"
version = "0.1.1"
edition = "2024"
description = "A terminal-first webhook relay CLI for local development."
homepage = "https://trytofu.dev"
Expand All @@ -19,6 +19,7 @@ dirs = "6"
comfy-table = "7"
colored = "3.0.0"
thiserror = "2.0.18"
chrono = "0.4"

[[bin]]
name = "tofu"
Expand Down
60 changes: 48 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Tofu CLI

A terminal-first webhook relay for local development. Give webhook providers one stable URL, forward events to localhost, inspect deliveries, and replay requests.
A terminal-first webhook relay for local development. Give webhook providers one stable URL, forward events to your dev or preview services, inspect requests, and replay them after code changes.

> Tofu is in early access. Some features are still being tested.

Expand All @@ -16,39 +16,74 @@ curl -fsSL https://trytofu.dev/install | sh
brew install trytofu/tap/tofu
```

## What you need

- A Tofu account
- A local, staging, or preview URL that your webhook provider can reach
- One workspace selected in the CLI

## Quick start

```bash
tofu login

tofu workspaces list
tofu workspaces use <workspace-slug>

tofu hooks create stripe --name "Stripe"
tofu hooks url stripe
tofu targets set local "http://127.0.0.1:3000/api/webhooks/stripe" --hook stripe
tofu watch stripe
tofu hooks url stripe

tofu events list --hook stripe
tofu replay latest --hook stripe
```

1. `tofu login` opens browser-based device approval.
2. `tofu hooks create` gives you a stable URL to paste into the provider dashboard.
3. `tofu targets set` points that hook at your local server.
4. `tofu watch` streams incoming events and delivery results.
1. `tofu login` opens browser-based device approval and saves your token.
2. `tofu workspaces use` chooses where hooks and targets are managed.
3. `tofu hooks create` creates a stable provider URL.
4. `tofu targets set` forwards matching events to your service.
5. `tofu hooks url` prints the URL to paste into Stripe, GitHub, Resend, or another provider.
6. `tofu events list` and `tofu replay` let you inspect and resend retained events.

`http://127.0.0.1:3000/...` only works if your local service is reachable from the Tofu API. In many setups that means using a tunnel, staging URL, or preview deployment as the target URL.

## Commands

| Command | Description |
|---------|-------------|
| `tofu login` | Authenticate via browser device approval |
| `tofu login --token <token>` | Authenticate with a personal API token |
| `tofu logout` | Clear the saved token |
| `tofu whoami` | Show the authenticated user |
| `tofu usage` | Show plan usage and remaining limits |
| `tofu workspaces list` | List workspaces you can access |
| `tofu workspaces use <slug>` | Set the active workspace |
| `tofu workspaces create <name-or-slug>` | Create a workspace |
| `tofu workspaces members list` | List active workspace members |
| `tofu workspaces members add <email>` | Add a member to the active workspace |
| `tofu hooks create <slug>` | Create a named hook with a stable provider URL |
| `tofu hooks url <slug>` | Print the provider URL |
| `tofu hooks list` | List hooks in the current workspace |
| `tofu hooks status <slug>` | Show hook details and targets |
| `tofu targets add <name> <url> --hook <slug>` | Add a forwarding target |
| `tofu targets set <name> <url> --hook <slug>` | Set a forwarding target |
| `tofu targets list --hook <slug>` | List targets for a hook |
| `tofu watch <slug>` | Stream live events and delivery status |
| `tofu replay latest --hook <slug>` | Replay the most recent event |
| `tofu targets enable <name> --hook <slug>` | Enable a target |
| `tofu targets disable <name> --hook <slug>` | Disable a target |
| `tofu targets delete <name> --hook <slug>` | Delete a target |
| `tofu events list --hook <slug>` | List recent events |
| `tofu events latest --hook <slug>` | Show the latest event for a hook |
| `tofu events show <event-id>` | Show event headers, body preview, and deliveries |
| `tofu events expire <event-id>` | Remove stored payload immediately |
| `tofu replay <event-id>` | Replay an event to enabled targets |
| `tofu replay <event-id> --target <name>` | Replay an event to one target |
| `tofu replay latest --hook <slug>` | Replay the most recent event for a hook |
| `tofu health` | Check API connectivity |
| `tofu config show` | Show current config (token masked) |

All commands accept `--json` for machine-readable output.
All commands accept `--json` for machine-readable output. Use `--api-base-url <url>` to temporarily point the CLI at another API.

Live event streaming with `tofu watch` is not included in this rewrite yet. Until it lands, use `tofu events list --hook <slug>` or `tofu events latest --hook <slug>` after sending a test webhook.

## Configuration

Expand All @@ -59,15 +94,16 @@ api_base_url = "https://api.trytofu.dev"
token = "tofu_pat_xxxxxxxx"
```

You can override the config location with `TOFU_CONFIG_PATH`, which is useful for scripts and tests.

## What Tofu does

- Accepts webhooks from providers (Stripe, GitHub, Resend, etc.) at a stable URL
- Stores request body and headers temporarily for replay
- Forwards events to one or more local/dev targets
- Streams live delivery status via `tofu watch`
- Replays retained events after code changes

Tofu is not an ngrok replacement. Use ngrok, localhost, staging, or a preview URL as your target. Tofu gives the provider a stable URL so you don't have to reconfigure it when your local setup changes.
Tofu is not an ngrok replacement. Use ngrok, staging, a preview URL, or another reachable endpoint as your target. Tofu gives the provider a stable URL so you do not have to reconfigure provider webhooks when your target changes.

## Docs

Expand Down
26 changes: 17 additions & 9 deletions src/api/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,13 @@ impl ApiClient {

/// Workspace
impl ApiClient {
pub async fn get_workspace(&self, workspace_id: &str) -> Result<Workspace, ApiError> {
let token = self.token.as_ref().ok_or(ApiError::NotAuthenticated)?;
let url = format!("{}/api/workspaces/{workspace_id}", self.base_url);
let r = self.get_authenticated_response(&url, token).await?;
r.json().await.map_err(ApiError::Request)
}

pub async fn list_workspaces(&self) -> Result<Vec<Workspace>, ApiError> {
let token = self.token.as_ref().ok_or(ApiError::NotAuthenticated)?;
let url = format!("{}/api/workspaces", self.base_url);
Expand Down Expand Up @@ -350,15 +357,6 @@ impl ApiClient {
.await?;
response.json::<Hook>().await.map_err(ApiError::Request)
}

#[allow(dead_code)]
pub async fn get_hook(&self, hook_id: &str) -> Result<Hook, ApiError> {
let token = self.token.as_ref().ok_or(ApiError::NotAuthenticated)?;
let url = format!("{}/api/hooks/{hook_id}", self.base_url);

let response = self.get_authenticated_response(&url, token).await?;
response.json::<Hook>().await.map_err(ApiError::Request)
}
}

// Targets
Expand Down Expand Up @@ -493,3 +491,13 @@ impl ApiClient {
Ok(())
}
}

impl ApiClient {
pub async fn stream_events(&self, hook_id: &str) -> Result<reqwest::Response, ApiError> {
let token = self.token.as_ref().ok_or(ApiError::NotAuthenticated)?;
let url = format!("{}/api/hooks/{hook_id}/events/stream", self.base_url);

let r = self.get_authenticated_response(&url, token).await?;
Ok(r)
}
}
10 changes: 10 additions & 0 deletions src/commands/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pub struct Cli {

#[derive(Subcommand)]
pub enum Commands {
/// Print the CLI version
Version,
/// Checks API health
Health,
/// Log in with your browser approval or an API token
Expand Down Expand Up @@ -70,6 +72,14 @@ pub enum Commands {
#[arg(long, help = "Target name to replay to")]
target: Option<String>,
},
/// Watch events for a hook
Watch {
slug: String,
#[arg(long, help = "Show delivery updates")]
deliveries: bool,
#[arg(long, help = "Filter delivery updates to a target name")]
target: Option<String>,
},
}

#[derive(Subcommand)]
Expand Down
Loading
Loading