mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
refactor(cli): restructure flat commands into 12 subcommand groups (#585)
This commit is contained in:
@@ -10,21 +10,14 @@ cargo install --path crates/sprout-cli
|
||||
|
||||
## Authentication
|
||||
|
||||
Two modes, checked in order:
|
||||
|
||||
| Priority | Env Var | Mode | Use Case |
|
||||
|----------|---------|------|----------|
|
||||
| 1 | `SPROUT_PRIVATE_KEY` | NIP-98 Schnorr signature | Agents with a keypair |
|
||||
| 2 | `SPROUT_PUBKEY` | X-Pubkey header (dev relay only) | Local development |
|
||||
| Env Var | Mode | Use Case |
|
||||
|---------|------|----------|
|
||||
| `SPROUT_PRIVATE_KEY` | NIP-98 Schnorr signature | Agents with a keypair |
|
||||
|
||||
```bash
|
||||
# Option 1: Private key (NIP-98 signed requests)
|
||||
# Private key identity (NIP-98 signed requests)
|
||||
export SPROUT_PRIVATE_KEY="nsec1..."
|
||||
sprout list-channels
|
||||
|
||||
# Option 2: Dev mode (no auth)
|
||||
export SPROUT_PUBKEY="<hex>"
|
||||
sprout list-channels
|
||||
sprout channels list
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -36,106 +29,117 @@ All output is JSON on stdout. Errors are JSON on stderr. Exit codes: 0=ok, 1=use
|
||||
export SPROUT_RELAY_URL="https://relay.example.com"
|
||||
|
||||
# Messages
|
||||
sprout send-message --channel <uuid> --content "Hello"
|
||||
sprout send-message --channel <uuid> --content "Reply" --reply-to <event-id> --broadcast
|
||||
sprout get-messages --channel <uuid> --limit 20
|
||||
sprout get-thread --channel <uuid> --event <event-id>
|
||||
sprout search --query "architecture"
|
||||
sprout edit-message --event <event-id> --content "Updated text"
|
||||
sprout delete-message --event <event-id>
|
||||
sprout messages send --channel <uuid> --content "Hello"
|
||||
sprout messages send --channel <uuid> --content "Reply" --reply-to <event-id> --broadcast
|
||||
sprout messages get --channel <uuid> --limit 20
|
||||
sprout messages thread --channel <uuid> --event <event-id>
|
||||
sprout messages search --query "architecture"
|
||||
sprout messages edit --event <event-id> --content "Updated text"
|
||||
sprout messages delete --event <event-id>
|
||||
|
||||
# Diffs
|
||||
sprout send-diff-message --channel <uuid> --diff - --repo https://github.com/org/repo --commit abc123 < diff.patch
|
||||
sprout messages send-diff --channel <uuid> --diff - --repo https://github.com/org/repo --commit abc123 < diff.patch
|
||||
|
||||
# Channels
|
||||
sprout list-channels
|
||||
sprout create-channel --name "my-channel" --type stream --visibility open
|
||||
sprout join-channel --channel <uuid>
|
||||
sprout set-channel-topic --channel <uuid> --topic "New topic"
|
||||
sprout channels list
|
||||
sprout channels create --name "my-channel" --type stream --visibility open
|
||||
sprout channels join --channel <uuid>
|
||||
sprout channels topic --channel <uuid> --topic "New topic"
|
||||
|
||||
# Reactions
|
||||
sprout add-reaction --event <event-id> --emoji "👍"
|
||||
sprout get-reactions --event <event-id>
|
||||
sprout reactions add --event <event-id> --emoji "👍"
|
||||
sprout reactions get --event <event-id>
|
||||
|
||||
# Users & Presence
|
||||
sprout get-users # your own profile
|
||||
sprout get-users --pubkey <hex> # single user
|
||||
sprout get-users --pubkey <hex> --pubkey <hex> # batch (max 200)
|
||||
sprout set-presence --status online
|
||||
sprout users get # your own profile
|
||||
sprout users get --pubkey <hex> # single user
|
||||
sprout users get --pubkey <hex> --pubkey <hex> # batch (max 200)
|
||||
sprout users set-presence --status online
|
||||
|
||||
# DMs
|
||||
sprout open-dm --pubkey <hex>
|
||||
sprout list-dms
|
||||
sprout dms open --pubkey <hex>
|
||||
sprout dms list
|
||||
|
||||
# Workflows
|
||||
sprout list-workflows --channel <uuid>
|
||||
sprout trigger-workflow --workflow <uuid>
|
||||
sprout approve-step --token <uuid> --approved true
|
||||
sprout workflows list --channel <uuid>
|
||||
sprout workflows trigger --workflow <uuid>
|
||||
sprout workflows approve --token <uuid>
|
||||
sprout workflows approve --token <uuid> --approved false --note "needs revision"
|
||||
|
||||
# Forum
|
||||
sprout vote-on-post --event <event-id> --direction up
|
||||
sprout messages vote --event <event-id> --direction up
|
||||
|
||||
# Canvas
|
||||
sprout get-canvas --channel <uuid>
|
||||
sprout set-canvas --channel <uuid> --content "# Welcome"
|
||||
sprout canvas get --channel <uuid>
|
||||
sprout canvas set --channel <uuid> --content "# Welcome"
|
||||
|
||||
# Pipe to jq
|
||||
sprout list-channels | jq '.[].name'
|
||||
sprout channels list | jq '.[].name'
|
||||
```
|
||||
|
||||
## All 44 Commands
|
||||
## 54 Subcommands across 12 Groups
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `send-message` | Send a message to a channel |
|
||||
| `send-diff-message` | Send a code diff with metadata |
|
||||
| `edit-message` | Edit a message you sent |
|
||||
| `delete-message` | Delete a message |
|
||||
| `get-messages` | List messages in a channel |
|
||||
| `get-thread` | Get a message thread |
|
||||
| `search` | Full-text search |
|
||||
| `list-channels` | List channels |
|
||||
| `get-channel` | Get channel details |
|
||||
| `create-channel` | Create a channel |
|
||||
| `update-channel` | Update channel name/description |
|
||||
| `set-channel-topic` | Set channel topic |
|
||||
| `set-channel-purpose` | Set channel purpose |
|
||||
| `join-channel` | Join a channel |
|
||||
| `leave-channel` | Leave a channel |
|
||||
| `archive-channel` | Archive a channel |
|
||||
| `unarchive-channel` | Unarchive a channel |
|
||||
| `delete-channel` | Delete a channel |
|
||||
| `list-channel-members` | List channel members |
|
||||
| `add-channel-member` | Add a member |
|
||||
| `remove-channel-member` | Remove a member |
|
||||
| `get-canvas` | Get channel canvas |
|
||||
| `set-canvas` | Set channel canvas |
|
||||
| `add-reaction` | React to a message |
|
||||
| `remove-reaction` | Remove a reaction |
|
||||
| `get-reactions` | List reactions |
|
||||
| `list-dms` | List DM conversations |
|
||||
| `open-dm` | Open a DM (1–8 pubkeys) |
|
||||
| `add-dm-member` | Add member to DM group |
|
||||
| `get-users` | Get user profile(s) |
|
||||
| `set-profile` | Update your profile |
|
||||
| `get-presence` | Get presence status |
|
||||
| `set-presence` | Set presence status |
|
||||
| `set-channel-add-policy` | Set who can add you to channels |
|
||||
| `list-workflows` | List workflows |
|
||||
| `create-workflow` | Create a workflow |
|
||||
| `update-workflow` | Update a workflow |
|
||||
| `delete-workflow` | Delete a workflow |
|
||||
| `trigger-workflow` | Trigger a workflow |
|
||||
| `get-workflow-runs` | Get workflow run history |
|
||||
| `get-workflow` | Get workflow definition |
|
||||
| `approve-step` | Approve/deny a workflow step |
|
||||
| `get-feed` | Get your activity feed |
|
||||
| `vote-on-post` | Vote on a forum post |
|
||||
| Group | Subcommand | Description |
|
||||
|-------|-----------|-------------|
|
||||
| `messages` | `send` | Send a message to a channel |
|
||||
| | `send-diff` | Send a code diff with metadata |
|
||||
| | `edit` | Edit a message you sent |
|
||||
| | `delete` | Delete a message |
|
||||
| | `get` | List messages in a channel |
|
||||
| | `thread` | Get a message thread |
|
||||
| | `search` | Full-text search |
|
||||
| | `vote` | Vote on a forum post |
|
||||
| `channels` | `list` | List channels |
|
||||
| | `get` | Get channel details |
|
||||
| | `create` | Create a channel |
|
||||
| | `update` | Update channel name/description |
|
||||
| | `topic` | Set channel topic |
|
||||
| | `purpose` | Set channel purpose |
|
||||
| | `join` | Join a channel |
|
||||
| | `leave` | Leave a channel |
|
||||
| | `archive` | Archive a channel |
|
||||
| | `unarchive` | Unarchive a channel |
|
||||
| | `delete` | Delete a channel |
|
||||
| | `members` | List channel members |
|
||||
| | `add-member` | Add a member |
|
||||
| | `remove-member` | Remove a member |
|
||||
| `canvas` | `get` | Get channel canvas |
|
||||
| | `set` | Set channel canvas |
|
||||
| `reactions` | `add` | React to a message |
|
||||
| | `remove` | Remove a reaction |
|
||||
| | `get` | List reactions |
|
||||
| `dms` | `list` | List DM conversations |
|
||||
| | `open` | Open a DM (1–8 pubkeys) |
|
||||
| | `add-member` | Add member to DM group |
|
||||
| `users` | `get` | Get user profile(s) |
|
||||
| | `set-profile` | Update your profile |
|
||||
| | `presence` | Get presence status |
|
||||
| | `set-presence` | Set presence status |
|
||||
| `workflows` | `list` | List workflows |
|
||||
| | `get` | Get workflow definition |
|
||||
| | `create` | Create a workflow |
|
||||
| | `update` | Update a workflow |
|
||||
| | `delete` | Delete a workflow |
|
||||
| | `trigger` | Trigger a workflow |
|
||||
| | `runs` | Get workflow run history |
|
||||
| | `approve` | Approve/deny a workflow step |
|
||||
| `feed` | `get` | Get your activity feed |
|
||||
| `social` | `publish` | Publish a NIP-01 note |
|
||||
| | `set-contacts` | Set NIP-02 contact list |
|
||||
| | `event` | Get a Nostr event |
|
||||
| | `notes` | Get notes for a user |
|
||||
| | `contacts` | Get NIP-02 contact list |
|
||||
| `repos` | `create` | Announce a git repository (NIP-34) |
|
||||
| | `get` | Get a repository announcement |
|
||||
| | `list` | List repository announcements |
|
||||
| `upload` | `file` | Upload a file to the Blossom store |
|
||||
| `pack` | `validate` | Validate a persona pack (local, no relay) |
|
||||
| | `inspect` | Inspect a persona pack (local, no relay) |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
sprout <command> [flags]
|
||||
sprout <group> <subcommand> [flags]
|
||||
│
|
||||
├─ main.rs ──▶ commands/*.rs ──▶ client.rs ──▶ Sprout Relay REST API
|
||||
│ (clap) (handlers) (reqwest)
|
||||
|
||||
@@ -71,44 +71,27 @@ cargo run -p sprout-admin -- mint-token \
|
||||
|
||||
This generates a keypair and prints:
|
||||
- **Private key (nsec)** — save for `SPROUT_PRIVATE_KEY` testing
|
||||
- **API Token** — save as `SPROUT_API_TOKEN`
|
||||
- **Pubkey** — save for `SPROUT_PUBKEY` testing
|
||||
|
||||
Export:
|
||||
|
||||
```bash
|
||||
export SPROUT_RELAY_URL="http://localhost:3000"
|
||||
export SPROUT_API_TOKEN="sprout_tok_..."
|
||||
export SPROUT_PRIVATE_KEY="nsec1..." # from the mint output
|
||||
```
|
||||
|
||||
### Option B: sprout auth (NIP-98, self-mintable scopes only)
|
||||
|
||||
Tests the CLI's own auth flow. Cannot mint `admin:channels`.
|
||||
|
||||
```bash
|
||||
export SPROUT_PRIVATE_KEY="nsec1..."
|
||||
export SPROUT_RELAY_URL="http://localhost:3000"
|
||||
cargo run -p sprout-cli -- auth
|
||||
# Prints a token string to stdout
|
||||
```
|
||||
|
||||
### Scope reference
|
||||
|
||||
| Scope | Self-mintable | Needed for |
|
||||
|-------|:---:|------------|
|
||||
| `messages:read` | ✅ | get-messages, get-thread, search, get-feed |
|
||||
| `messages:write` | ✅ | send-message, edit-message, delete-message, reactions, vote |
|
||||
| `channels:read` | ✅ | list-channels, get-channel, list-members |
|
||||
| `channels:write` | ✅ | create-channel, update-channel, join, leave, topic, purpose |
|
||||
| `users:read` | ✅ | get-users, get-presence |
|
||||
| `users:write` | ✅ | set-profile, set-presence, set-channel-add-policy |
|
||||
| `messages:read` | ✅ | `messages get`, `messages thread`, `messages search`, `feed get` |
|
||||
| `messages:write` | ✅ | `messages send`, `messages edit`, `messages delete`, `reactions`, `messages vote` |
|
||||
| `channels:read` | ✅ | `channels list`, `channels get`, `channels members` |
|
||||
| `channels:write` | ✅ | `channels create`, `channels update`, `channels join`, `channels leave`, `channels topic`, `channels purpose` |
|
||||
| `users:read` | ✅ | `users get`, `users presence` |
|
||||
| `users:write` | ✅ | `users set-profile`, `users set-presence` |
|
||||
| `files:read` | ✅ | — |
|
||||
| `files:write` | ✅ | — |
|
||||
| `admin:channels` | ❌ | archive, unarchive, delete-channel, add/remove-channel-member |
|
||||
|
||||
**Use Option A for full testing.** Option B covers most commands but skips
|
||||
admin operations.
|
||||
| `admin:channels` | ❌ | `channels archive`, `channels unarchive`, `channels delete`, `channels add-member`, `channels remove-member` |
|
||||
|
||||
---
|
||||
|
||||
@@ -116,7 +99,7 @@ admin operations.
|
||||
|
||||
```bash
|
||||
cargo test -p sprout-cli
|
||||
# Expected: 38 passed, 0 failed
|
||||
# Expected: see cargo test -p sprout-cli for current count
|
||||
|
||||
cargo clippy -p sprout-cli -- -D warnings
|
||||
# Expected: zero warnings
|
||||
@@ -127,147 +110,120 @@ cargo clippy -p sprout-cli -- -D warnings
|
||||
## 6. Live Testing — Command by Command
|
||||
|
||||
Run each command, verify exit code 0 and check output. Most commands
|
||||
return JSON (pipe through `jq .` to validate). Exceptions: `auth` prints
|
||||
a raw token string, and `delete-token`/`delete-all-tokens` may return
|
||||
empty (204). Commands are ordered so earlier ones create resources that
|
||||
later ones need.
|
||||
return JSON (pipe through `jq .` to validate). Commands are ordered so
|
||||
earlier ones create resources that later ones need.
|
||||
|
||||
### 6.1 Auth & Tokens
|
||||
### 6.1 Channels
|
||||
|
||||
```bash
|
||||
# list-tokens — list existing tokens
|
||||
sprout list-tokens | jq .
|
||||
|
||||
# auth — mint a new token (requires SPROUT_PRIVATE_KEY)
|
||||
SPROUT_PRIVATE_KEY="nsec1..." sprout auth
|
||||
# Should print: sprout_tok_...
|
||||
|
||||
# delete-token — delete a specific token by UUID
|
||||
# ⚠️ Do NOT delete the token you're currently using (SPROUT_API_TOKEN).
|
||||
# Mint a throwaway token first, then delete it:
|
||||
THROWAWAY=$(sprout auth) # mint a new token
|
||||
THROWAWAY_LIST=$(SPROUT_API_TOKEN="$THROWAWAY" sprout list-tokens)
|
||||
# Filter by name to avoid deleting the wrong token
|
||||
THROWAWAY_ID=$(echo "$THROWAWAY_LIST" | jq -r '[.[] // .tokens[] | select(.name == "sprout-cli")][0].id // empty')
|
||||
sprout delete-token --id "$THROWAWAY_ID"
|
||||
# May return 204 (empty) or JSON — both are success
|
||||
|
||||
# delete-all-tokens — DESTRUCTIVE, deletes all tokens for this pubkey
|
||||
# sprout delete-all-tokens
|
||||
# ⚠️ Only run this if you're about to re-mint
|
||||
```
|
||||
|
||||
### 6.2 Channels
|
||||
|
||||
```bash
|
||||
# create-channel (stream)
|
||||
sprout create-channel --name "test-stream" --type stream --visibility open \
|
||||
# channels create (stream)
|
||||
sprout channels create --name "test-stream" --type stream --visibility open \
|
||||
--description "CLI test channel" | jq .
|
||||
# Save the channel ID:
|
||||
CHANNEL_ID=$(sprout create-channel --name "test-cli" --type stream --visibility open | jq -r '.id')
|
||||
CHANNEL_ID=$(sprout channels create --name "test-cli" --type stream --visibility open | jq -r '.id')
|
||||
|
||||
# create-channel (forum) — needed for vote-on-post later
|
||||
FORUM_ID=$(sprout create-channel --name "test-forum" --type forum --visibility open | jq -r '.id')
|
||||
# channels create (forum) — needed for messages vote later
|
||||
FORUM_ID=$(sprout channels create --name "test-forum" --type forum --visibility open | jq -r '.id')
|
||||
|
||||
# list-channels
|
||||
sprout list-channels | jq .
|
||||
sprout list-channels --visibility open | jq .
|
||||
sprout list-channels --member | jq .
|
||||
# channels list
|
||||
sprout channels list | jq .
|
||||
sprout channels list --visibility open | jq .
|
||||
sprout channels list --member | jq .
|
||||
|
||||
# get-channel
|
||||
sprout get-channel --channel "$CHANNEL_ID" | jq .
|
||||
# channels get
|
||||
sprout channels get --channel "$CHANNEL_ID" | jq .
|
||||
|
||||
# update-channel
|
||||
sprout update-channel --channel "$CHANNEL_ID" --name "test-cli-updated" \
|
||||
# channels update
|
||||
sprout channels update --channel "$CHANNEL_ID" --name "test-cli-updated" \
|
||||
--description "Updated" | jq .
|
||||
|
||||
# set-channel-topic
|
||||
sprout set-channel-topic --channel "$CHANNEL_ID" --topic "Test topic" | jq .
|
||||
# channels topic
|
||||
sprout channels topic --channel "$CHANNEL_ID" --topic "Test topic" | jq .
|
||||
|
||||
# set-channel-purpose
|
||||
sprout set-channel-purpose --channel "$CHANNEL_ID" --purpose "Testing" | jq .
|
||||
# channels purpose
|
||||
sprout channels purpose --channel "$CHANNEL_ID" --purpose "Testing" | jq .
|
||||
|
||||
# join-channel (may already be a member from create)
|
||||
sprout join-channel --channel "$CHANNEL_ID" | jq .
|
||||
# channels join (may already be a member from create)
|
||||
sprout channels join --channel "$CHANNEL_ID" | jq .
|
||||
|
||||
# leave-channel
|
||||
sprout leave-channel --channel "$CHANNEL_ID" | jq .
|
||||
# channels leave
|
||||
sprout channels leave --channel "$CHANNEL_ID" | jq .
|
||||
|
||||
# Re-join so we can send messages
|
||||
sprout join-channel --channel "$CHANNEL_ID" | jq .
|
||||
sprout channels join --channel "$CHANNEL_ID" | jq .
|
||||
|
||||
# archive-channel (requires admin:channels scope)
|
||||
sprout archive-channel --channel "$CHANNEL_ID" | jq .
|
||||
# channels archive (requires admin:channels scope)
|
||||
sprout channels archive --channel "$CHANNEL_ID" | jq .
|
||||
|
||||
# unarchive-channel
|
||||
sprout unarchive-channel --channel "$CHANNEL_ID" | jq .
|
||||
# channels unarchive
|
||||
sprout channels unarchive --channel "$CHANNEL_ID" | jq .
|
||||
```
|
||||
|
||||
### 6.3 Canvas
|
||||
### 6.2 Canvas
|
||||
|
||||
```bash
|
||||
# set-canvas
|
||||
sprout set-canvas --channel "$CHANNEL_ID" --content "# Test Canvas" | jq .
|
||||
# canvas set
|
||||
sprout canvas set --channel "$CHANNEL_ID" --content "# Test Canvas" | jq .
|
||||
|
||||
# set-canvas from stdin
|
||||
echo "# Canvas from stdin" | sprout set-canvas --channel "$CHANNEL_ID" --content - | jq .
|
||||
# canvas set from stdin
|
||||
echo "# Canvas from stdin" | sprout canvas set --channel "$CHANNEL_ID" --content - | jq .
|
||||
|
||||
# get-canvas
|
||||
sprout get-canvas --channel "$CHANNEL_ID" | jq .
|
||||
# canvas get
|
||||
sprout canvas get --channel "$CHANNEL_ID" | jq .
|
||||
```
|
||||
|
||||
### 6.4 Messages
|
||||
### 6.3 Messages
|
||||
|
||||
```bash
|
||||
# send-message
|
||||
MSG=$(sprout send-message --channel "$CHANNEL_ID" --content "Hello from CLI test" | jq .)
|
||||
# messages send
|
||||
MSG=$(sprout messages send --channel "$CHANNEL_ID" --content "Hello from CLI test" | jq .)
|
||||
echo "$MSG"
|
||||
EVENT_ID=$(echo "$MSG" | jq -r '.id // .event_id')
|
||||
|
||||
# send-message with reply + broadcast
|
||||
REPLY=$(sprout send-message --channel "$CHANNEL_ID" --content "Reply" \
|
||||
# messages send with reply + broadcast
|
||||
REPLY=$(sprout messages send --channel "$CHANNEL_ID" --content "Reply" \
|
||||
--reply-to "$EVENT_ID" --broadcast | jq .)
|
||||
echo "$REPLY"
|
||||
REPLY_ID=$(echo "$REPLY" | jq -r '.id // .event_id')
|
||||
|
||||
# send-message with mentions
|
||||
sprout send-message --channel "$CHANNEL_ID" --content "Hey @someone" \
|
||||
# messages send with mentions
|
||||
sprout messages send --channel "$CHANNEL_ID" --content "Hey @someone" \
|
||||
--mention "0000000000000000000000000000000000000000000000000000000000000001" | jq .
|
||||
|
||||
# get-messages
|
||||
sprout get-messages --channel "$CHANNEL_ID" | jq .
|
||||
sprout get-messages --channel "$CHANNEL_ID" --limit 5 | jq .
|
||||
# messages get
|
||||
sprout messages get --channel "$CHANNEL_ID" | jq .
|
||||
sprout messages get --channel "$CHANNEL_ID" --limit 5 | jq .
|
||||
|
||||
# get-thread
|
||||
sprout get-thread --channel "$CHANNEL_ID" --event "$EVENT_ID" | jq .
|
||||
# messages thread
|
||||
sprout messages thread --channel "$CHANNEL_ID" --event "$EVENT_ID" | jq .
|
||||
|
||||
# search
|
||||
sprout search --query "Hello" | jq .
|
||||
sprout search --query "CLI test" --limit 5 | jq .
|
||||
# messages search
|
||||
sprout messages search --query "Hello" | jq .
|
||||
sprout messages search --query "CLI test" --limit 5 | jq .
|
||||
|
||||
# edit-message
|
||||
sprout edit-message --event "$EVENT_ID" --content "Edited by CLI test" | jq .
|
||||
# messages edit
|
||||
sprout messages edit --event "$EVENT_ID" --content "Edited by CLI test" | jq .
|
||||
|
||||
# delete-message
|
||||
sprout delete-message --event "$REPLY_ID" | jq .
|
||||
# messages delete
|
||||
sprout messages delete --event "$REPLY_ID" | jq .
|
||||
```
|
||||
|
||||
### 6.5 Diff Messages
|
||||
### 6.4 Diff Messages
|
||||
|
||||
```bash
|
||||
# send-diff-message from stdin
|
||||
# messages send-diff from stdin
|
||||
echo '--- a/foo.rs
|
||||
+++ b/foo.rs
|
||||
@@ -1,3 +1,3 @@
|
||||
-fn old() {}
|
||||
+fn new() {}' | sprout send-diff-message \
|
||||
+fn new() {}' | sprout messages send-diff \
|
||||
--channel "$CHANNEL_ID" \
|
||||
--diff - \
|
||||
--repo "https://github.com/example/repo" \
|
||||
--commit "abcdef1234567890abcdef1234567890abcdef12" | jq .
|
||||
|
||||
# send-diff-message with metadata
|
||||
echo "diff content" | sprout send-diff-message \
|
||||
# messages send-diff with metadata
|
||||
echo "diff content" | sprout messages send-diff \
|
||||
--channel "$CHANNEL_ID" \
|
||||
--diff - \
|
||||
--repo "https://github.com/example/repo" \
|
||||
@@ -276,8 +232,8 @@ echo "diff content" | sprout send-diff-message \
|
||||
--lang "rust" \
|
||||
--description "Refactored main" | jq .
|
||||
|
||||
# send-diff-message with branch + PR metadata
|
||||
echo "diff content" | sprout send-diff-message \
|
||||
# messages send-diff with branch + PR metadata
|
||||
echo "diff content" | sprout messages send-diff \
|
||||
--channel "$CHANNEL_ID" \
|
||||
--diff - \
|
||||
--repo "https://github.com/example/repo" \
|
||||
@@ -288,99 +244,92 @@ echo "diff content" | sprout send-diff-message \
|
||||
--pr 42 | jq .
|
||||
```
|
||||
|
||||
### 6.6 Reactions
|
||||
### 6.5 Reactions
|
||||
|
||||
```bash
|
||||
# Send a message to react to
|
||||
REACT_MSG=$(sprout send-message --channel "$CHANNEL_ID" --content "React to this")
|
||||
REACT_MSG=$(sprout messages send --channel "$CHANNEL_ID" --content "React to this")
|
||||
REACT_ID=$(echo "$REACT_MSG" | jq -r '.id // .event_id')
|
||||
|
||||
# add-reaction
|
||||
sprout add-reaction --event "$REACT_ID" --emoji "👍" | jq .
|
||||
# reactions add
|
||||
sprout reactions add --event "$REACT_ID" --emoji "👍" | jq .
|
||||
|
||||
# get-reactions
|
||||
sprout get-reactions --event "$REACT_ID" | jq .
|
||||
# reactions get
|
||||
sprout reactions get --event "$REACT_ID" | jq .
|
||||
|
||||
# remove-reaction
|
||||
sprout remove-reaction --event "$REACT_ID" --emoji "👍" | jq .
|
||||
# reactions remove
|
||||
sprout reactions remove --event "$REACT_ID" --emoji "👍" | jq .
|
||||
```
|
||||
|
||||
### 6.7 DMs
|
||||
### 6.6 DMs
|
||||
|
||||
```bash
|
||||
# list-dms
|
||||
sprout list-dms | jq .
|
||||
# dms list
|
||||
sprout dms list | jq .
|
||||
|
||||
# open-dm (needs a real pubkey — use your own or a test one)
|
||||
# dms open (needs a real pubkey — use your own or a test one)
|
||||
# Get your own pubkey first:
|
||||
MY_PUBKEY=$(sprout get-users | jq -r '.pubkey // .[0].pubkey // empty')
|
||||
MY_PUBKEY=$(sprout users get | jq -r '.pubkey // .[0].pubkey // empty')
|
||||
echo "My pubkey: $MY_PUBKEY"
|
||||
|
||||
# open-dm with a synthetic pubkey (relay will create the user)
|
||||
DM_RESULT=$(sprout open-dm --pubkey "0000000000000000000000000000000000000000000000000000000000000001")
|
||||
# dms open with a synthetic pubkey (relay will create the user)
|
||||
DM_RESULT=$(sprout dms open --pubkey "0000000000000000000000000000000000000000000000000000000000000001")
|
||||
echo "$DM_RESULT" | jq .
|
||||
DM_ID=$(echo "$DM_RESULT" | jq -r '.channel_id // .id // empty')
|
||||
|
||||
# add-dm-member (requires messages:write scope — NOT admin:channels)
|
||||
sprout add-dm-member --channel "$DM_ID" \
|
||||
# dms add-member (requires messages:write scope — NOT admin:channels)
|
||||
sprout dms add-member --channel "$DM_ID" \
|
||||
--pubkey "0000000000000000000000000000000000000000000000000000000000000002" | jq .
|
||||
```
|
||||
|
||||
### 6.8 Users & Presence
|
||||
### 6.7 Users & Presence
|
||||
|
||||
```bash
|
||||
# get-users — own profile (0 pubkeys)
|
||||
sprout get-users | jq .
|
||||
# users get — own profile (0 pubkeys)
|
||||
sprout users get | jq .
|
||||
|
||||
# get-users — single pubkey
|
||||
sprout get-users --pubkey "$MY_PUBKEY" | jq .
|
||||
# users get — single pubkey
|
||||
sprout users get --pubkey "$MY_PUBKEY" | jq .
|
||||
|
||||
# get-users — batch (2+ pubkeys)
|
||||
sprout get-users --pubkey "$MY_PUBKEY" --pubkey "$MY_PUBKEY" | jq .
|
||||
# users get — batch (2+ pubkeys)
|
||||
sprout users get --pubkey "$MY_PUBKEY" --pubkey "$MY_PUBKEY" | jq .
|
||||
|
||||
# set-profile
|
||||
sprout set-profile --name "CLI Test Agent" --about "Testing sprout-cli" | jq .
|
||||
# users set-profile
|
||||
sprout users set-profile --name "CLI Test Agent" --about "Testing sprout-cli" | jq .
|
||||
|
||||
# get-presence
|
||||
sprout get-presence --pubkeys "$MY_PUBKEY" | jq .
|
||||
# users presence
|
||||
sprout users presence --pubkeys "$MY_PUBKEY" | jq .
|
||||
|
||||
# set-presence
|
||||
sprout set-presence --status online | jq .
|
||||
sprout set-presence --status away | jq .
|
||||
sprout set-presence --status offline | jq .
|
||||
|
||||
# set-channel-add-policy
|
||||
sprout set-channel-add-policy --policy anyone | jq .
|
||||
sprout set-channel-add-policy --policy owner_only | jq .
|
||||
sprout set-channel-add-policy --policy nobody | jq .
|
||||
# Reset to default
|
||||
sprout set-channel-add-policy --policy anyone | jq .
|
||||
# users set-presence
|
||||
sprout users set-presence --status online | jq .
|
||||
sprout users set-presence --status away | jq .
|
||||
sprout users set-presence --status offline | jq .
|
||||
```
|
||||
|
||||
### 6.9 Channel Members (add/remove require admin:channels)
|
||||
### 6.8 Channel Members (add/remove require admin:channels)
|
||||
|
||||
```bash
|
||||
# add-channel-member
|
||||
sprout add-channel-member --channel "$CHANNEL_ID" \
|
||||
# channels add-member
|
||||
sprout channels add-member --channel "$CHANNEL_ID" \
|
||||
--pubkey "0000000000000000000000000000000000000000000000000000000000000001" \
|
||||
--role member | jq .
|
||||
|
||||
# list-channel-members
|
||||
sprout list-channel-members --channel "$CHANNEL_ID" | jq .
|
||||
# channels members
|
||||
sprout channels members --channel "$CHANNEL_ID" | jq .
|
||||
|
||||
# remove-channel-member
|
||||
sprout remove-channel-member --channel "$CHANNEL_ID" \
|
||||
# channels remove-member
|
||||
sprout channels remove-member --channel "$CHANNEL_ID" \
|
||||
--pubkey "0000000000000000000000000000000000000000000000000000000000000001" | jq .
|
||||
```
|
||||
|
||||
### 6.10 Workflows
|
||||
### 6.9 Workflows
|
||||
|
||||
```bash
|
||||
# create-workflow
|
||||
# workflows create
|
||||
# NOTE: trigger uses `on:` tag (serde internally tagged enum).
|
||||
# Valid triggers: message_posted, reaction_added, diff_posted, schedule, webhook
|
||||
# Steps use `action:` tag: send_message, send_dm, set_channel_topic, add_reaction, etc.
|
||||
WF=$(sprout create-workflow --channel "$CHANNEL_ID" \
|
||||
WF=$(sprout workflows create --channel "$CHANNEL_ID" \
|
||||
--yaml 'name: test-wf
|
||||
trigger:
|
||||
on: webhook
|
||||
@@ -391,14 +340,14 @@ steps:
|
||||
echo "$WF"
|
||||
WF_ID=$(echo "$WF" | jq -r '.id')
|
||||
|
||||
# list-workflows
|
||||
sprout list-workflows --channel "$CHANNEL_ID" | jq .
|
||||
# workflows list
|
||||
sprout workflows list --channel "$CHANNEL_ID" | jq .
|
||||
|
||||
# get-workflow
|
||||
sprout get-workflow --workflow "$WF_ID" | jq .
|
||||
# workflows get
|
||||
sprout workflows get --workflow "$WF_ID" | jq .
|
||||
|
||||
# update-workflow
|
||||
sprout update-workflow --workflow "$WF_ID" \
|
||||
# workflows update
|
||||
sprout workflows update --workflow "$WF_ID" \
|
||||
--yaml 'name: test-wf-updated
|
||||
trigger:
|
||||
on: webhook
|
||||
@@ -407,43 +356,44 @@ steps:
|
||||
action: send_message
|
||||
text: "Updated"' | jq .
|
||||
|
||||
# trigger-workflow
|
||||
sprout trigger-workflow --workflow "$WF_ID" | jq .
|
||||
# workflows trigger
|
||||
sprout workflows trigger --workflow "$WF_ID" | jq .
|
||||
|
||||
# get-workflow-runs
|
||||
sprout get-workflow-runs --workflow "$WF_ID" | jq .
|
||||
# workflows runs
|
||||
sprout workflows runs --workflow "$WF_ID" | jq .
|
||||
|
||||
# approve-step — requires a workflow run waiting for approval
|
||||
# workflows approve — requires a workflow run waiting for approval
|
||||
# This is hard to test ad-hoc without a workflow that has an approval gate.
|
||||
# Test the validation instead:
|
||||
sprout approve-step --token "00000000-0000-0000-0000-000000000000" --approved true 2>&1 || true
|
||||
sprout workflows approve --token "00000000-0000-0000-0000-000000000000" 2>&1 || true
|
||||
# Should fail with relay error (token not found), not a validation error
|
||||
# To test the deny path: sprout workflows approve --token <UUID> --approved false
|
||||
|
||||
# delete-workflow
|
||||
sprout delete-workflow --workflow "$WF_ID" | jq .
|
||||
# workflows delete
|
||||
sprout workflows delete --workflow "$WF_ID" | jq .
|
||||
```
|
||||
|
||||
### 6.11 Feed
|
||||
### 6.10 Feed
|
||||
|
||||
```bash
|
||||
sprout get-feed | jq .
|
||||
sprout get-feed --limit 5 | jq .
|
||||
sprout feed get | jq .
|
||||
sprout feed get --limit 5 | jq .
|
||||
```
|
||||
|
||||
### 6.12 Forum & Voting
|
||||
### 6.11 Forum & Voting
|
||||
|
||||
```bash
|
||||
# Send a forum post (kind 45001) to the forum channel
|
||||
FORUM_POST=$(sprout send-message --channel "$FORUM_ID" \
|
||||
FORUM_POST=$(sprout messages send --channel "$FORUM_ID" \
|
||||
--content "Forum post for vote testing" --kind 45001 | jq .)
|
||||
echo "$FORUM_POST"
|
||||
FORUM_EVENT_ID=$(echo "$FORUM_POST" | jq -r '.id // .event_id')
|
||||
|
||||
# vote-on-post (up)
|
||||
sprout vote-on-post --event "$FORUM_EVENT_ID" --direction up | jq .
|
||||
# messages vote (up)
|
||||
sprout messages vote --event "$FORUM_EVENT_ID" --direction up | jq .
|
||||
|
||||
# vote-on-post (down)
|
||||
sprout vote-on-post --event "$FORUM_EVENT_ID" --direction down | jq .
|
||||
# messages vote (down)
|
||||
sprout messages vote --event "$FORUM_EVENT_ID" --direction down | jq .
|
||||
```
|
||||
|
||||
---
|
||||
@@ -454,69 +404,56 @@ Verify the CLI produces correct JSON on stderr and correct exit codes.
|
||||
|
||||
```bash
|
||||
# Exit 1: Invalid UUID
|
||||
sprout get-channel --channel "not-a-uuid" 2>&1; echo "exit: $?"
|
||||
sprout channels get --channel "not-a-uuid" 2>&1; echo "exit: $?"
|
||||
# stderr: {"error":"user_error","message":"invalid UUID: not-a-uuid"}
|
||||
# exit: 1
|
||||
|
||||
# Exit 1: Invalid hex64
|
||||
sprout delete-message --event "not-hex" 2>&1; echo "exit: $?"
|
||||
sprout messages delete --event "not-hex" 2>&1; echo "exit: $?"
|
||||
# stderr: {"error":"user_error","message":"must be a 64-character hex string: not-hex"}
|
||||
# exit: 1
|
||||
|
||||
# Exit 1: Invalid --approved value
|
||||
sprout approve-step --token "00000000-0000-0000-0000-000000000000" \
|
||||
--approved maybe 2>&1; echo "exit: $?"
|
||||
# stderr: {"error":"user_error","message":"--approved must be 'true' or 'false' (got: maybe)"}
|
||||
# exit: 1
|
||||
|
||||
# Exit 1: Invalid --type value
|
||||
sprout create-channel --name x --type invalid --visibility open 2>&1; echo "exit: $?"
|
||||
sprout channels create --name x --type invalid --visibility open 2>&1; echo "exit: $?"
|
||||
# stderr: {"error":"user_error","message":"--type must be 'stream' or 'forum' (got: invalid)"}
|
||||
# exit: 1
|
||||
|
||||
# Exit 1: Invalid --direction value
|
||||
sprout vote-on-post --event "$(printf '0%.0s' {1..64})" \
|
||||
sprout messages vote --event "$(printf '0%.0s' {1..64})" \
|
||||
--direction sideways 2>&1; echo "exit: $?"
|
||||
# exit: 1
|
||||
|
||||
# Exit 1: Empty body guard
|
||||
sprout set-profile 2>&1; echo "exit: $?"
|
||||
sprout users set-profile 2>&1; echo "exit: $?"
|
||||
# exit: 1 (at least one field required)
|
||||
|
||||
# Exit 3: No auth configured
|
||||
env -u SPROUT_API_TOKEN -u SPROUT_PRIVATE_KEY -u SPROUT_PUBKEY \
|
||||
cargo run -p sprout-cli -- list-channels 2>&1; echo "exit: $?"
|
||||
# stderr: {"error":"auth_error","message":"auth error: Set SPROUT_API_TOKEN, SPROUT_PRIVATE_KEY, or SPROUT_PUBKEY"}
|
||||
env -u SPROUT_PRIVATE_KEY \
|
||||
cargo run -p sprout-cli -- channels list 2>&1; echo "exit: $?"
|
||||
# stderr: {"error":"auth_error","message":"SPROUT_PRIVATE_KEY is required (use --private-key or set env var)"}
|
||||
# exit: 3
|
||||
|
||||
# Exit 2: Non-existent channel (valid UUID)
|
||||
sprout get-channel --channel "00000000-0000-0000-0000-000000000000" 2>&1; echo "exit: $?"
|
||||
sprout channels get --channel "00000000-0000-0000-0000-000000000000" 2>&1; echo "exit: $?"
|
||||
# stderr: {"error":"relay_error","message":"..."}
|
||||
# exit: 2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Auth Mode Testing
|
||||
## 8. Auth Testing
|
||||
|
||||
Test all three authentication tiers.
|
||||
Test authentication.
|
||||
|
||||
```bash
|
||||
# Mode 1: Bearer token (SPROUT_API_TOKEN)
|
||||
SPROUT_API_TOKEN="sprout_tok_..." sprout list-channels | jq .
|
||||
# Should succeed
|
||||
|
||||
# Mode 2: Private key auto-mint (SPROUT_PRIVATE_KEY)
|
||||
SPROUT_PRIVATE_KEY="nsec1..." sprout list-channels | jq .
|
||||
# Should succeed (mints a 1-day token at startup)
|
||||
|
||||
# Mode 3: Dev mode (SPROUT_PUBKEY) — only works with SPROUT_REQUIRE_AUTH_TOKEN=false
|
||||
SPROUT_PUBKEY="<your-64-char-hex-pubkey>" sprout list-channels | jq .
|
||||
# Private key (SPROUT_PRIVATE_KEY)
|
||||
SPROUT_PRIVATE_KEY="nsec1..." sprout channels list | jq .
|
||||
# Should succeed
|
||||
|
||||
# No auth → exit 3
|
||||
env -u SPROUT_API_TOKEN -u SPROUT_PRIVATE_KEY -u SPROUT_PUBKEY \
|
||||
cargo run -p sprout-cli -- list-channels 2>&1; echo "exit: $?"
|
||||
env -u SPROUT_PRIVATE_KEY \
|
||||
cargo run -p sprout-cli -- channels list 2>&1; echo "exit: $?"
|
||||
# stderr: {"error":"auth_error","message":"SPROUT_PRIVATE_KEY is required (use --private-key or set env var)"}
|
||||
# exit: 3
|
||||
```
|
||||
|
||||
@@ -526,8 +463,8 @@ env -u SPROUT_API_TOKEN -u SPROUT_PRIVATE_KEY -u SPROUT_PUBKEY \
|
||||
|
||||
```bash
|
||||
# Delete test channels
|
||||
sprout delete-channel --channel "$CHANNEL_ID" | jq .
|
||||
sprout delete-channel --channel "$FORUM_ID" | jq .
|
||||
sprout channels delete --channel "$CHANNEL_ID" | jq .
|
||||
sprout channels delete --channel "$FORUM_ID" | jq .
|
||||
```
|
||||
|
||||
---
|
||||
@@ -536,51 +473,57 @@ sprout delete-channel --channel "$FORUM_ID" | jq .
|
||||
|
||||
| # | Command | Tested | Notes |
|
||||
|---|---------|:------:|-------|
|
||||
| 1 | `send-message` | ☐ | Basic, reply, broadcast, mentions |
|
||||
| 2 | `send-diff-message` | ☐ | Stdin, metadata, branch/PR |
|
||||
| 3 | `edit-message` | ☐ | |
|
||||
| 4 | `delete-message` | ☐ | |
|
||||
| 5 | `get-messages` | ☐ | With limit |
|
||||
| 6 | `get-thread` | ☐ | |
|
||||
| 7 | `search` | ☐ | With limit |
|
||||
| 8 | `list-channels` | ☐ | With visibility, member |
|
||||
| 9 | `get-channel` | ☐ | |
|
||||
| 10 | `create-channel` | ☐ | Stream and forum |
|
||||
| 11 | `update-channel` | ☐ | |
|
||||
| 12 | `set-channel-topic` | ☐ | |
|
||||
| 13 | `set-channel-purpose` | ☐ | |
|
||||
| 14 | `join-channel` | ☐ | |
|
||||
| 15 | `leave-channel` | ☐ | |
|
||||
| 16 | `archive-channel` | ☐ | Needs admin:channels |
|
||||
| 17 | `unarchive-channel` | ☐ | Needs admin:channels |
|
||||
| 18 | `delete-channel` | ☐ | Needs admin:channels |
|
||||
| 19 | `list-channel-members` | ☐ | |
|
||||
| 20 | `add-channel-member` | ☐ | Needs admin:channels |
|
||||
| 21 | `remove-channel-member` | ☐ | Needs admin:channels |
|
||||
| 22 | `get-canvas` | ☐ | |
|
||||
| 23 | `set-canvas` | ☐ | Direct and stdin |
|
||||
| 24 | `add-reaction` | ☐ | |
|
||||
| 25 | `remove-reaction` | ☐ | |
|
||||
| 26 | `get-reactions` | ☐ | |
|
||||
| 27 | `list-dms` | ☐ | |
|
||||
| 28 | `open-dm` | ☐ | |
|
||||
| 29 | `add-dm-member` | ☐ | Needs messages:write |
|
||||
| 30 | `get-users` | ☐ | Self, single, batch |
|
||||
| 31 | `set-profile` | ☐ | |
|
||||
| 32 | `get-presence` | ☐ | |
|
||||
| 33 | `set-presence` | ☐ | online, away, offline |
|
||||
| 34 | `set-channel-add-policy` | ☐ | anyone, owner_only, nobody |
|
||||
| 35 | `list-workflows` | ☐ | |
|
||||
| 36 | `create-workflow` | ☐ | |
|
||||
| 37 | `update-workflow` | ☐ | |
|
||||
| 38 | `delete-workflow` | ☐ | |
|
||||
| 39 | `trigger-workflow` | ☐ | |
|
||||
| 40 | `get-workflow-runs` | ☐ | |
|
||||
| 41 | `get-workflow` | ☐ | |
|
||||
| 42 | `approve-step` | ☐ | Validation only (needs approval gate) |
|
||||
| 43 | `get-feed` | ☐ | |
|
||||
| 44 | `vote-on-post` | ☐ | Up and down |
|
||||
| 45 | `auth` | ☐ | Mint token via NIP-98 |
|
||||
| 46 | `list-tokens` | ☐ | |
|
||||
| 47 | `delete-token` | ☐ | |
|
||||
| 48 | `delete-all-tokens` | ☐ | Optional (destructive) |
|
||||
| 1 | `messages send` | ☐ | Basic, reply, broadcast, mentions |
|
||||
| 2 | `messages send-diff` | ☐ | Stdin, metadata, branch/PR |
|
||||
| 3 | `messages edit` | ☐ | |
|
||||
| 4 | `messages delete` | ☐ | |
|
||||
| 5 | `messages get` | ☐ | With limit |
|
||||
| 6 | `messages thread` | ☐ | |
|
||||
| 7 | `messages search` | ☐ | With limit |
|
||||
| 8 | `messages vote` | ☐ | Up and down |
|
||||
| 9 | `channels list` | ☐ | With visibility, member |
|
||||
| 10 | `channels get` | ☐ | |
|
||||
| 11 | `channels create` | ☐ | Stream and forum |
|
||||
| 12 | `channels update` | ☐ | |
|
||||
| 13 | `channels topic` | ☐ | |
|
||||
| 14 | `channels purpose` | ☐ | |
|
||||
| 15 | `channels join` | ☐ | |
|
||||
| 16 | `channels leave` | ☐ | |
|
||||
| 17 | `channels archive` | ☐ | Needs admin:channels |
|
||||
| 18 | `channels unarchive` | ☐ | Needs admin:channels |
|
||||
| 19 | `channels delete` | ☐ | Needs admin:channels |
|
||||
| 20 | `channels members` | ☐ | |
|
||||
| 21 | `channels add-member` | ☐ | Needs admin:channels |
|
||||
| 22 | `channels remove-member` | ☐ | Needs admin:channels |
|
||||
| 23 | `canvas get` | ☐ | |
|
||||
| 24 | `canvas set` | ☐ | Direct and stdin |
|
||||
| 25 | `reactions add` | ☐ | |
|
||||
| 26 | `reactions remove` | ☐ | |
|
||||
| 27 | `reactions get` | ☐ | |
|
||||
| 28 | `dms list` | ☐ | |
|
||||
| 29 | `dms open` | ☐ | |
|
||||
| 30 | `dms add-member` | ☐ | Needs messages:write |
|
||||
| 31 | `users get` | ☐ | Self, single, batch |
|
||||
| 32 | `users set-profile` | ☐ | |
|
||||
| 33 | `users presence` | ☐ | |
|
||||
| 34 | `users set-presence` | ☐ | online, away, offline |
|
||||
| 35 | `workflows list` | ☐ | |
|
||||
| 36 | `workflows create` | ☐ | |
|
||||
| 37 | `workflows update` | ☐ | |
|
||||
| 38 | `workflows delete` | ☐ | |
|
||||
| 39 | `workflows trigger` | ☐ | |
|
||||
| 40 | `workflows runs` | ☐ | |
|
||||
| 41 | `workflows get` | ☐ | |
|
||||
| 42 | `workflows approve` | ☐ | Validation only (needs approval gate); bare = approve, `--approved false` = deny |
|
||||
| 43 | `feed get` | ☐ | |
|
||||
| 44 | `social publish` | ☐ | |
|
||||
| 45 | `social set-contacts` | ☐ | |
|
||||
| 46 | `social event` | ☐ | |
|
||||
| 47 | `social notes` | ☐ | |
|
||||
| 48 | `social contacts` | ☐ | |
|
||||
| 49 | `repos create` | ☐ | |
|
||||
| 50 | `repos get` | ☐ | |
|
||||
| 51 | `repos list` | ☐ | |
|
||||
| 52 | `upload file` | ☐ | |
|
||||
| 53 | `pack validate` | ☐ | Local, no relay |
|
||||
| 54 | `pack inspect` | ☐ | Local, no relay |
|
||||
|
||||
@@ -2,22 +2,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::client::SproutClient;
|
||||
use crate::error::CliError;
|
||||
use crate::validate::{read_or_stdin, validate_hex64, validate_uuid};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn parse_uuid(s: &str) -> Result<Uuid, CliError> {
|
||||
Uuid::parse_str(s).map_err(|e| CliError::Usage(format!("invalid channel UUID: {e}")))
|
||||
}
|
||||
|
||||
fn sign_and_submit_builder(
|
||||
builder: nostr::EventBuilder,
|
||||
client: &SproutClient,
|
||||
) -> Result<nostr::Event, CliError> {
|
||||
client.sign_event(builder)
|
||||
}
|
||||
use crate::validate::{parse_uuid, read_or_stdin, validate_hex64, validate_uuid};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Read commands — POST /query
|
||||
@@ -127,7 +112,7 @@ pub async fn cmd_create_channel(
|
||||
sprout_sdk::build_create_channel(channel_uuid, name, Some(vis), Some(ct), description)
|
||||
.map_err(|e| CliError::Other(format!("build_create_channel failed: {e}")))?;
|
||||
|
||||
let event = sign_and_submit_builder(builder, client)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
let resp = client.submit_event(event).await?;
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
@@ -144,13 +129,12 @@ pub async fn cmd_update_channel(
|
||||
"at least one field required (--name, --description)".into(),
|
||||
));
|
||||
}
|
||||
validate_uuid(channel_id)?;
|
||||
let channel_uuid = parse_uuid(channel_id)?;
|
||||
|
||||
let builder = sprout_sdk::build_update_channel(channel_uuid, name, description)
|
||||
.map_err(|e| CliError::Other(format!("build_update_channel failed: {e}")))?;
|
||||
|
||||
let event = sign_and_submit_builder(builder, client)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
let resp = client.submit_event(event).await?;
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
@@ -161,13 +145,12 @@ pub async fn cmd_set_channel_topic(
|
||||
channel_id: &str,
|
||||
topic: &str,
|
||||
) -> Result<(), CliError> {
|
||||
validate_uuid(channel_id)?;
|
||||
let channel_uuid = parse_uuid(channel_id)?;
|
||||
|
||||
let builder = sprout_sdk::build_set_topic(channel_uuid, topic)
|
||||
.map_err(|e| CliError::Other(format!("build_set_topic failed: {e}")))?;
|
||||
|
||||
let event = sign_and_submit_builder(builder, client)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
let resp = client.submit_event(event).await?;
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
@@ -178,52 +161,48 @@ pub async fn cmd_set_channel_purpose(
|
||||
channel_id: &str,
|
||||
purpose: &str,
|
||||
) -> Result<(), CliError> {
|
||||
validate_uuid(channel_id)?;
|
||||
let channel_uuid = parse_uuid(channel_id)?;
|
||||
|
||||
let builder = sprout_sdk::build_set_purpose(channel_uuid, purpose)
|
||||
.map_err(|e| CliError::Other(format!("build_set_purpose failed: {e}")))?;
|
||||
|
||||
let event = sign_and_submit_builder(builder, client)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
let resp = client.submit_event(event).await?;
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cmd_join_channel(client: &SproutClient, channel_id: &str) -> Result<(), CliError> {
|
||||
validate_uuid(channel_id)?;
|
||||
let channel_uuid = parse_uuid(channel_id)?;
|
||||
|
||||
let builder = sprout_sdk::build_join(channel_uuid)
|
||||
.map_err(|e| CliError::Other(format!("build_join failed: {e}")))?;
|
||||
|
||||
let event = sign_and_submit_builder(builder, client)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
let resp = client.submit_event(event).await?;
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cmd_leave_channel(client: &SproutClient, channel_id: &str) -> Result<(), CliError> {
|
||||
validate_uuid(channel_id)?;
|
||||
let channel_uuid = parse_uuid(channel_id)?;
|
||||
|
||||
let builder = sprout_sdk::build_leave(channel_uuid)
|
||||
.map_err(|e| CliError::Other(format!("build_leave failed: {e}")))?;
|
||||
|
||||
let event = sign_and_submit_builder(builder, client)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
let resp = client.submit_event(event).await?;
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cmd_archive_channel(client: &SproutClient, channel_id: &str) -> Result<(), CliError> {
|
||||
validate_uuid(channel_id)?;
|
||||
let channel_uuid = parse_uuid(channel_id)?;
|
||||
|
||||
let builder = sprout_sdk::build_archive(channel_uuid)
|
||||
.map_err(|e| CliError::Other(format!("build_archive failed: {e}")))?;
|
||||
|
||||
let event = sign_and_submit_builder(builder, client)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
let resp = client.submit_event(event).await?;
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
@@ -233,26 +212,24 @@ pub async fn cmd_unarchive_channel(
|
||||
client: &SproutClient,
|
||||
channel_id: &str,
|
||||
) -> Result<(), CliError> {
|
||||
validate_uuid(channel_id)?;
|
||||
let channel_uuid = parse_uuid(channel_id)?;
|
||||
|
||||
let builder = sprout_sdk::build_unarchive(channel_uuid)
|
||||
.map_err(|e| CliError::Other(format!("build_unarchive failed: {e}")))?;
|
||||
|
||||
let event = sign_and_submit_builder(builder, client)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
let resp = client.submit_event(event).await?;
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cmd_delete_channel(client: &SproutClient, channel_id: &str) -> Result<(), CliError> {
|
||||
validate_uuid(channel_id)?;
|
||||
let channel_uuid = parse_uuid(channel_id)?;
|
||||
|
||||
let builder = sprout_sdk::build_delete_channel(channel_uuid)
|
||||
.map_err(|e| CliError::Other(format!("build_delete_channel failed: {e}")))?;
|
||||
|
||||
let event = sign_and_submit_builder(builder, client)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
let resp = client.submit_event(event).await?;
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
@@ -264,7 +241,6 @@ pub async fn cmd_add_channel_member(
|
||||
pubkey: &str,
|
||||
role: Option<&str>,
|
||||
) -> Result<(), CliError> {
|
||||
validate_uuid(channel_id)?;
|
||||
validate_hex64(pubkey)?;
|
||||
let channel_uuid = parse_uuid(channel_id)?;
|
||||
|
||||
@@ -284,7 +260,7 @@ pub async fn cmd_add_channel_member(
|
||||
let builder = sprout_sdk::build_add_member(channel_uuid, pubkey, typed_role)
|
||||
.map_err(|e| CliError::Other(format!("build_add_member failed: {e}")))?;
|
||||
|
||||
let event = sign_and_submit_builder(builder, client)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
let resp = client.submit_event(event).await?;
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
@@ -295,14 +271,13 @@ pub async fn cmd_remove_channel_member(
|
||||
channel_id: &str,
|
||||
pubkey: &str,
|
||||
) -> Result<(), CliError> {
|
||||
validate_uuid(channel_id)?;
|
||||
validate_hex64(pubkey)?;
|
||||
let channel_uuid = parse_uuid(channel_id)?;
|
||||
|
||||
let builder = sprout_sdk::build_remove_member(channel_uuid, pubkey)
|
||||
.map_err(|e| CliError::Other(format!("build_remove_member failed: {e}")))?;
|
||||
|
||||
let event = sign_and_submit_builder(builder, client)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
let resp = client.submit_event(event).await?;
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
@@ -313,15 +288,77 @@ pub async fn cmd_set_canvas(
|
||||
channel_id: &str,
|
||||
content: &str,
|
||||
) -> Result<(), CliError> {
|
||||
validate_uuid(channel_id)?;
|
||||
let content = read_or_stdin(content)?;
|
||||
let channel_uuid = parse_uuid(channel_id)?;
|
||||
|
||||
let builder = sprout_sdk::build_set_canvas(channel_uuid, &content)
|
||||
.map_err(|e| CliError::Other(format!("build_set_canvas failed: {e}")))?;
|
||||
|
||||
let event = sign_and_submit_builder(builder, client)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
let resp = client.submit_event(event).await?;
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dispatch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn dispatch(cmd: crate::ChannelsCmd, client: &SproutClient) -> Result<(), CliError> {
|
||||
use crate::ChannelsCmd;
|
||||
match cmd {
|
||||
ChannelsCmd::List { visibility, member } => {
|
||||
let vis_str = visibility.as_ref().map(|v| v.to_string());
|
||||
cmd_list_channels(client, vis_str.as_deref(), Some(member)).await
|
||||
}
|
||||
ChannelsCmd::Get { channel } => cmd_get_channel(client, &channel).await,
|
||||
ChannelsCmd::Create {
|
||||
name,
|
||||
channel_type,
|
||||
visibility,
|
||||
description,
|
||||
} => {
|
||||
cmd_create_channel(
|
||||
client,
|
||||
&name,
|
||||
&channel_type.to_string(),
|
||||
&visibility.to_string(),
|
||||
description.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
ChannelsCmd::Update {
|
||||
channel,
|
||||
name,
|
||||
description,
|
||||
} => cmd_update_channel(client, &channel, name.as_deref(), description.as_deref()).await,
|
||||
ChannelsCmd::Topic { channel, topic } => {
|
||||
cmd_set_channel_topic(client, &channel, &topic).await
|
||||
}
|
||||
ChannelsCmd::Purpose { channel, purpose } => {
|
||||
cmd_set_channel_purpose(client, &channel, &purpose).await
|
||||
}
|
||||
ChannelsCmd::Join { channel } => cmd_join_channel(client, &channel).await,
|
||||
ChannelsCmd::Leave { channel } => cmd_leave_channel(client, &channel).await,
|
||||
ChannelsCmd::Archive { channel } => cmd_archive_channel(client, &channel).await,
|
||||
ChannelsCmd::Unarchive { channel } => cmd_unarchive_channel(client, &channel).await,
|
||||
ChannelsCmd::Delete { channel } => cmd_delete_channel(client, &channel).await,
|
||||
ChannelsCmd::Members { channel } => cmd_list_channel_members(client, &channel).await,
|
||||
ChannelsCmd::AddMember {
|
||||
channel,
|
||||
pubkey,
|
||||
role,
|
||||
} => cmd_add_channel_member(client, &channel, &pubkey, role.as_deref()).await,
|
||||
ChannelsCmd::RemoveMember { channel, pubkey } => {
|
||||
cmd_remove_channel_member(client, &channel, &pubkey).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn dispatch_canvas(cmd: crate::CanvasCmd, client: &SproutClient) -> Result<(), CliError> {
|
||||
use crate::CanvasCmd;
|
||||
match cmd {
|
||||
CanvasCmd::Get { channel } => cmd_get_canvas(client, &channel).await,
|
||||
CanvasCmd::Set { channel, content } => cmd_set_canvas(client, &channel, &content).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,3 +45,16 @@ pub async fn cmd_add_dm_member(
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dispatch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn dispatch(cmd: crate::DmsCmd, client: &SproutClient) -> Result<(), CliError> {
|
||||
use crate::DmsCmd;
|
||||
match cmd {
|
||||
DmsCmd::List { limit } => cmd_list_dms(client, limit).await,
|
||||
DmsCmd::Open { pubkeys } => cmd_open_dm(client, &pubkeys).await,
|
||||
DmsCmd::AddMember { channel, pubkey } => cmd_add_dm_member(client, &channel, &pubkey).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,3 +24,18 @@ pub async fn cmd_get_feed(
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dispatch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn dispatch(cmd: crate::FeedCmd, client: &SproutClient) -> Result<(), CliError> {
|
||||
use crate::FeedCmd;
|
||||
match cmd {
|
||||
FeedCmd::Get {
|
||||
since,
|
||||
limit,
|
||||
types,
|
||||
} => cmd_get_feed(client, since, limit, types.as_deref()).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use nostr::{EventId, PublicKey};
|
||||
use nostr::PublicKey;
|
||||
use sprout_sdk::{DiffMeta, ThreadRef, VoteDirection};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::client::SproutClient;
|
||||
use crate::error::CliError;
|
||||
use crate::validate::{
|
||||
infer_language, read_or_stdin, truncate_diff, validate_content_size, validate_hex64,
|
||||
validate_uuid, MAX_DIFF_BYTES,
|
||||
infer_language, parse_event_id, parse_uuid, read_or_stdin, truncate_diff,
|
||||
validate_content_size, validate_hex64, validate_uuid, MAX_DIFF_BYTES,
|
||||
};
|
||||
use sprout_sdk::mentions::{
|
||||
extract_at_names, match_names_to_profiles, merge_mentions, normalize_mention_pubkeys,
|
||||
@@ -17,16 +17,6 @@ use sprout_sdk::mentions::{
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Parse a 64-char hex string into a nostr::EventId.
|
||||
fn parse_event_id(hex: &str) -> Result<EventId, CliError> {
|
||||
EventId::parse(hex).map_err(|e| CliError::Usage(format!("invalid event ID: {e}")))
|
||||
}
|
||||
|
||||
/// Parse a UUID string into uuid::Uuid.
|
||||
fn parse_uuid(s: &str) -> Result<Uuid, CliError> {
|
||||
Uuid::parse_str(s).map_err(|e| CliError::Usage(format!("invalid channel UUID: {e}")))
|
||||
}
|
||||
|
||||
/// Extract the thread root event ID from a Nostr tag array.
|
||||
///
|
||||
/// Parses `"e"` tags with NIP-10 markers:
|
||||
@@ -323,7 +313,6 @@ pub struct SendMessageParams {
|
||||
}
|
||||
|
||||
pub async fn cmd_send_message(client: &SproutClient, p: SendMessageParams) -> Result<(), CliError> {
|
||||
validate_uuid(&p.channel_id)?;
|
||||
validate_content_size(&p.content)?;
|
||||
if let Some(ref r) = p.reply_to {
|
||||
validate_hex64(r)?;
|
||||
@@ -409,7 +398,6 @@ pub async fn cmd_send_diff_message(
|
||||
client: &SproutClient,
|
||||
p: SendDiffParams,
|
||||
) -> Result<(), CliError> {
|
||||
validate_uuid(&p.channel_id)?;
|
||||
if let Some(r) = &p.reply_to {
|
||||
validate_hex64(r)?;
|
||||
}
|
||||
@@ -553,6 +541,91 @@ pub async fn cmd_vote_on_post(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dispatch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn dispatch(cmd: crate::MessagesCmd, client: &SproutClient) -> Result<(), CliError> {
|
||||
use crate::MessagesCmd;
|
||||
match cmd {
|
||||
MessagesCmd::Send {
|
||||
channel,
|
||||
content,
|
||||
kind,
|
||||
reply_to,
|
||||
broadcast,
|
||||
mentions,
|
||||
files,
|
||||
} => {
|
||||
cmd_send_message(
|
||||
client,
|
||||
SendMessageParams {
|
||||
channel_id: channel,
|
||||
content,
|
||||
kind,
|
||||
reply_to,
|
||||
broadcast,
|
||||
mentions,
|
||||
files,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
MessagesCmd::SendDiff {
|
||||
channel,
|
||||
diff,
|
||||
repo,
|
||||
commit,
|
||||
file,
|
||||
parent_commit,
|
||||
source_branch,
|
||||
target_branch,
|
||||
pr,
|
||||
lang,
|
||||
description,
|
||||
reply_to,
|
||||
} => {
|
||||
cmd_send_diff_message(
|
||||
client,
|
||||
SendDiffParams {
|
||||
channel_id: channel,
|
||||
diff,
|
||||
repo_url: repo,
|
||||
commit_sha: commit,
|
||||
file_path: file,
|
||||
parent_commit_sha: parent_commit,
|
||||
source_branch,
|
||||
target_branch,
|
||||
pr_number: pr,
|
||||
language: lang,
|
||||
description,
|
||||
reply_to,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
MessagesCmd::Edit { event, content } => cmd_edit_message(client, &event, &content).await,
|
||||
MessagesCmd::Delete { event } => cmd_delete_message(client, &event).await,
|
||||
MessagesCmd::Get {
|
||||
channel,
|
||||
limit,
|
||||
before,
|
||||
since,
|
||||
kinds,
|
||||
} => cmd_get_messages(client, &channel, limit, before, since, kinds.as_deref()).await,
|
||||
MessagesCmd::Thread {
|
||||
channel,
|
||||
event,
|
||||
depth_limit,
|
||||
limit,
|
||||
} => cmd_get_thread(client, &channel, &event, depth_limit, limit).await,
|
||||
MessagesCmd::Search { query, limit } => cmd_search(client, &query, limit).await,
|
||||
MessagesCmd::Vote { event, direction } => {
|
||||
cmd_vote_on_post(client, &event, &direction).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{find_root_from_tags, parse_member_pubkeys};
|
||||
|
||||
@@ -6,5 +6,6 @@ pub mod pack;
|
||||
pub mod reactions;
|
||||
pub mod repos;
|
||||
pub mod social;
|
||||
pub mod upload;
|
||||
pub mod users;
|
||||
pub mod workflows;
|
||||
|
||||
@@ -80,3 +80,16 @@ pub async fn cmd_get_reactions(client: &SproutClient, event_id: &str) -> Result<
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dispatch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn dispatch(cmd: crate::ReactionsCmd, client: &SproutClient) -> Result<(), CliError> {
|
||||
use crate::ReactionsCmd;
|
||||
match cmd {
|
||||
ReactionsCmd::Add { event, emoji } => cmd_add_reaction(client, &event, &emoji).await,
|
||||
ReactionsCmd::Remove { event, emoji } => cmd_remove_reaction(client, &event, &emoji).await,
|
||||
ReactionsCmd::Get { event } => cmd_get_reactions(client, &event).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,3 +95,34 @@ pub async fn cmd_list_repos(
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dispatch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn dispatch(cmd: crate::ReposCmd, client: &SproutClient) -> Result<(), CliError> {
|
||||
use crate::ReposCmd;
|
||||
match cmd {
|
||||
ReposCmd::Create {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
clone_urls,
|
||||
web,
|
||||
relays,
|
||||
} => {
|
||||
cmd_create_repo(
|
||||
client,
|
||||
&id,
|
||||
name.as_deref(),
|
||||
description.as_deref(),
|
||||
&clone_urls,
|
||||
web.as_deref(),
|
||||
&relays,
|
||||
)
|
||||
.await
|
||||
}
|
||||
ReposCmd::Get { id, owner } => cmd_get_repo(client, &id, owner.as_deref()).await,
|
||||
ReposCmd::List { owner, limit } => cmd_list_repos(client, owner.as_deref(), limit).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
use nostr::EventId;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::client::SproutClient;
|
||||
use crate::error::CliError;
|
||||
use crate::validate::validate_hex64;
|
||||
|
||||
/// Per-module helper.
|
||||
fn parse_event_id(hex: &str) -> Result<EventId, CliError> {
|
||||
EventId::parse(hex).map_err(|e| CliError::Usage(format!("invalid event ID: {e}")))
|
||||
}
|
||||
use crate::validate::{parse_event_id, validate_hex64};
|
||||
|
||||
/// A single contact entry (CLI-local, not from sprout-sdk).
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -117,3 +111,24 @@ pub async fn cmd_get_contact_list(client: &SproutClient, pubkey: &str) -> Result
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dispatch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn dispatch(cmd: crate::SocialCmd, client: &SproutClient) -> Result<(), CliError> {
|
||||
use crate::SocialCmd;
|
||||
match cmd {
|
||||
SocialCmd::PublishNote { content, reply_to } => {
|
||||
cmd_publish_note(client, &content, reply_to.as_deref()).await
|
||||
}
|
||||
SocialCmd::SetContactList { contacts } => cmd_set_contact_list(client, &contacts).await,
|
||||
SocialCmd::GetEvent { event } => cmd_get_event(client, &event).await,
|
||||
SocialCmd::GetUserNotes {
|
||||
pubkey,
|
||||
limit,
|
||||
before,
|
||||
} => cmd_get_user_notes(client, &pubkey, limit, before).await,
|
||||
SocialCmd::GetContactList { pubkey } => cmd_get_contact_list(client, &pubkey).await,
|
||||
}
|
||||
}
|
||||
|
||||
15
crates/sprout-cli/src/commands/upload.rs
Normal file
15
crates/sprout-cli/src/commands/upload.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use crate::client::SproutClient;
|
||||
use crate::error::CliError;
|
||||
|
||||
pub async fn dispatch(cmd: crate::UploadCmd, client: &SproutClient) -> Result<(), CliError> {
|
||||
match cmd {
|
||||
crate::UploadCmd::File { file } => {
|
||||
let desc = client.upload_file(&file).await?;
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&desc).map_err(|e| CliError::Other(e.to_string()))?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ use crate::client::SproutClient;
|
||||
use crate::error::CliError;
|
||||
use crate::validate::validate_hex64;
|
||||
|
||||
// TODO(phase-4): Replace raw nostr::EventBuilder usage in cmd_set_presence with sprout-sdk builder
|
||||
|
||||
/// Get user profiles (kind:0 metadata events).
|
||||
///
|
||||
/// - 0 pubkeys, no name → query our own profile
|
||||
@@ -225,3 +227,31 @@ pub async fn cmd_set_presence(client: &SproutClient, status: &str) -> Result<(),
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dispatch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn dispatch(cmd: crate::UsersCmd, client: &SproutClient) -> Result<(), CliError> {
|
||||
use crate::UsersCmd;
|
||||
match cmd {
|
||||
UsersCmd::Get { pubkeys, name } => cmd_get_users(client, &pubkeys, name.as_deref()).await,
|
||||
UsersCmd::SetProfile {
|
||||
name,
|
||||
avatar,
|
||||
about,
|
||||
nip05,
|
||||
} => {
|
||||
cmd_set_profile(
|
||||
client,
|
||||
name.as_deref(),
|
||||
avatar.as_deref(),
|
||||
about.as_deref(),
|
||||
nip05.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
UsersCmd::Presence { pubkeys } => cmd_get_presence(client, &pubkeys).await,
|
||||
UsersCmd::SetPresence { status } => cmd_set_presence(client, &status.to_string()).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ use crate::client::SproutClient;
|
||||
use crate::error::CliError;
|
||||
use crate::validate::{parse_uuid, read_or_stdin, sdk_err, validate_uuid};
|
||||
|
||||
// TODO(phase-4): Replace raw nostr::EventBuilder usage with sprout-sdk builder functions
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Read commands — POST /query
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -143,3 +145,36 @@ pub async fn cmd_approve_step(
|
||||
println!("{resp}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dispatch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn dispatch(cmd: crate::WorkflowsCmd, client: &SproutClient) -> Result<(), CliError> {
|
||||
use crate::WorkflowsCmd;
|
||||
match cmd {
|
||||
WorkflowsCmd::List { channel } => cmd_list_workflows(client, &channel).await,
|
||||
WorkflowsCmd::Get { workflow } => cmd_get_workflow(client, &workflow).await,
|
||||
WorkflowsCmd::Create { channel, yaml } => {
|
||||
cmd_create_workflow(client, &channel, &yaml).await
|
||||
}
|
||||
WorkflowsCmd::Update {
|
||||
channel,
|
||||
workflow,
|
||||
yaml,
|
||||
} => cmd_update_workflow(client, &channel, &workflow, &yaml).await,
|
||||
WorkflowsCmd::Delete { workflow } => cmd_delete_workflow(client, &workflow).await,
|
||||
WorkflowsCmd::Trigger { workflow } => cmd_trigger_workflow(client, &workflow).await,
|
||||
WorkflowsCmd::Runs { workflow, limit } => {
|
||||
cmd_get_workflow_runs(client, &workflow, limit).await
|
||||
}
|
||||
WorkflowsCmd::Approve {
|
||||
token,
|
||||
approved,
|
||||
note,
|
||||
} => {
|
||||
// approved is already a bool — no parse_bool_flag needed
|
||||
cmd_approve_step(client, &token, approved, note.as_deref()).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,17 +6,25 @@ pub const MAX_CONTENT_BYTES: usize = 65_536;
|
||||
/// Maximum diff size in bytes (60 KiB).
|
||||
pub const MAX_DIFF_BYTES: usize = 61_440;
|
||||
|
||||
/// Parse a hex string into a `nostr::EventId`. Returns `CliError::Usage` on failure.
|
||||
pub fn parse_event_id(hex: &str) -> Result<nostr::EventId, CliError> {
|
||||
nostr::EventId::parse(hex).map_err(|e| CliError::Usage(format!("invalid event ID: {e}")))
|
||||
}
|
||||
|
||||
/// Parse a UUID string into a `uuid::Uuid`. Returns `CliError::Usage` on failure.
|
||||
///
|
||||
/// Note: `validate_uuid` (below) returns `()` for validation only; this function
|
||||
/// returns the parsed `Uuid` for callers that need the value.
|
||||
pub fn parse_uuid(s: &str) -> Result<uuid::Uuid, CliError> {
|
||||
uuid::Uuid::parse_str(s).map_err(|e| CliError::Usage(format!("invalid UUID: {e}")))
|
||||
}
|
||||
|
||||
/// Validate UUID string. Returns CliError::Usage on failure.
|
||||
pub fn validate_uuid(s: &str) -> Result<(), CliError> {
|
||||
uuid::Uuid::parse_str(s).map_err(|_| CliError::Usage(format!("invalid UUID: {s}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse and validate a UUID string, returning the parsed Uuid value.
|
||||
pub fn parse_uuid(s: &str) -> Result<uuid::Uuid, CliError> {
|
||||
uuid::Uuid::parse_str(s).map_err(|_| CliError::Usage(format!("invalid UUID: {s}")))
|
||||
}
|
||||
|
||||
/// Validate 64-character lowercase hex string (event_id, pubkey).
|
||||
pub fn validate_hex64(s: &str) -> Result<(), CliError> {
|
||||
if s.len() != 64 || !s.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
@@ -367,6 +375,31 @@ mod tests {
|
||||
// Note: `extract_at_names`, `merge_mentions`, and `normalize_mention_pubkeys`
|
||||
// moved to `sprout_sdk::mentions` and are tested there.
|
||||
|
||||
// --- parse_event_id ---
|
||||
|
||||
#[test]
|
||||
fn parse_event_id_valid() {
|
||||
let hex = "a".repeat(64);
|
||||
assert!(super::parse_event_id(&hex).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_event_id_invalid() {
|
||||
assert!(super::parse_event_id("not-a-hex-id").is_err());
|
||||
}
|
||||
|
||||
// --- parse_uuid ---
|
||||
|
||||
#[test]
|
||||
fn parse_uuid_valid() {
|
||||
assert!(super::parse_uuid("550e8400-e29b-41d4-a716-446655440000").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_uuid_invalid() {
|
||||
assert!(super::parse_uuid("not-a-uuid").is_err());
|
||||
}
|
||||
|
||||
// ── validate_repo_id ─────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user