Skip to content

Commit d616f4d

Browse files
committed
auth: Enable Tokio I/O for login runtime
1 parent 52e3834 commit d616f4d

File tree

13 files changed

+453
-27
lines changed

13 files changed

+453
-27
lines changed

cli/src/app.rs

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ impl std::error::Error for ClassifiedError {}
105105
#[derive(Clone, Debug, Eq, PartialEq)]
106106
enum Command {
107107
Help,
108+
HelpText { name: String, text: String },
108109
Auth(services::auth_command::AuthRequest),
109110
Completion(services::completion::CompletionRequest),
110111
Config(services::config::ConfigSubcommand),
@@ -117,9 +118,10 @@ enum Command {
117118
}
118119

119120
impl Command {
120-
fn name(&self) -> &'static str {
121+
fn name(&self) -> &str {
121122
match self {
122123
Self::Help => "help",
124+
Self::HelpText { name, .. } => name.as_str(),
123125
Self::Auth(_) => services::auth_command::NAME,
124126
Self::Completion(_) => services::completion::NAME,
125127
Self::Config(_) => services::config::NAME,
@@ -294,11 +296,22 @@ where
294296
Err(error) => {
295297
// Handle --help specially - user explicitly requested help
296298
if error.kind() == clap::error::ErrorKind::DisplayHelp {
299+
if let Some((name, text)) = render_subcommand_help_from_args(&args_vec) {
300+
return Ok(Command::HelpText { name, text });
301+
}
302+
297303
// Return Help command for successful output
298304
return Ok(Command::Help);
299305
}
300306
// Handle missing subcommand as validation error, not help display
301307
if error.kind() == clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand {
308+
if args_vec.get(1).map(String::as_str) == Some(services::auth_command::NAME) {
309+
return Ok(Command::HelpText {
310+
name: services::auth_command::NAME.to_string(),
311+
text: cli_schema::auth_help_text(),
312+
});
313+
}
314+
302315
// This means a required subcommand was not provided
303316
return Err(ClassifiedError::parse(
304317
"Missing required subcommand. Try: run 'sce --help' to see valid commands.",
@@ -350,6 +363,25 @@ fn classify_clap_error(error: &clap::Error) -> ClassifiedError {
350363
}
351364
}
352365

366+
fn render_subcommand_help_from_args(args: &[String]) -> Option<(String, String)> {
367+
let command_name = args.get(1)?.clone();
368+
let command_path = args[1..]
369+
.iter()
370+
.take_while(|arg| !arg.starts_with('-'))
371+
.map(String::as_str)
372+
.collect::<Vec<_>>();
373+
374+
if command_path.is_empty() {
375+
return None;
376+
}
377+
378+
if command_path.as_slice() == [services::auth_command::NAME] {
379+
return Some((command_name, cli_schema::auth_help_text()));
380+
}
381+
382+
cli_schema::render_help_for_path(&command_path).map(|text| (command_name, text))
383+
}
384+
353385
/// Clean up clap error messages to match our error message style.
354386
fn clean_clap_error_message(message: &str, kind: clap::error::ErrorKind) -> String {
355387
use clap::error::ErrorKind;
@@ -603,6 +635,7 @@ fn convert_hooks_subcommand(
603635
fn dispatch(command: &Command) -> Result<String, ClassifiedError> {
604636
match command {
605637
Command::Help => Ok(command_surface::help_text()),
638+
Command::HelpText { text, .. } => Ok(text.clone()),
606639
Command::Auth(request) => services::auth_command::run_auth_subcommand(*request)
607640
.map_err(|error| ClassifiedError::runtime(error.to_string())),
608641
Command::Completion(request) => Ok(services::completion::render_completion(*request)),
@@ -700,6 +733,68 @@ mod tests {
700733
assert!(stdout.contains("Usage:"));
701734
}
702735

736+
#[test]
737+
fn bare_auth_writes_auth_help_to_stdout() {
738+
let mut stdout = Vec::new();
739+
let mut stderr = Vec::new();
740+
let code = run_with_dependency_check_and_streams(
741+
vec!["sce".to_string(), "auth".to_string()],
742+
|| Ok(()),
743+
&mut stdout,
744+
&mut stderr,
745+
);
746+
assert_eq!(code, ExitCode::SUCCESS);
747+
assert!(stderr.is_empty());
748+
749+
let stdout = String::from_utf8(stdout).expect("stdout should be utf-8");
750+
assert!(stdout.contains("Usage: auth <COMMAND>"));
751+
assert!(stdout.contains("login"));
752+
assert!(stdout.contains("status"));
753+
assert!(stdout.contains("sce auth status"));
754+
}
755+
756+
#[test]
757+
fn auth_help_writes_auth_specific_help_to_stdout() {
758+
let mut stdout = Vec::new();
759+
let mut stderr = Vec::new();
760+
let code = run_with_dependency_check_and_streams(
761+
vec!["sce".to_string(), "auth".to_string(), "--help".to_string()],
762+
|| Ok(()),
763+
&mut stdout,
764+
&mut stderr,
765+
);
766+
assert_eq!(code, ExitCode::SUCCESS);
767+
assert!(stderr.is_empty());
768+
769+
let stdout = String::from_utf8(stdout).expect("stdout should be utf-8");
770+
assert!(stdout.contains("Authenticate with `WorkOS` device authorization flow"));
771+
assert!(stdout.contains("Usage: auth <COMMAND>"));
772+
assert!(stdout.contains("sce auth login"));
773+
}
774+
775+
#[test]
776+
fn auth_login_help_writes_nested_auth_help_to_stdout() {
777+
let mut stdout = Vec::new();
778+
let mut stderr = Vec::new();
779+
let code = run_with_dependency_check_and_streams(
780+
vec![
781+
"sce".to_string(),
782+
"auth".to_string(),
783+
"login".to_string(),
784+
"--help".to_string(),
785+
],
786+
|| Ok(()),
787+
&mut stdout,
788+
&mut stderr,
789+
);
790+
assert_eq!(code, ExitCode::SUCCESS);
791+
assert!(stderr.is_empty());
792+
793+
let stdout = String::from_utf8(stdout).expect("stdout should be utf-8");
794+
assert!(stdout.contains("Start login flow and store credentials"));
795+
assert!(stdout.contains("--format <FORMAT>"));
796+
}
797+
703798
#[test]
704799
fn parse_failure_keeps_stdout_empty_and_reports_stderr() {
705800
let mut stdout = Vec::new();
@@ -1121,6 +1216,20 @@ mod tests {
11211216
);
11221217
}
11231218

1219+
#[test]
1220+
fn parser_routes_bare_auth_to_auth_help_text() {
1221+
let command = parse_command(vec!["sce".to_string(), "auth".to_string()])
1222+
.expect("bare auth should parse to help text");
1223+
1224+
match command {
1225+
Command::HelpText { name, text } => {
1226+
assert_eq!(name, crate::services::auth_command::NAME);
1227+
assert!(text.contains("Usage: auth <COMMAND>"));
1228+
}
1229+
other => panic!("expected auth help text, got {other:?}"),
1230+
}
1231+
}
1232+
11241233
#[test]
11251234
fn parser_routes_sync_json_format() {
11261235
let command = parse_command(vec![

cli/src/cli_schema.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//!
33
//! This module defines the complete command-line interface using clap derive macros.
44
5-
use clap::{Parser, Subcommand, ValueEnum};
5+
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
66
use std::path::PathBuf;
77

88
/// Shared Context Engineering CLI
@@ -35,6 +35,27 @@ impl Cli {
3535
}
3636
}
3737

38+
pub fn render_help_for_path(path: &[&str]) -> Option<String> {
39+
let mut command = Cli::command();
40+
41+
for segment in path {
42+
command = command.find_subcommand_mut(segment)?.clone();
43+
}
44+
45+
let mut buffer = Vec::new();
46+
command
47+
.write_long_help(&mut buffer)
48+
.expect("help rendering should write to memory");
49+
50+
Some(String::from_utf8(buffer).expect("help output should be valid UTF-8"))
51+
}
52+
53+
pub fn auth_help_text() -> String {
54+
let base = render_help_for_path(&["auth"]).expect("auth help should be renderable");
55+
56+
format!("{base}\nExamples:\n sce auth login\n sce auth status\n sce auth logout\n")
57+
}
58+
3859
#[derive(Subcommand, Debug, Clone, PartialEq, Eq)]
3960
pub enum Commands {
4061
/// Authenticate with `WorkOS` device authorization flow
@@ -343,6 +364,25 @@ mod tests {
343364
}
344365
}
345366

367+
#[test]
368+
fn auth_help_text_lists_supported_subcommands() {
369+
let help = auth_help_text();
370+
371+
assert!(help.contains("Usage: auth <COMMAND>"));
372+
assert!(help.contains("login"));
373+
assert!(help.contains("logout"));
374+
assert!(help.contains("status"));
375+
assert!(help.contains("sce auth status"));
376+
}
377+
378+
#[test]
379+
fn render_help_for_auth_login_path_is_specific_to_login() {
380+
let help = render_help_for_path(&["auth", "login"]).expect("auth login help should render");
381+
382+
assert!(help.contains("Start login flow and store credentials"));
383+
assert!(help.contains("--format <FORMAT>"));
384+
}
385+
346386
#[test]
347387
fn parse_version_command() {
348388
let cli = Cli::try_parse_from(["sce", "version"]).expect("version should parse");

cli/src/services/auth_command.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ fn shared_runtime() -> Result<&'static tokio::runtime::Runtime> {
115115
}
116116

117117
let runtime = tokio::runtime::Builder::new_current_thread()
118+
.enable_io()
118119
.enable_time()
119120
.build()
120121
.context("failed to create auth command runtime. Try: rerun the command; if the issue persists, verify the local Tokio runtime environment.")?;
@@ -296,6 +297,7 @@ mod tests {
296297
use anyhow::{anyhow, Result};
297298
use serde_json::Value;
298299
use std::path::{Path, PathBuf};
300+
use tokio::net::TcpListener;
299301

300302
use super::{
301303
build_authenticated_status_report, render_login_result, render_logout_result,
@@ -597,6 +599,17 @@ mod tests {
597599
assert_eq!(preserved, "runtime failed. Try: rerun the command.");
598600
}
599601

602+
#[test]
603+
fn shared_runtime_enables_tokio_io_for_auth_login_flow() -> Result<()> {
604+
let runtime = super::shared_runtime()?;
605+
let listener = runtime.block_on(async { TcpListener::bind("127.0.0.1:0").await })?;
606+
607+
let local_addr = listener.local_addr()?;
608+
assert!(local_addr.port() > 0);
609+
610+
Ok(())
611+
}
612+
600613
#[test]
601614
fn dispatcher_preserves_actionable_errors() {
602615
let error = run_auth_subcommand_with(

0 commit comments

Comments
 (0)