Add explicit non-interactive public CLI mode
This commit is contained in:
parent
e6c2a1a981
commit
afd7456423
6 changed files with 226 additions and 5 deletions
|
|
@ -117,6 +117,8 @@ struct LoginArgs {
|
|||
#[arg(long)]
|
||||
browser: bool,
|
||||
#[arg(long)]
|
||||
non_interactive: bool,
|
||||
#[arg(long)]
|
||||
plan: bool,
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
|
|
@ -285,6 +287,8 @@ struct RunArgs {
|
|||
#[arg(long)]
|
||||
local: bool,
|
||||
#[arg(long)]
|
||||
non_interactive: bool,
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
}
|
||||
|
||||
|
|
@ -837,6 +841,37 @@ fn auth_logout_report(args: AuthLogoutArgs, cwd: PathBuf) -> Result<Value> {
|
|||
logout_report(args, cwd, "auth logout")
|
||||
}
|
||||
|
||||
fn non_interactive_auth_machine_error(message: &str, next_actions: Vec<&'static str>) -> Value {
|
||||
let mut machine_error = cli_error_summary_for_category("authentication", message);
|
||||
if let Some(object) = machine_error.as_object_mut() {
|
||||
object.insert("next_actions".to_owned(), json!(next_actions));
|
||||
object.insert("browser_opened".to_owned(), json!(false));
|
||||
}
|
||||
machine_error
|
||||
}
|
||||
|
||||
fn non_interactive_browser_login_report(args: &LoginArgs) -> Value {
|
||||
let message =
|
||||
"browser login requires an interactive browser, but non-interactive mode is enabled";
|
||||
let next_actions = vec![
|
||||
"rerun without --non-interactive to open the browser",
|
||||
"disasmer login --browser --plan",
|
||||
"use DISASMER_AGENT_PUBLIC_KEY for automation",
|
||||
];
|
||||
json!({
|
||||
"command": "login",
|
||||
"status": "authentication_required",
|
||||
"coordinator": args.coordinator,
|
||||
"non_interactive": true,
|
||||
"browser_requested": true,
|
||||
"browser_opened": false,
|
||||
"safe_failure": true,
|
||||
"message": message,
|
||||
"next_actions": next_actions,
|
||||
"machine_error": non_interactive_auth_machine_error(message, next_actions),
|
||||
})
|
||||
}
|
||||
|
||||
fn logout_report(args: AuthLogoutArgs, cwd: PathBuf, command: &str) -> Result<Value> {
|
||||
let session_file = session_config_file(&cwd);
|
||||
let existed = session_file.exists();
|
||||
|
|
@ -2593,7 +2628,14 @@ fn run_cli() -> Result<()> {
|
|||
}
|
||||
Commands::Login(args) => {
|
||||
let json_output = args.json;
|
||||
if args.complete_browser_code.is_some() {
|
||||
if args.non_interactive
|
||||
&& args.browser
|
||||
&& !args.plan
|
||||
&& args.complete_browser_code.is_none()
|
||||
{
|
||||
let report = non_interactive_browser_login_report(&args);
|
||||
emit_report(&report, json_output)?;
|
||||
} else if args.complete_browser_code.is_some() {
|
||||
let report = execute_browser_login_completion(args)?;
|
||||
emit_report(&report, json_output)?;
|
||||
} else if args.browser && !args.plan {
|
||||
|
|
@ -4297,7 +4339,39 @@ fn run_plan(args: RunArgs, cwd: PathBuf, session: CliSession) -> Result<RunPlan>
|
|||
})
|
||||
}
|
||||
|
||||
fn non_interactive_run_requires_auth_report(args: RunArgs, cwd: PathBuf) -> Value {
|
||||
let project = args.project.unwrap_or(cwd);
|
||||
let entry = args.entry.unwrap_or_else(|| "build".to_owned());
|
||||
let message = "non-interactive run requires an authenticated human or agent session unless --local or --coordinator is explicit";
|
||||
let next_actions = vec![
|
||||
"disasmer login --browser",
|
||||
"set DISASMER_AGENT_PUBLIC_KEY for automation",
|
||||
"pass --local to run against local services",
|
||||
"pass --coordinator for an explicit self-hosted coordinator",
|
||||
];
|
||||
json!({
|
||||
"command": "run",
|
||||
"status": "authentication_required",
|
||||
"project_root": project,
|
||||
"entry": entry,
|
||||
"non_interactive": true,
|
||||
"browser_opened": false,
|
||||
"safe_failure": true,
|
||||
"message": message,
|
||||
"next_actions": next_actions,
|
||||
"private_website_required": false,
|
||||
"machine_error": non_interactive_auth_machine_error(message, next_actions),
|
||||
})
|
||||
}
|
||||
|
||||
fn run_report(args: RunArgs, cwd: PathBuf, session: CliSession) -> Result<Value> {
|
||||
if args.non_interactive
|
||||
&& !args.local
|
||||
&& args.coordinator.is_none()
|
||||
&& !session.is_authenticated()
|
||||
{
|
||||
return Ok(non_interactive_run_requires_auth_report(args, cwd));
|
||||
}
|
||||
let plan = run_plan(args, cwd, session)?;
|
||||
if should_execute_local_node(&plan) {
|
||||
return Ok(serde_json::to_value(execute_local_node_run(plan)?)?);
|
||||
|
|
@ -5062,6 +5136,35 @@ mod tests {
|
|||
assert_eq!(plan.session, CliSession::Anonymous);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_interactive_run_without_session_requires_explicit_auth_or_local() {
|
||||
let Cli {
|
||||
command: Commands::Run(args),
|
||||
} = parse(&["disasmer", "run", "--non-interactive", "--json"])
|
||||
else {
|
||||
panic!("wrong command");
|
||||
};
|
||||
let report = run_report(args, PathBuf::from("/repo"), CliSession::Anonymous).unwrap();
|
||||
|
||||
assert_eq!(report["command"], "run");
|
||||
assert_eq!(report["status"], "authentication_required");
|
||||
assert_eq!(report["non_interactive"], true);
|
||||
assert_eq!(report["browser_opened"], false);
|
||||
assert_eq!(report["private_website_required"], false);
|
||||
assert_eq!(report["machine_error"]["category"], "authentication");
|
||||
assert_eq!(report["machine_error"]["stable_exit_code"], 20);
|
||||
assert_eq!(report["machine_error"]["browser_opened"], false);
|
||||
let next_actions = report["machine_error"]["next_actions"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.collect::<Vec<_>>();
|
||||
assert!(next_actions.contains(&"disasmer login --browser"));
|
||||
assert!(next_actions.contains(&"set DISASMER_AGENT_PUBLIC_KEY for automation"));
|
||||
assert!(next_actions.contains(&"pass --local to run against local services"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_project_and_named_entry_are_respected() {
|
||||
let Cli {
|
||||
|
|
@ -5157,6 +5260,7 @@ mod tests {
|
|||
project: None,
|
||||
coordinator: None,
|
||||
local: false,
|
||||
non_interactive: true,
|
||||
json: false,
|
||||
};
|
||||
let plan = run_plan(
|
||||
|
|
@ -5233,6 +5337,7 @@ mod tests {
|
|||
project: Some(temp.path().to_path_buf()),
|
||||
coordinator: Some(format!("http://{addr}")),
|
||||
local: false,
|
||||
non_interactive: true,
|
||||
json: false,
|
||||
},
|
||||
PathBuf::from("/unused"),
|
||||
|
|
@ -5310,6 +5415,7 @@ mod tests {
|
|||
project: Some(temp.path().to_path_buf()),
|
||||
coordinator: Some(format!("http://{addr}")),
|
||||
local: false,
|
||||
non_interactive: false,
|
||||
json: false,
|
||||
},
|
||||
PathBuf::from("/unused"),
|
||||
|
|
@ -5322,6 +5428,7 @@ mod tests {
|
|||
project: Some(temp.path().to_path_buf()),
|
||||
coordinator: Some(format!("http://{addr}")),
|
||||
local: false,
|
||||
non_interactive: false,
|
||||
json: false,
|
||||
},
|
||||
PathBuf::from("/unused"),
|
||||
|
|
@ -5433,6 +5540,34 @@ mod tests {
|
|||
assert!(flow.state.starts_with("sha256:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_login_non_interactive_fails_before_opening_browser() {
|
||||
let Cli {
|
||||
command: Commands::Login(args),
|
||||
} = parse(&["disasmer", "login", "--browser", "--non-interactive"])
|
||||
else {
|
||||
panic!("wrong command");
|
||||
};
|
||||
let report = non_interactive_browser_login_report(&args);
|
||||
|
||||
assert_eq!(report["command"], "login");
|
||||
assert_eq!(report["status"], "authentication_required");
|
||||
assert_eq!(report["non_interactive"], true);
|
||||
assert_eq!(report["browser_requested"], true);
|
||||
assert_eq!(report["browser_opened"], false);
|
||||
assert_eq!(report["machine_error"]["category"], "authentication");
|
||||
assert_eq!(report["machine_error"]["stable_exit_code"], 20);
|
||||
assert_eq!(report["machine_error"]["browser_opened"], false);
|
||||
let next_actions = report["machine_error"]["next_actions"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.collect::<Vec<_>>();
|
||||
assert!(next_actions.contains(&"rerun without --non-interactive to open the browser"));
|
||||
assert!(next_actions.contains(&"use DISASMER_AGENT_PUBLIC_KEY for automation"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_login_completion_detects_raw_provider_token_fields() {
|
||||
assert!(contains_provider_token_field(&json!({
|
||||
|
|
@ -5849,6 +5984,7 @@ mod tests {
|
|||
&["disasmer", "auth", "status"],
|
||||
&["disasmer", "logout", "--yes"],
|
||||
&["disasmer", "auth", "logout", "--yes"],
|
||||
&["disasmer", "login", "--browser", "--non-interactive"],
|
||||
&[
|
||||
"disasmer",
|
||||
"key",
|
||||
|
|
@ -5866,6 +6002,7 @@ mod tests {
|
|||
&["disasmer", "project", "select", "project"],
|
||||
&["disasmer", "inspect"],
|
||||
&["disasmer", "build"],
|
||||
&["disasmer", "run", "--non-interactive"],
|
||||
&["disasmer", "node", "enroll"],
|
||||
&["disasmer", "node", "list"],
|
||||
&["disasmer", "node", "status"],
|
||||
|
|
@ -5961,6 +6098,13 @@ mod tests {
|
|||
for args in [
|
||||
&["disasmer", "doctor", "--json"][..],
|
||||
&["disasmer", "login", "--json"],
|
||||
&[
|
||||
"disasmer",
|
||||
"login",
|
||||
"--browser",
|
||||
"--non-interactive",
|
||||
"--json",
|
||||
],
|
||||
&["disasmer", "logout", "--yes", "--json"],
|
||||
&["disasmer", "auth", "status", "--json"],
|
||||
&[
|
||||
|
|
@ -5986,6 +6130,7 @@ mod tests {
|
|||
&["disasmer", "build", "--json"],
|
||||
&["disasmer", "bundle", "inspect", "--json"],
|
||||
&["disasmer", "run", "--json"],
|
||||
&["disasmer", "run", "--non-interactive", "--json"],
|
||||
&["disasmer", "node", "attach", "--json"],
|
||||
&["disasmer", "node", "enroll", "--json"],
|
||||
&["disasmer", "process", "status", "--json"],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue