mirror of
https://github.com/lahfir/agent-desktop.git
synced 2026-07-31 11:19:16 +00:00
300 lines
9.9 KiB
Rust
300 lines
9.9 KiB
Rust
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
|
|
|
|
mod batch;
|
|
mod cli;
|
|
mod cli_args;
|
|
mod command_policy;
|
|
mod diagnostic;
|
|
mod dispatch;
|
|
|
|
/// Shared blanket-default `PlatformAdapter` test double, sourced once from
|
|
/// `tests/support/noop_ops.rs` (also consumed by the standalone
|
|
/// `conformance` integration-test crate) and reused by any in-crate unit
|
|
/// test that needs "some adapter" without exercising a live capability.
|
|
#[cfg(test)]
|
|
#[path = "../tests/support/noop_ops.rs"]
|
|
mod test_noop_ops;
|
|
|
|
use agent_desktop_core::{
|
|
AdapterError, AppError, DeliverySemantics, ErrorCode,
|
|
context::{CommandContext, WaitSelector},
|
|
output::{ErrorPayload, Response},
|
|
session::resolve_active_session,
|
|
};
|
|
use clap::{CommandFactory, Parser};
|
|
use cli::{Cli, Commands};
|
|
use cli_args::skills::SkillsAction;
|
|
use std::io::{BufWriter, Write};
|
|
use std::process::ExitCode;
|
|
|
|
const EXIT_INVALID_ARGS: u8 = 2;
|
|
|
|
fn main() -> ExitCode {
|
|
#[cfg(target_os = "macos")]
|
|
if let Some(exit_code) = run_permission_prompt_helper() {
|
|
return exit_code;
|
|
}
|
|
run()
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
fn run_permission_prompt_helper() -> Option<ExitCode> {
|
|
let (exit_code, response) = agent_desktop_macos::permission_prompt_helper_from_env()?;
|
|
let stdout = std::io::stdout();
|
|
let mut writer = BufWriter::new(stdout.lock());
|
|
let result = writer
|
|
.write_all(response.as_bytes())
|
|
.and_then(|()| writer.write_all(b"\n"))
|
|
.and_then(|()| writer.flush());
|
|
Some(match result {
|
|
Ok(()) => ExitCode::from(exit_code),
|
|
Err(error) => report_output_failure(error),
|
|
})
|
|
}
|
|
|
|
fn run() -> ExitCode {
|
|
#[cfg(target_os = "windows")]
|
|
if let Err(bootstrap_error) = agent_desktop_windows::ensure_owned_process_mta_and_dpi() {
|
|
return finish("unknown", Err(pre_dispatch_error(bootstrap_error.into())));
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
let _ = agent_desktop_core::install_private_file_ops(Box::new(
|
|
agent_desktop_windows::WindowsPrivateFile,
|
|
));
|
|
|
|
let mut cli = match Cli::try_parse() {
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
if matches!(
|
|
e.kind(),
|
|
clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion
|
|
) {
|
|
return match e.print() {
|
|
Ok(()) => ExitCode::SUCCESS,
|
|
Err(write_err) => report_output_failure(write_err),
|
|
};
|
|
}
|
|
let msg = e.to_string();
|
|
let first_line = msg.lines().next().unwrap_or("parse error");
|
|
let error = AppError::invalid_input(diagnostic::bounded_text(first_line, 512));
|
|
return if let Err(write_err) = emit_response(&Response::err(
|
|
"unknown",
|
|
ErrorPayload::from_app_error(&error),
|
|
)) {
|
|
report_output_failure(write_err)
|
|
} else {
|
|
ExitCode::from(EXIT_INVALID_ARGS)
|
|
};
|
|
}
|
|
};
|
|
|
|
init_tracing(cli.verbose);
|
|
|
|
let cmd = match cli.command.take() {
|
|
Some(c) => c,
|
|
None => {
|
|
return match Cli::command().print_help() {
|
|
Ok(()) => ExitCode::SUCCESS,
|
|
Err(write_err) => report_output_failure(write_err),
|
|
};
|
|
}
|
|
};
|
|
|
|
let cmd_name = cmd.name();
|
|
|
|
match cmd {
|
|
Commands::Version => finish(
|
|
cmd_name,
|
|
agent_desktop_core::commands::version::execute().map_err(pre_dispatch_error),
|
|
),
|
|
Commands::Skills(a) => {
|
|
let result = match a.action.unwrap_or(SkillsAction::List) {
|
|
SkillsAction::List => agent_desktop_core::commands::skills::list(),
|
|
SkillsAction::Path => agent_desktop_core::commands::skills::path(),
|
|
SkillsAction::Get(g) => agent_desktop_core::commands::skills::get(
|
|
agent_desktop_core::commands::skills::GetArgs {
|
|
name: g.name,
|
|
full: g.full,
|
|
reference: g.reference,
|
|
},
|
|
),
|
|
};
|
|
finish(cmd_name, result.map_err(pre_dispatch_error))
|
|
}
|
|
cmd => {
|
|
let wait_selector = match build_wait_selector(&cli) {
|
|
Ok(wait_selector) => wait_selector,
|
|
Err(error) => return finish(cmd_name, Err(error)),
|
|
};
|
|
let session_id = match resolve_active_session(
|
|
cli.session.as_deref(),
|
|
std::env::var("AGENT_DESKTOP_SESSION").ok().as_deref(),
|
|
) {
|
|
Ok(session_id) => session_id,
|
|
Err(err) => {
|
|
return finish(cmd_name, Err(pre_dispatch_error(err)));
|
|
}
|
|
};
|
|
let context = match CommandContext::new(session_id, cli.trace, cli.trace_strict) {
|
|
Ok(context) => context
|
|
.with_headed(cli.headed)
|
|
.with_wait_selector(wait_selector.clone()),
|
|
Err(err) => {
|
|
return finish(cmd_name, Err(pre_dispatch_error(err)));
|
|
}
|
|
};
|
|
if let Some(wait) = wait_selector.as_ref() {
|
|
if let Err(err) = validate_wait_for_command(&cmd, wait) {
|
|
return finish(cmd_name, Err(err));
|
|
}
|
|
}
|
|
run_with_adapter(cmd, cmd_name, &context)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn build_wait_selector(cli: &Cli) -> Result<Option<WaitSelector>, AppError> {
|
|
let query = cli
|
|
.post_action_wait
|
|
.wait_for
|
|
.as_ref()
|
|
.map(|raw| (raw, false))
|
|
.or_else(|| {
|
|
cli.post_action_wait
|
|
.wait_for_gone
|
|
.as_ref()
|
|
.map(|raw| (raw, true))
|
|
});
|
|
let Some((query_raw, gone)) = query else {
|
|
if cli.post_action_wait.wait_timeout.is_some() {
|
|
return Err(AppError::invalid_input_with_suggestion(
|
|
"--wait-timeout requires --wait-for or --wait-for-gone",
|
|
"Add a selector wait flag or remove --wait-timeout",
|
|
));
|
|
}
|
|
return Ok(None);
|
|
};
|
|
Ok(Some(WaitSelector {
|
|
query_raw: query_raw.clone(),
|
|
gone,
|
|
timeout_ms: cli.post_action_wait.wait_timeout.unwrap_or(30_000),
|
|
}))
|
|
}
|
|
|
|
fn validate_wait_for_command(cmd: &Commands, wait: &WaitSelector) -> Result<(), AppError> {
|
|
if !cmd.supports_post_action_wait() {
|
|
return Err(AppError::invalid_input_with_suggestion(
|
|
format!(
|
|
"Command '{}' does not support --wait-for or --wait-for-gone",
|
|
cmd.name()
|
|
),
|
|
"Use snapshot --wait-for \"<selector>\" or a supported ref action (click, type, …).",
|
|
));
|
|
}
|
|
agent_desktop_core::commands::query::validate_selector(&wait.query_raw)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn run_with_adapter(cmd: Commands, cmd_name: &str, context: &CommandContext) -> ExitCode {
|
|
let adapter = build_adapter();
|
|
let adapter: &dyn agent_desktop_core::PlatformAdapter = &adapter;
|
|
let report = if command_policy::requires_permission_report(&cmd) {
|
|
match agent_desktop_core::Deadline::standard()
|
|
.map_err(AppError::from)
|
|
.and_then(|deadline| adapter.permission_report(deadline).map_err(AppError::from))
|
|
{
|
|
Ok(report) => report,
|
|
Err(err) => return finish(cmd_name, Err(pre_dispatch_error(err))),
|
|
}
|
|
} else {
|
|
agent_desktop_core::PermissionReport::default()
|
|
};
|
|
if let Err(err) = command_policy::preflight(&cmd, &report) {
|
|
return finish(cmd_name, Err(err));
|
|
}
|
|
|
|
let result = dispatch::dispatch(cmd, adapter, &report, context);
|
|
finish(cmd_name, result)
|
|
}
|
|
|
|
fn pre_dispatch_error(error: AppError) -> AppError {
|
|
match error {
|
|
AppError::Adapter(mut source) => {
|
|
source.disposition = DeliverySemantics::not_delivered();
|
|
source.into()
|
|
}
|
|
other => AdapterError::new(ErrorCode::Internal, other.to_string())
|
|
.with_disposition(DeliverySemantics::not_delivered())
|
|
.into(),
|
|
}
|
|
}
|
|
|
|
fn finish(cmd_name: &str, result: Result<serde_json::Value, AppError>) -> ExitCode {
|
|
match result {
|
|
Ok(data) => {
|
|
if let Err(write_err) = emit_response(&Response::ok(cmd_name, data)) {
|
|
return report_output_failure(write_err);
|
|
}
|
|
ExitCode::SUCCESS
|
|
}
|
|
Err(e) => {
|
|
if let Err(write_err) = emit_response(&Response::err(
|
|
cmd_name,
|
|
agent_desktop_core::ErrorPayload::from_app_error(&e),
|
|
)) {
|
|
return report_output_failure(write_err);
|
|
}
|
|
ExitCode::FAILURE
|
|
}
|
|
}
|
|
}
|
|
|
|
fn report_output_failure(write_err: std::io::Error) -> ExitCode {
|
|
eprintln!("agent-desktop: failed to write response to stdout: {write_err}");
|
|
ExitCode::FAILURE
|
|
}
|
|
|
|
fn emit_response(response: &Response) -> std::io::Result<()> {
|
|
let stdout = std::io::stdout();
|
|
let mut writer = BufWriter::new(stdout.lock());
|
|
serde_json::to_writer(&mut writer, response).map_err(std::io::Error::other)?;
|
|
writer.write_all(b"\n")?;
|
|
writer.flush()
|
|
}
|
|
|
|
fn build_adapter() -> impl agent_desktop_core::PlatformAdapter {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
agent_desktop_macos::MacOSAdapter::new()
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
agent_desktop_windows::WindowsAdapter::new()
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
agent_desktop_linux::LinuxAdapter::new()
|
|
}
|
|
|
|
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
|
compile_error!("Unsupported platform")
|
|
}
|
|
|
|
fn init_tracing(verbose: bool) {
|
|
use tracing_subscriber::{EnvFilter, fmt};
|
|
let filter = if verbose { "debug" } else { "warn" };
|
|
fmt()
|
|
.with_env_filter(
|
|
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(filter)),
|
|
)
|
|
.with_writer(std::io::stderr)
|
|
.with_ansi(false)
|
|
.init();
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "tests/main_tests.rs"]
|
|
mod main_tests;
|