Add support for multiline secrets via stdin and file input

Support reading secret values from stdin or files to handle
multiline content like SSH keys, certificates, and config files.

New value patterns:
- `--value -` reads from stdin
- `--value @<path>` reads from file
- `--value <text>` uses literal value (backward compatible)

File path handling:
- Supports tilde expansion for home directory (~/)
- Provides clear error messages if file cannot be read

Examples:
# From stdin
cat ~/.ssh/id_ed25519 | tangled spindle secret add \
--repo myrepo --key SSH_KEY --value -

# From file
tangled spindle secret add --repo myrepo \
--key SSH_KEY --value @~/.ssh/id_ed25519

# Literal value (existing behavior)
tangled spindle secret add --repo myrepo \
--key API_KEY --value "my-secret-key"

Fixes issue where multiline values were split into multiple
arguments by the shell, causing clap parsing errors.

Changed files
+27 -2
crates
tangled-cli
src
commands
+1 -1
crates/tangled-cli/src/cli.rs
···
/// Secret key
#[arg(long)]
pub key: String,
-
/// Secret value
+
/// Secret value (use '@filename' to read from file, '-' to read from stdin)
#[arg(long)]
pub value: String,
}
+26 -1
crates/tangled-cli/src/commands/spindle.rs
···
.unwrap_or_else(|| "https://spindle.tangled.sh".to_string());
let api = tangled_api::TangledClient::new(&spindle_base);
-
api.add_repo_secret(&pds, &session.access_jwt, &repo_at, &args.key, &args.value)
+
// Handle special value patterns: @file or - (stdin)
+
let value = if args.value == "-" {
+
// Read from stdin
+
use std::io::Read;
+
let mut buffer = String::new();
+
std::io::stdin().read_to_string(&mut buffer)?;
+
buffer
+
} else if let Some(path) = args.value.strip_prefix('@') {
+
// Read from file, expand ~ if needed
+
let expanded_path = if path.starts_with("~/") {
+
if let Some(home) = std::env::var("HOME").ok() {
+
path.replacen("~/", &format!("{}/", home), 1)
+
} else {
+
path.to_string()
+
}
+
} else {
+
path.to_string()
+
};
+
std::fs::read_to_string(&expanded_path)
+
.map_err(|e| anyhow!("Failed to read file '{}': {}", expanded_path, e))?
+
} else {
+
// Use value as-is
+
args.value
+
};
+
+
api.add_repo_secret(&pds, &session.access_jwt, &repo_at, &args.key, &value)
.await?;
println!("Added secret '{}' to {}", args.key, args.repo);
Ok(())