Skip to content
Open
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
46 changes: 12 additions & 34 deletions crates/alien-cli/src/commands/platform/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,21 @@ use crate::auth::{force_login, save_workspace};
use crate::commands::platform::workspace::{prompt_workspace, validate_workspace_name};
use crate::error::Result;
use crate::execution_context::ExecutionMode;
use crate::output::print_json;
use crate::ui::{command, contextual_heading, dim_label, success_line};
use clap::Parser;
use serde::Serialize;

#[derive(Parser, Debug, Clone)]
#[command(
about = "Authenticate with Alien and choose a default workspace",
long_about = "Authenticate with the Alien platform and set the default workspace used by platform-managed commands.",
after_help = "EXAMPLES:
alien login
alien login --workspace my-workspace
alien login --workspace my-workspace --json"
alien login --workspace my-workspace"
)]
pub struct LoginArgs {
/// Emit structured JSON output
#[arg(long)]
pub json: bool,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct LoginOutput {
workspace: String,
used_api_key: bool,
}
pub struct LoginArgs {}

pub async fn login_task(args: LoginArgs, ctx: ExecutionMode) -> Result<()> {
pub async fn login_task(_args: LoginArgs, ctx: ExecutionMode) -> Result<()> {
let auth_opts = ctx.auth_opts();
let used_api_key = auth_opts.api_key.is_some();
let http = force_login(&auth_opts).await?;

let workspace = if let ExecutionMode::Platform {
Expand All @@ -41,26 +26,19 @@ pub async fn login_task(args: LoginArgs, ctx: ExecutionMode) -> Result<()> {
{
validate_workspace_name(&http, workspace).await?
} else {
prompt_workspace(&http, args.json).await?
prompt_workspace(&http, false).await?
};

save_workspace(&workspace)?;

if args.json {
print_json(&LoginOutput {
workspace,
used_api_key,
})?;
} else {
println!("{}", contextual_heading("Logged in to", &workspace, &[]));
println!("{}", success_line("Workspace ready."));
println!(
"{} run {} in a project directory or {}.",
dim_label("Next"),
command("alien link"),
command("alien release --project <name>")
);
}
println!("{}", contextual_heading("Logged in to", &workspace, &[]));
println!("{}", success_line("Workspace ready."));
println!(
"{} run {} in a project directory or {}.",
dim_label("Next"),
command("alien link"),
command("alien release --project <name>")
);

Ok(())
}
10 changes: 6 additions & 4 deletions crates/alien-cli/src/commands/platform/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::auth::{load_workspace, save_workspace};
use crate::error::{ErrorData, Result};
use crate::execution_context::ExecutionMode;
use crate::interaction::InteractionMode;
use crate::output::{can_prompt, print_json, prompt_select};
use crate::output::{print_json, prompt_select};
use crate::ui::{command, dim_label, make_table, print_table, success_line};
use alien_error::{AlienError, Context};
use alien_platform_api::SdkResultExt;
Expand Down Expand Up @@ -167,9 +167,11 @@ pub async fn prompt_workspace(http: &crate::auth::AuthHttp, json_mode: bool) ->
return Ok(workspaces[0].clone());
}

InteractionMode::new(json_mode, can_prompt()).require_prompt(
"Workspace selection requires a real terminal. Pass `--workspace <name>` or run `alien workspaces set <name>` first.",
)?;
if InteractionMode::current(json_mode).is_machine() {
return Err(AlienError::new(ErrorData::WorkspaceSelectionRequired {
workspaces,
}));
}

prompt_select("Select a workspace:", &workspaces)
}
38 changes: 38 additions & 0 deletions crates/alien-cli/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,19 @@ pub enum ErrorData {
field: String,
},

/// Several workspaces exist but one can't be chosen without a terminal.
#[error(
code = "WORKSPACE_SELECTION_REQUIRED",
message = "Several workspaces are available; selecting one needs an interactive terminal",
hint = "Run `alien workspaces ls` to see them, then `alien workspaces set <name>` (or pass `--workspace <name>`).",
retryable = "false",
internal = "false"
)]
WorkspaceSelectionRequired {
/// Names the caller can pass to `--workspace`, exposed under `context.workspaces`.
workspaces: Vec<String>,
},

/// Invalid project name specified.
#[error(
code = "INVALID_PROJECT_NAME",
Expand Down Expand Up @@ -294,3 +307,28 @@ pub enum ErrorData {
}

pub type Result<T> = alien_error::Result<T, ErrorData>;

#[cfg(test)]
mod tests {
use super::*;
use alien_error::AlienErrorData;

#[test]
fn workspace_selection_required_exposes_names_in_context() {
let err = ErrorData::WorkspaceSelectionRequired {
workspaces: vec!["alpha".to_string(), "beta".to_string()],
};

assert_eq!(err.code(), "WORKSPACE_SELECTION_REQUIRED");

let context = err.context().expect("variant exposes a context payload");
let names: Vec<&str> = context
.get("workspaces")
.and_then(|value| value.as_array())
.expect("context.workspaces is an array")
.iter()
.filter_map(|value| value.as_str())
.collect();
assert_eq!(names, vec!["alpha", "beta"]);
}
}
2 changes: 1 addition & 1 deletion crates/alien-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ impl Cli {
#[cfg(feature = "platform")]
Some(Commands::Platform(PlatformCommand::Link(args))) => args.json,
#[cfg(feature = "platform")]
Some(Commands::Platform(PlatformCommand::Login(args))) => args.json,
Some(Commands::Platform(PlatformCommand::Login(_))) => false,
#[cfg(feature = "platform")]
Some(Commands::Platform(PlatformCommand::Workspaces(args))) => args.json,
#[cfg(feature = "platform")]
Expand Down
2 changes: 1 addition & 1 deletion crates/alien-cli/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1531,7 +1531,7 @@ fn build_resource_noun(
}

#[cfg(test)]
mod tests {
mod command_event_tests {
use super::*;

fn empty_command_state() -> CommandEventState {
Expand Down
Loading