diff --git a/AGENTS.md b/AGENTS.md index d539209c96..1b5fdc39f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -152,6 +152,16 @@ await api.invoke('your_command', { request: { ... } }); - Desktop-only host adapters belong in `src/apps/desktop`, then flow through typed capability interfaces and, when event delivery is needed, the production transport adapter. - In shared core, avoid host-specific APIs such as `tauri::AppHandle`; use shared abstractions such as `bitfun_events::EventEmitter`. +#### Child processes in GUI hosts + +- GUI hosts such as Desktop and Installer must not spawn child processes with bare + `std::process::Command` or `tokio::process::Command`. Prefer + `bitfun_services_core::process_manager::{create_command, create_tokio_command}` + or the existing facade for that layer. If a direct command is unavoidable, + Windows code must explicitly apply `CREATE_NO_WINDOW`; Node child processes + must set `windowsHide: true`. Apply the same policy to test fixtures so tests + do not flash console windows either. + ### Remote scenarios BitFun is not a local-only desktop app. The workspace, the runtime that executes diff --git a/src/apps/desktop/src/api/dispatch_host.rs b/src/apps/desktop/src/api/dispatch_host.rs index 71ebb3775e..cc13b3ef34 100644 --- a/src/apps/desktop/src/api/dispatch_host.rs +++ b/src/apps/desktop/src/api/dispatch_host.rs @@ -10,7 +10,6 @@ use std::time::Duration; use anyhow::{anyhow, Context}; use serde_json::Value; use tokio::io::AsyncWriteExt; -use tokio::process::Command; const TARGET_COMMAND_TIMEOUT: Duration = Duration::from_secs(110); const MAX_TARGET_RESPONSE_BYTES: usize = 4 * 1024 * 1024; @@ -53,7 +52,7 @@ fn target_cli_verb(command: &str) -> Option<&'static str> { async fn invoke_cli(executable: &Path, verb: &str, args: Value) -> anyhow::Result { let request = serde_json::to_vec(&args).context("serialize target dispatch request")?; - let mut child = Command::new(executable) + let mut child = bitfun_core::util::process_manager::create_tokio_command(executable) .arg("dispatch") .arg(verb) .stdin(std::process::Stdio::piped()) diff --git a/src/crates/services/services-integrations/src/workspace_search/auto_index.rs b/src/crates/services/services-integrations/src/workspace_search/auto_index.rs index 3444438f99..45df31e4b7 100644 --- a/src/crates/services/services-integrations/src/workspace_search/auto_index.rs +++ b/src/crates/services/services-integrations/src/workspace_search/auto_index.rs @@ -1,6 +1,6 @@ use std::path::{Path, PathBuf}; -use std::process::Command; +use bitfun_services_core::process_manager; use tokio::task::spawn_blocking; pub(crate) const DEFAULT_AUTO_INDEX_MIN_FILES: usize = 2_000; @@ -99,7 +99,7 @@ fn git_ls_files_indexable_count( policy: AutoIndexPolicy, carried: usize, ) -> Result { - let output = Command::new("git") + let output = process_manager::create_command("git") .arg("ls-files") .args(selectors) .arg("-z") @@ -128,7 +128,7 @@ fn git_ls_files_indexable_count( } fn git_worktree_root(repo_root: &Path) -> Result { - let output = Command::new("git") + let output = process_manager::create_command("git") .args(["rev-parse", "--show-toplevel"]) .current_dir(repo_root) .output() @@ -146,7 +146,7 @@ fn git_worktree_root(repo_root: &Path) -> Result { } let worktree_root = dunce::canonicalize(root) .map_err(|error| format!("cannot canonicalize Git worktree root: {error}"))?; - let head = Command::new("git") + let head = process_manager::create_command("git") .args(["rev-parse", "--verify", "HEAD^{commit}"]) .current_dir(&worktree_root) .output() @@ -179,7 +179,7 @@ mod tests { #[test] fn git_count_uses_visible_tracked_and_untracked_files_and_size_limit() { let repo = TempDir::new().expect("temp repo"); - Command::new("git") + process_manager::create_command("git") .args(["init", "--quiet"]) .current_dir(repo.path()) .status() @@ -187,12 +187,12 @@ mod tests { write(repo.path().join("tracked.txt"), "tracked").expect("write tracked"); write(repo.path().join("untracked.txt"), "untracked").expect("write untracked"); write(repo.path().join("large.bin"), vec![0_u8; 160]).expect("write large file"); - Command::new("git") + process_manager::create_command("git") .args(["add", "tracked.txt"]) .current_dir(repo.path()) .status() .expect("git add should work"); - Command::new("git") + process_manager::create_command("git") .args([ "-c", "user.name=BitFun Test", @@ -228,7 +228,7 @@ mod tests { #[test] fn git_workspaces_without_a_head_are_unsupported() { let repo = TempDir::new().expect("temp repo"); - Command::new("git") + process_manager::create_command("git") .args(["init", "--quiet"]) .current_dir(repo.path()) .status()