Files
claw-code/rust/crates/rusty-claude-cli/src/args.rs
Yeachan-Heo 32e89df631 Enable Claude OAuth login without requiring API keys
This adds an end-to-end OAuth PKCE login/logout path to the Rust CLI,
persists OAuth credentials under the Claude config home, and teaches the
API client to use persisted bearer credentials with refresh support when
env-based API credentials are absent.

Constraint: Reuse existing runtime OAuth primitives and keep browser/callback orchestration in the CLI
Constraint: Preserve auth precedence as API key, then auth-token env, then persisted OAuth credentials
Rejected: Put browser launch and token exchange entirely in runtime | caused boundary creep across shared crates
Rejected: Duplicate credential parsing in CLI and api | increased drift and refresh inconsistency
Confidence: medium
Scope-risk: moderate
Reversibility: clean
Directive: Keep logout non-destructive to unrelated credentials.json fields and do not silently fall back to stale expired tokens
Tested: cargo fmt; cargo clippy --workspace --all-targets -- -D warnings; cargo test
Not-tested: Manual live Anthropic OAuth browser flow against real authorize/token endpoints
2026-03-31 23:38:05 +00:00

103 lines
2.6 KiB
Rust

use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
#[derive(Debug, Clone, Parser, PartialEq, Eq)]
#[command(
name = "rusty-claude-cli",
version,
about = "Rust Claude CLI prototype"
)]
pub struct Cli {
#[arg(long, default_value = "claude-3-7-sonnet")]
pub model: String,
#[arg(long, value_enum, default_value_t = PermissionMode::WorkspaceWrite)]
pub permission_mode: PermissionMode,
#[arg(long)]
pub config: Option<PathBuf>,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
pub output_format: OutputFormat,
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(Debug, Clone, Subcommand, PartialEq, Eq)]
pub enum Command {
/// Read upstream TS sources and print extracted counts
DumpManifests,
/// Print the current bootstrap phase skeleton
BootstrapPlan,
/// Start the OAuth login flow
Login,
/// Clear saved OAuth credentials
Logout,
/// Run a non-interactive prompt and exit
Prompt { prompt: Vec<String> },
}
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
pub enum PermissionMode {
ReadOnly,
WorkspaceWrite,
DangerFullAccess,
}
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
pub enum OutputFormat {
Text,
Json,
Ndjson,
}
#[cfg(test)]
mod tests {
use clap::Parser;
use super::{Cli, Command, OutputFormat, PermissionMode};
#[test]
fn parses_requested_flags() {
let cli = Cli::parse_from([
"rusty-claude-cli",
"--model",
"claude-3-5-haiku",
"--permission-mode",
"read-only",
"--config",
"/tmp/config.toml",
"--output-format",
"ndjson",
"prompt",
"hello",
"world",
]);
assert_eq!(cli.model, "claude-3-5-haiku");
assert_eq!(cli.permission_mode, PermissionMode::ReadOnly);
assert_eq!(
cli.config.as_deref(),
Some(std::path::Path::new("/tmp/config.toml"))
);
assert_eq!(cli.output_format, OutputFormat::Ndjson);
assert_eq!(
cli.command,
Some(Command::Prompt {
prompt: vec!["hello".into(), "world".into()]
})
);
}
#[test]
fn parses_login_and_logout_commands() {
let login = Cli::parse_from(["rusty-claude-cli", "login"]);
assert_eq!(login.command, Some(Command::Login));
let logout = Cli::parse_from(["rusty-claude-cli", "logout"]);
assert_eq!(logout.command, Some(Command::Logout));
}
}