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
26 changes: 5 additions & 21 deletions crates/sprout-cli/src/commands/dms.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use nostr::{EventBuilder, Kind, Tag};

use crate::client::SproutClient;
use crate::error::CliError;
use crate::validate::{validate_hex64, validate_uuid};
use crate::validate::{parse_uuid, sdk_err};

/// List DM conversations by querying kind:41010 (DM open) events authored by us.
pub async fn cmd_list_dms(client: &SproutClient, limit: Option<u32>) -> Result<(), CliError> {
Expand All @@ -23,16 +21,8 @@ pub async fn cmd_open_dm(client: &SproutClient, pubkeys: &[String]) -> Result<()
if pubkeys.is_empty() || pubkeys.len() > 8 {
return Err(CliError::Usage("--pubkey: must provide 1–8 pubkeys".into()));
}
for pk in pubkeys {
validate_hex64(pk)?;
}

let mut tags: Vec<Tag> = Vec::new();
for pk in pubkeys {
tags.push(Tag::parse(&["p", pk]).map_err(|e| CliError::Other(format!("tag error: {e}")))?);
}

let builder = EventBuilder::new(Kind::Custom(41010), "", tags);
let refs: Vec<&str> = pubkeys.iter().map(String::as_str).collect();
let builder = sprout_sdk::build_dm_open(&refs).map_err(sdk_err)?;
let event = client.sign_event(builder)?;

let resp = client.submit_event(event).await?;
Expand All @@ -46,15 +36,9 @@ pub async fn cmd_add_dm_member(
channel_id: &str,
pubkey: &str,
) -> Result<(), CliError> {
validate_uuid(channel_id)?;
validate_hex64(pubkey)?;

let tags = vec![
Tag::parse(&["h", channel_id]).map_err(|e| CliError::Other(format!("tag error: {e}")))?,
Tag::parse(&["p", pubkey]).map_err(|e| CliError::Other(format!("tag error: {e}")))?,
];
let channel_uuid = parse_uuid(channel_id)?;

let builder = EventBuilder::new(Kind::Custom(41011), "", tags);
let builder = sprout_sdk::build_dm_add_member(channel_uuid, pubkey).map_err(sdk_err)?;
let event = client.sign_event(builder)?;

let resp = client.submit_event(event).await?;
Expand Down
19 changes: 1 addition & 18 deletions crates/sprout-cli/src/commands/users.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use nostr::{EventBuilder, Kind, Tag};

use crate::client::SproutClient;
use crate::error::CliError;
use crate::validate::validate_hex64;
Expand Down Expand Up @@ -220,22 +218,7 @@ pub async fn cmd_get_presence(client: &SproutClient, pubkeys_csv: &str) -> Resul
/// This will fail until the CLI gains a WS publish path. The kind is correct
/// per the protocol spec (KIND_PRESENCE_UPDATE = 20001).
pub async fn cmd_set_presence(client: &SproutClient, status: &str) -> Result<(), CliError> {
match status {
"online" | "away" | "offline" => {}
_ => {
return Err(CliError::Usage(format!(
"--status must be one of: online, away, offline (got: {status})"
)))
}
}

let tags =
vec![Tag::parse(&["status", status])
.map_err(|e| CliError::Other(format!("tag error: {e}")))?];

// KIND_PRESENCE_UPDATE (20001) — ephemeral, WS-only. HTTP bridge will reject this
// until the CLI gains a WebSocket publish path.
let builder = EventBuilder::new(Kind::Custom(20001), "", tags);
let builder = sprout_sdk::build_presence_update(status).map_err(crate::validate::sdk_err)?;
let event = client.sign_event(builder)?;

let resp = client.submit_event(event).await?;
Expand Down
55 changes: 17 additions & 38 deletions crates/sprout-cli/src/commands/workflows.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use nostr::{EventBuilder, Kind, Tag};
use sha2::{Digest, Sha256};

use crate::client::SproutClient;
use crate::error::CliError;
use crate::validate::{read_or_stdin, validate_uuid};
use crate::validate::{parse_uuid, read_or_stdin, sdk_err, validate_uuid};

// ---------------------------------------------------------------------------
// Read commands — POST /query
Expand Down Expand Up @@ -61,17 +60,12 @@ pub async fn cmd_create_workflow(
channel_id: &str,
yaml: &str,
) -> Result<(), CliError> {
validate_uuid(channel_id)?;
let channel_uuid = parse_uuid(channel_id)?;
let yaml_definition = read_or_stdin(yaml)?;

// Generate a unique d-tag for this workflow
let workflow_id = uuid::Uuid::new_v4().to_string();
let tags = vec![
Tag::parse(&["d", &workflow_id]).map_err(|e| CliError::Other(format!("tag error: {e}")))?,
Tag::parse(&["h", channel_id]).map_err(|e| CliError::Other(format!("tag error: {e}")))?,
];

let builder = EventBuilder::new(Kind::Custom(30620), &yaml_definition, tags);
let workflow_id = uuid::Uuid::new_v4();
let builder = sprout_sdk::build_workflow_def(channel_uuid, workflow_id, &yaml_definition)
.map_err(sdk_err)?;
let event = client.sign_event(builder)?;

let resp = client.submit_event(event).await?;
Expand All @@ -82,17 +76,16 @@ pub async fn cmd_create_workflow(
/// Update a workflow — sign and submit an updated kind:30620 event with same d-tag.
pub async fn cmd_update_workflow(
client: &SproutClient,
channel_id: &str,
workflow_id: &str,
yaml: &str,
) -> Result<(), CliError> {
validate_uuid(workflow_id)?;
let channel_uuid = parse_uuid(channel_id)?;
let wf_uuid = parse_uuid(workflow_id)?;
let yaml_definition = read_or_stdin(yaml)?;

let tags =
vec![Tag::parse(&["d", workflow_id])
.map_err(|e| CliError::Other(format!("tag error: {e}")))?];

let builder = EventBuilder::new(Kind::Custom(30620), &yaml_definition, tags);
let builder = sprout_sdk::build_workflow_update(channel_uuid, wf_uuid, &yaml_definition)
.map_err(sdk_err)?;
let event = client.sign_event(builder)?;

let resp = client.submit_event(event).await?;
Expand All @@ -102,17 +95,11 @@ pub async fn cmd_update_workflow(

/// Delete a workflow — sign and submit a kind:5 deletion event.
pub async fn cmd_delete_workflow(client: &SproutClient, workflow_id: &str) -> Result<(), CliError> {
validate_uuid(workflow_id)?;
let wf_uuid = parse_uuid(workflow_id)?;
let keys = client.keys();

// NIP-09 deletion targeting the parameterized replaceable event
let tags = vec![Tag::parse(&[
"a",
&format!("30620:{}:{}", keys.public_key().to_hex(), workflow_id),
])
.map_err(|e| CliError::Other(format!("tag error: {e}")))?];

let builder = EventBuilder::new(Kind::Custom(5), "", tags);
let builder =
sprout_sdk::build_workflow_delete(&keys.public_key().to_hex(), wf_uuid).map_err(sdk_err)?;
let event = client.sign_event(builder)?;

let resp = client.submit_event(event).await?;
Expand All @@ -125,13 +112,9 @@ pub async fn cmd_trigger_workflow(
client: &SproutClient,
workflow_id: &str,
) -> Result<(), CliError> {
validate_uuid(workflow_id)?;
let wf_uuid = parse_uuid(workflow_id)?;

let tags =
vec![Tag::parse(&["d", workflow_id])
.map_err(|e| CliError::Other(format!("tag error: {e}")))?];

let builder = EventBuilder::new(Kind::Custom(46020), "", tags);
let builder = sprout_sdk::build_workflow_trigger(wf_uuid).map_err(sdk_err)?;
let event = client.sign_event(builder)?;

let resp = client.submit_event(event).await?;
Expand All @@ -148,16 +131,12 @@ pub async fn cmd_approve_step(
) -> Result<(), CliError> {
validate_uuid(approval_token)?;

let kind = if approved { 46030 } else { 46031 };
let content = note.unwrap_or("");

// The relay expects d-tag = hex(SHA256(token)), not the raw token UUID.
let token_hash = hex::encode(Sha256::digest(approval_token.as_bytes()));
let tags =
vec![Tag::parse(&["d", &token_hash])
.map_err(|e| CliError::Other(format!("tag error: {e}")))?];

let builder = EventBuilder::new(Kind::Custom(kind), content, tags);
let builder =
sprout_sdk::build_workflow_approval(&token_hash, approved, content).map_err(sdk_err)?;
let event = client.sign_event(builder)?;

let resp = client.submit_event(event).await?;
Expand Down
10 changes: 7 additions & 3 deletions crates/sprout-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,8 @@ enum Cmd {
},
/// Update a workflow
UpdateWorkflow {
#[arg(long)]
channel: String,
#[arg(long)]
workflow: String,
#[arg(long)]
Expand Down Expand Up @@ -819,9 +821,11 @@ async fn run(cli: Cli) -> Result<(), CliError> {
Cmd::CreateWorkflow { channel, yaml } => {
commands::workflows::cmd_create_workflow(&client, &channel, &yaml).await
}
Cmd::UpdateWorkflow { workflow, yaml } => {
commands::workflows::cmd_update_workflow(&client, &workflow, &yaml).await
}
Cmd::UpdateWorkflow {
channel,
workflow,
yaml,
} => commands::workflows::cmd_update_workflow(&client, &channel, &workflow, &yaml).await,
Cmd::DeleteWorkflow { workflow } => {
commands::workflows::cmd_delete_workflow(&client, &workflow).await
}
Expand Down
15 changes: 15 additions & 0 deletions crates/sprout-cli/src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ pub fn validate_uuid(s: &str) -> Result<(), CliError> {
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()) {
Expand Down Expand Up @@ -141,6 +146,16 @@ pub fn infer_language(file_path: &str) -> Option<String> {
Some(lang.to_string())
}

/// Map `SdkError` to the appropriate `CliError` variant.
///
/// `InvalidInput` is a user error (exit 1), everything else is internal (exit 4).
pub fn sdk_err(e: sprout_sdk::SdkError) -> CliError {
match e {
sprout_sdk::SdkError::InvalidInput(msg) => CliError::Usage(msg),
other => CliError::Other(other.to_string()),
}
}

/// Read content from a string value or stdin if the value is "-".
pub fn read_or_stdin(value: &str) -> Result<String, CliError> {
if value == "-" {
Expand Down
Loading
Loading