Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e87dc49
cli: Add WorkOS auth service models and expand dependency baseline
davidabram Mar 8, 2026
28255d4
token_storage: Implement WorkOS token persistence
davidabram Mar 8, 2026
1f9caa0
auth: Implement WorkOS device authorization flow with polling
davidabram Mar 8, 2026
1c2573c
auth: Implement token refresh runtime for WorkOS
davidabram Mar 9, 2026
30c0c5a
fix: Remove leftover conflict marker from app.rs
davidabram Mar 9, 2026
c988600
auth: Reformat long lines in auth service
davidabram Mar 10, 2026
f5311c2
cli: Add auth command schema with login/logout/status subcommands
davidabram Mar 10, 2026
01f0388
cli(auth): Add auth command service with login/logout/status
davidabram Mar 10, 2026
c5f326e
cli: Wire auth commands into app dispatch
davidabram Mar 10, 2026
f18f4d2
token_storage: Add delete_tokens() for logout operation
davidabram Mar 10, 2026
8092125
cli: Register auth command as implemented in command surface
davidabram Mar 10, 2026
8d388c5
cli: Add workos_client_id env/config resolution with source tracking
davidabram Mar 10, 2026
b75a907
(config): Add shared auth config resolver with baked defaults
davidabram Mar 10, 2026
e4d7537
tests: Extract BinaryIntegrationHarness to shared support module
davidabram Mar 10, 2026
845e496
tests: Add integration tests for config precedence chain
davidabram Mar 10, 2026
0f8b127
tests: Add auth config precedence integration tests
davidabram Mar 10, 2026
caffe69
cli: Add Nix entrypoint for config-precedence integration tests
davidabram Mar 10, 2026
ab47479
cli: Document opt-in config precedence integration coverage
davidabram Mar 11, 2026
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
574 changes: 475 additions & 99 deletions cli/Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ categories = ["command-line-utilities", "development-tools"]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
clap_complete = "4"
dirs = "6"
hmac = "0.12"
inquire = "0.7"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
tokio = { version = "1", default-features = false, features = ["rt"] }
Expand Down
12 changes: 12 additions & 0 deletions cli/flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@
};
};

apps.config-precedence-integration-tests = {
type = "app";
program = toString (
pkgs.writeShellScript "sce-config-precedence-integration-tests" ''
exec ${rustToolchain}/bin/cargo test --manifest-path cli/Cargo.toml --test config_precedence_integration "$@"
''
);
meta = {
description = "Run config-precedence integration tests for the sce CLI crate";
};
};

checks.cli-setup-command-surface = mkCheck "sce-cli-setup-command-surface-check" ''
runHook preCheck

Expand Down
77 changes: 72 additions & 5 deletions cli/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::io::{self, Write};
use std::process::ExitCode;

use crate::{cli_schema, command_surface, dependency_contract, services};
use crate::{cli_schema, command_surface, services};
use anyhow::Context;

const EXIT_CODE_PARSE_FAILURE: u8 = 2;
Expand Down Expand Up @@ -105,6 +105,7 @@ impl std::error::Error for ClassifiedError {}
#[derive(Clone, Debug, Eq, PartialEq)]
enum Command {
Help,
Auth(services::auth_command::AuthRequest),
Completion(services::completion::CompletionRequest),
Config(services::config::ConfigSubcommand),
Setup(services::setup::SetupRequest),
Expand All @@ -119,6 +120,7 @@ impl Command {
fn name(&self) -> &'static str {
match self {
Self::Help => "help",
Self::Auth(_) => services::auth_command::NAME,
Self::Completion(_) => services::completion::NAME,
Self::Config(_) => services::config::NAME,
Self::Setup(_) => services::setup::NAME,
Expand All @@ -135,9 +137,7 @@ pub fn run<I>(args: I) -> ExitCode
where
I: IntoIterator<Item = String>,
{
run_with_dependency_check(args, || {
dependency_contract::dependency_contract_snapshot().0
})
run_with_dependency_check(args, || Ok(()))
}

fn run_with_dependency_check<I, F>(args: I, dependency_check: F) -> ExitCode
Expand Down Expand Up @@ -218,7 +218,7 @@ where
F: FnOnce() -> anyhow::Result<()>,
{
dependency_check().map_err(|error| {
ClassifiedError::dependency(format!("Failed to initialize dependency contract: {error}"))
ClassifiedError::dependency(format!("Failed to initialize dependency checks: {error}"))
})?;

let logger = services::observability::Logger::from_env().map_err(|error| {
Expand Down Expand Up @@ -443,6 +443,7 @@ fn extract_quoted_value(message: &str) -> Option<String> {
fn convert_clap_command(command: cli_schema::Commands) -> Result<Command, ClassifiedError> {
match command {
cli_schema::Commands::Config { subcommand } => convert_config_subcommand(subcommand),
cli_schema::Commands::Auth { subcommand } => convert_auth_subcommand(subcommand),
cli_schema::Commands::Setup {
opencode,
claude,
Expand Down Expand Up @@ -476,6 +477,32 @@ fn convert_clap_command(command: cli_schema::Commands) -> Result<Command, Classi
}
}

fn convert_auth_subcommand(
subcommand: cli_schema::AuthSubcommand,
) -> Result<Command, ClassifiedError> {
let subcommand = match subcommand {
cli_schema::AuthSubcommand::Login { format } => {
services::auth_command::AuthSubcommand::Login {
format: convert_output_format(format),
}
}
cli_schema::AuthSubcommand::Logout { format } => {
services::auth_command::AuthSubcommand::Logout {
format: convert_output_format(format),
}
}
cli_schema::AuthSubcommand::Status { format } => {
services::auth_command::AuthSubcommand::Status {
format: convert_output_format(format),
}
}
};

Ok(Command::Auth(services::auth_command::AuthRequest {
subcommand,
}))
}

/// Convert clap output format to service output format.
fn convert_output_format(
format: cli_schema::OutputFormat,
Expand Down Expand Up @@ -592,6 +619,8 @@ fn convert_hooks_subcommand(
fn dispatch(command: &Command) -> Result<String, ClassifiedError> {
match command {
Command::Help => Ok(command_surface::help_text()),
Command::Auth(request) => services::auth_command::run_auth_subcommand(*request)
.map_err(|error| ClassifiedError::runtime(error.to_string())),
Command::Completion(request) => Ok(services::completion::render_completion(*request)),
Command::Config(subcommand) => services::config::run_config_subcommand(subcommand.clone())
.map_err(|error| ClassifiedError::runtime(error.to_string())),
Expand Down Expand Up @@ -1058,6 +1087,44 @@ mod tests {
);
}

#[test]
fn parser_routes_auth_login_subcommand() {
let command = parse_command(vec![
"sce".to_string(),
"auth".to_string(),
"login".to_string(),
])
.expect("auth login should parse");
assert_eq!(
command,
Command::Auth(crate::services::auth_command::AuthRequest {
subcommand: crate::services::auth_command::AuthSubcommand::Login {
format: crate::services::auth_command::AuthFormat::Text,
},
})
);
}

#[test]
fn parser_routes_auth_status_json_subcommand() {
let command = parse_command(vec![
"sce".to_string(),
"auth".to_string(),
"status".to_string(),
"--format".to_string(),
"json".to_string(),
])
.expect("auth status json should parse");
assert_eq!(
command,
Command::Auth(crate::services::auth_command::AuthRequest {
subcommand: crate::services::auth_command::AuthSubcommand::Status {
format: crate::services::auth_command::AuthFormat::Json,
},
})
);
}

#[test]
fn parser_routes_sync_json_format() {
let command = parse_command(vec![
Expand Down
118 changes: 118 additions & 0 deletions cli/src/cli_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ impl Cli {

#[derive(Subcommand, Debug, Clone, PartialEq, Eq)]
pub enum Commands {
/// Authenticate with WorkOS device authorization flow
Auth {
#[command(subcommand)]
subcommand: AuthSubcommand,
},

/// Inspect and validate resolved CLI configuration
Config {
#[command(subcommand)]
Expand Down Expand Up @@ -118,6 +124,31 @@ pub enum Commands {
},
}

/// Config subcommands
#[derive(Subcommand, Debug, Clone, PartialEq, Eq)]
pub enum AuthSubcommand {
/// Start login flow and store credentials
Login {
/// Output format
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
},

/// Clear stored credentials
Logout {
/// Output format
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
},

/// Show current authentication status
Status {
/// Output format
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
},
}

/// Config subcommands
#[derive(Subcommand, Debug, Clone, PartialEq, Eq)]
pub enum ConfigSubcommand {
Expand Down Expand Up @@ -224,6 +255,93 @@ pub enum LogLevel {
mod tests {
use super::*;

#[test]
fn parse_auth_login() {
let cli = Cli::try_parse_from(["sce", "auth", "login"]).expect("auth login should parse");
match cli.command {
Some(Commands::Auth { subcommand }) => match subcommand {
AuthSubcommand::Login { format } => {
assert_eq!(format, OutputFormat::Text);
}
_ => panic!("Expected Login subcommand"),
},
_ => panic!("Expected Auth command"),
}
}

#[test]
fn parse_auth_login_json() {
let cli = Cli::try_parse_from(["sce", "auth", "login", "--format", "json"])
.expect("auth login --format json should parse");
match cli.command {
Some(Commands::Auth { subcommand }) => match subcommand {
AuthSubcommand::Login { format } => {
assert_eq!(format, OutputFormat::Json);
}
_ => panic!("Expected Login subcommand"),
},
_ => panic!("Expected Auth command"),
}
}

#[test]
fn parse_auth_logout() {
let cli = Cli::try_parse_from(["sce", "auth", "logout"]).expect("auth logout should parse");
match cli.command {
Some(Commands::Auth { subcommand }) => match subcommand {
AuthSubcommand::Logout { format } => {
assert_eq!(format, OutputFormat::Text);
}
_ => panic!("Expected Logout subcommand"),
},
_ => panic!("Expected Auth command"),
}
}

#[test]
fn parse_auth_logout_json() {
let cli = Cli::try_parse_from(["sce", "auth", "logout", "--format", "json"])
.expect("auth logout --format json should parse");
match cli.command {
Some(Commands::Auth { subcommand }) => match subcommand {
AuthSubcommand::Logout { format } => {
assert_eq!(format, OutputFormat::Json);
}
_ => panic!("Expected Logout subcommand"),
},
_ => panic!("Expected Auth command"),
}
}

#[test]
fn parse_auth_status() {
let cli = Cli::try_parse_from(["sce", "auth", "status"]).expect("auth status should parse");
match cli.command {
Some(Commands::Auth { subcommand }) => match subcommand {
AuthSubcommand::Status { format } => {
assert_eq!(format, OutputFormat::Text);
}
_ => panic!("Expected Status subcommand"),
},
_ => panic!("Expected Auth command"),
}
}

#[test]
fn parse_auth_status_json() {
let cli = Cli::try_parse_from(["sce", "auth", "status", "--format", "json"])
.expect("auth status --format json should parse");
match cli.command {
Some(Commands::Auth { subcommand }) => match subcommand {
AuthSubcommand::Status { format } => {
assert_eq!(format, OutputFormat::Json);
}
_ => panic!("Expected Status subcommand"),
},
_ => panic!("Expected Auth command"),
}
}

#[test]
fn parse_version_command() {
let cli = Cli::try_parse_from(["sce", "version"]).expect("version should parse");
Expand Down
29 changes: 27 additions & 2 deletions cli/src/command_surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ pub const COMMANDS: &[CommandContract] = &[
status: ImplementationStatus::Implemented,
purpose: "Validate local git-hook installation readiness",
},
CommandContract {
name: services::auth_command::NAME,
status: ImplementationStatus::Implemented,
purpose: "Authenticate with WorkOS and inspect local auth state",
},
CommandContract {
name: services::mcp::NAME,
status: ImplementationStatus::Placeholder,
Expand Down Expand Up @@ -84,13 +89,14 @@ pub fn help_text() -> String {
Usage:\n sce [command]\n\n\
Config usage:\n sce config <show|validate> [--format <text|json>] [options]\n\n\
Setup usage:\n sce setup [--opencode|--claude|--both] [--non-interactive] [--hooks] [--repo <path>]\n\n\
Auth usage:\n sce auth <login|logout|status> [--format <text|json>]\n\n\
Completion usage:\n sce completion --shell <bash|zsh|fish>\n\n\
Output format contract:\n Supported commands accept --format <text|json>\n\n\
Examples:\n sce setup\n sce setup --opencode --non-interactive --hooks\n sce setup --hooks --repo ../demo-repo\n sce doctor --format json\n sce version --format json\n\n\
Examples:\n sce setup\n sce setup --opencode --non-interactive --hooks\n sce setup --hooks --repo ../demo-repo\n sce auth status\n sce auth login --format json\n sce doctor --format json\n sce version --format json\n\n\
Commands:\n{}\n\n\
Setup defaults to interactive target selection when no setup target flag is passed, and installs hooks in the same run.\n\
Use '--hooks' to install required git hooks for the current repository or '--repo <path>' for a specific repository.\n\
`setup`, `doctor`, and `hooks` are implemented; `mcp` and `sync` remain placeholder-oriented.\n",
`setup`, `doctor`, `auth`, `hooks`, `version`, and `completion` are implemented; `mcp` and `sync` remain placeholder-oriented.\n",
command_rows
)
}
Expand Down Expand Up @@ -126,6 +132,25 @@ mod tests {
assert!(help.contains("version"));
}

#[test]
fn command_surface_includes_auth_as_known_implemented_command() {
let auth = COMMANDS
.iter()
.find(|command| command.name == crate::services::auth_command::NAME)
.expect("auth command should be listed");

assert_eq!(auth.status, ImplementationStatus::Implemented);
assert!(crate::command_surface::is_known_command("auth"));
}

#[test]
fn help_text_mentions_auth_usage_examples() {
let help = help_text();
assert!(help.contains("sce auth <login|logout|status> [--format <text|json>]"));
assert!(help.contains("sce auth status"));
assert!(help.contains("sce auth login --format json"));
}

#[test]
fn help_text_mentions_completion_command() {
let help = help_text();
Expand Down
Loading
Loading