Add screen orientation polyfill, refactor provider launch config, and update desktop runtime manager
- Add .claude schedule tasks to .gitignore - Add screen-orientation polyfill for Expo web - Refactor agent provider launch config into shared utility - Update desktop runtime manager with improved binary management - Update desktop release workflow
This commit is contained in:
@@ -521,7 +521,7 @@ jobs:
|
|||||||
releaseBody: See the assets to download and install this version.
|
releaseBody: See the assets to download and install this version.
|
||||||
releaseDraft: ${{ env.RELEASE_DRAFT }}
|
releaseDraft: ${{ env.RELEASE_DRAFT }}
|
||||||
prerelease: false
|
prerelease: false
|
||||||
args: --bundles nsis,msi
|
args: --bundles nsis
|
||||||
|
|
||||||
- name: Build Windows app (smoke only)
|
- name: Build Windows app (smoke only)
|
||||||
if: env.IS_SMOKE_TAG == 'true'
|
if: env.IS_SMOKE_TAG == 'true'
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ valknut-report.json/
|
|||||||
**/.paseo-provider-history/
|
**/.paseo-provider-history/
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
**/.claude/settings.local.json
|
**/.claude/settings.local.json
|
||||||
|
.claude/scheduled_tasks.lock
|
||||||
|
.claude/worktrees/
|
||||||
.plans/
|
.plans/
|
||||||
packages/server/src/server/fixtures/dictation/dictation-debug-largest.wav
|
packages/server/src/server/fixtures/dictation/dictation-debug-largest.wav
|
||||||
packages/server/src/server/fixtures/dictation/dictation-debug-largest.transcript.txt
|
packages/server/src/server/fixtures/dictation/dictation-debug-largest.transcript.txt
|
||||||
|
|||||||
Generated
+10
@@ -9138,6 +9138,15 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-log": {
|
||||||
|
"version": "2.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.8.0.tgz",
|
||||||
|
"integrity": "sha512-a+7rOq3MJwpTOLLKbL8d0qGZ85hgHw5pNOWusA9o3cf7cEgtYHiGY/+O8fj8MvywQIGqFv0da2bYQDlrqLE7rw==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tsconfig/node10": {
|
"node_modules/@tsconfig/node10": {
|
||||||
"version": "1.0.12",
|
"version": "1.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||||
@@ -27589,6 +27598,7 @@
|
|||||||
"@tanstack/react-query": "^5.90.11",
|
"@tanstack/react-query": "^5.90.11",
|
||||||
"@tanstack/react-virtual": "^3.13.21",
|
"@tanstack/react-virtual": "^3.13.21",
|
||||||
"@tauri-apps/api": "^2.9.1",
|
"@tauri-apps/api": "^2.9.1",
|
||||||
|
"@tauri-apps/plugin-log": "^2.8.0",
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/addon-unicode11": "^0.9.0",
|
"@xterm/addon-unicode11": "^0.9.0",
|
||||||
"@xterm/addon-webgl": "^0.19.0",
|
"@xterm/addon-webgl": "^0.19.0",
|
||||||
|
|||||||
@@ -2,6 +2,17 @@
|
|||||||
import { polyfillCrypto } from "./src/polyfills/crypto";
|
import { polyfillCrypto } from "./src/polyfills/crypto";
|
||||||
polyfillCrypto();
|
polyfillCrypto();
|
||||||
|
|
||||||
|
// Polyfill screen.orientation for WebKitGTK (Tauri Linux) which lacks the API
|
||||||
|
import { polyfillScreenOrientation } from "./src/polyfills/screen-orientation";
|
||||||
|
polyfillScreenOrientation();
|
||||||
|
|
||||||
|
// Bridge console.log/warn/error to Tauri's log plugin so JS output appears in app.log
|
||||||
|
if ((globalThis as { __TAURI__?: unknown }).__TAURI__) {
|
||||||
|
import("@tauri-apps/plugin-log").then(({ attachConsole }) => {
|
||||||
|
attachConsole();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Configure Unistyles before Expo Router pulls in any components using StyleSheet.
|
// Configure Unistyles before Expo Router pulls in any components using StyleSheet.
|
||||||
import "./src/styles/unistyles";
|
import "./src/styles/unistyles";
|
||||||
import "expo-router/entry";
|
import "expo-router/entry";
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
"@tanstack/react-query": "^5.90.11",
|
"@tanstack/react-query": "^5.90.11",
|
||||||
"@tanstack/react-virtual": "^3.13.21",
|
"@tanstack/react-virtual": "^3.13.21",
|
||||||
"@tauri-apps/api": "^2.9.1",
|
"@tauri-apps/api": "^2.9.1",
|
||||||
|
"@tauri-apps/plugin-log": "^2.8.0",
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/addon-unicode11": "^0.9.0",
|
"@xterm/addon-unicode11": "^0.9.0",
|
||||||
"@xterm/addon-webgl": "^0.19.0",
|
"@xterm/addon-webgl": "^0.19.0",
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
export function polyfillScreenOrientation() {
|
||||||
|
if (
|
||||||
|
typeof window === "undefined" ||
|
||||||
|
typeof screen === "undefined" ||
|
||||||
|
screen.orientation
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(screen, "orientation", {
|
||||||
|
value: {
|
||||||
|
get type() {
|
||||||
|
return window.innerWidth > window.innerHeight
|
||||||
|
? "landscape-primary"
|
||||||
|
: "portrait-primary";
|
||||||
|
},
|
||||||
|
get angle() {
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
addEventListener() {},
|
||||||
|
removeEventListener() {},
|
||||||
|
dispatchEvent() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
Generated
+1
-1
@@ -2629,7 +2629,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paseo"
|
name = "paseo"
|
||||||
version = "0.1.24"
|
version = "0.1.25"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ fn maybe_run_managed_headless_command(app: &AppHandle) -> Result<bool, String> {
|
|||||||
let Some(command) = parse_managed_headless_command()? else {
|
let Some(command) = parse_managed_headless_command()? else {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
};
|
||||||
|
log::info!("[headless] running managed headless command: {:?}", command);
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
let _ = window.hide();
|
let _ = window.hide();
|
||||||
}
|
}
|
||||||
@@ -625,17 +626,36 @@ pub fn run() {
|
|||||||
garbage_collect_attachment_files
|
garbage_collect_attachment_files
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
log::info!(
|
||||||
|
"[app] Paseo Desktop v{} starting",
|
||||||
|
app.package_info().version
|
||||||
|
);
|
||||||
|
|
||||||
if maybe_run_managed_headless_command(app.handle())? {
|
if maybe_run_managed_headless_command(app.handle())? {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
app.handle().plugin(
|
app.handle().plugin(
|
||||||
tauri_plugin_log::Builder::default()
|
tauri_plugin_log::Builder::default()
|
||||||
.level(log::LevelFilter::Info)
|
.level(log::LevelFilter::Info)
|
||||||
|
.level_for("tao", log::LevelFilter::Warn)
|
||||||
|
.level_for("wry", log::LevelFilter::Warn)
|
||||||
|
.max_file_size(5_000_000)
|
||||||
|
.rotation_strategy(tauri_plugin_log::RotationStrategy::KeepAll)
|
||||||
|
.clear_targets()
|
||||||
|
.target(tauri_plugin_log::Target::new(
|
||||||
|
tauri_plugin_log::TargetKind::LogDir {
|
||||||
|
file_name: Some("app.log".into()),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.target(tauri_plugin_log::Target::new(
|
||||||
|
tauri_plugin_log::TargetKind::Stdout,
|
||||||
|
))
|
||||||
|
.target(tauri_plugin_log::Target::new(
|
||||||
|
tauri_plugin_log::TargetKind::Webview,
|
||||||
|
))
|
||||||
.build(),
|
.build(),
|
||||||
)?;
|
)?;
|
||||||
}
|
|
||||||
|
|
||||||
// Start from Tauri's default menu so macOS standard shortcuts (Cmd+A/C/V/etc)
|
// Start from Tauri's default menu so macOS standard shortcuts (Cmd+A/C/V/etc)
|
||||||
// keep working. Then inject our zoom controls into a View menu.
|
// keep working. Then inject our zoom controls into a View menu.
|
||||||
|
|||||||
@@ -563,13 +563,20 @@ fn bundled_runtime_root(app: &AppHandle) -> Result<PathBuf, String> {
|
|||||||
if let Ok(resource_dir) = app.path().resource_dir() {
|
if let Ok(resource_dir) = app.path().resource_dir() {
|
||||||
let candidate = resource_dir.join("managed-runtime");
|
let candidate = resource_dir.join("managed-runtime");
|
||||||
if candidate.exists() {
|
if candidate.exists() {
|
||||||
|
log::info!("[runtime] found bundled runtime at {}", candidate.display());
|
||||||
return Ok(candidate);
|
return Ok(candidate);
|
||||||
}
|
}
|
||||||
|
log::info!(
|
||||||
|
"[runtime] no bundled runtime at {}, checking dev path",
|
||||||
|
candidate.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let dev = dev_resource_root().join("managed-runtime");
|
let dev = dev_resource_root().join("managed-runtime");
|
||||||
if dev.exists() {
|
if dev.exists() {
|
||||||
|
log::info!("[runtime] using dev runtime at {}", dev.display());
|
||||||
return Ok(dev);
|
return Ok(dev);
|
||||||
}
|
}
|
||||||
|
log::error!("[runtime] no managed runtime found");
|
||||||
Err("Managed runtime resources are not bundled with this desktop build.".to_string())
|
Err("Managed runtime resources are not bundled with this desktop build.".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,17 +722,25 @@ fn cli_command(
|
|||||||
let node = runtime_root.join(&manifest.node_relative_path);
|
let node = runtime_root.join(&manifest.node_relative_path);
|
||||||
let cli = runtime_root.join(&manifest.cli_entrypoint_relative_path);
|
let cli = runtime_root.join(&manifest.cli_entrypoint_relative_path);
|
||||||
if !node.exists() {
|
if !node.exists() {
|
||||||
|
log::error!("[cli] bundled Node missing at {}", node.display());
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Bundled Node runtime is missing at {}",
|
"Bundled Node runtime is missing at {}",
|
||||||
node.display()
|
node.display()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if !cli.exists() {
|
if !cli.exists() {
|
||||||
|
log::error!("[cli] bundled CLI missing at {}", cli.display());
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Bundled CLI entrypoint is missing at {}",
|
"Bundled CLI entrypoint is missing at {}",
|
||||||
cli.display()
|
cli.display()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
log::info!(
|
||||||
|
"[cli] node={} cli={} args={:?}",
|
||||||
|
node.display(),
|
||||||
|
cli.display(),
|
||||||
|
args
|
||||||
|
);
|
||||||
let mut command = Command::new(node);
|
let mut command = Command::new(node);
|
||||||
command.arg(cli);
|
command.arg(cli);
|
||||||
command.args(args);
|
command.args(args);
|
||||||
@@ -893,10 +908,22 @@ fn write_state_file(path: &Path, value: &ManagedStateFile) -> Result<(), String>
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_runtime_ready_internal(app: &AppHandle) -> Result<ManagedRuntimeStatus, String> {
|
fn ensure_runtime_ready_internal(app: &AppHandle) -> Result<ManagedRuntimeStatus, String> {
|
||||||
|
log::info!("[runtime] ensuring runtime is ready");
|
||||||
let (bundled_root, pointer) = load_bundled_runtime_pointer(app)?;
|
let (bundled_root, pointer) = load_bundled_runtime_pointer(app)?;
|
||||||
let runtime_root = bundled_root.join(&pointer.relative_root);
|
let runtime_root = bundled_root.join(&pointer.relative_root);
|
||||||
let paths = resolve_paths(app)?;
|
let paths = resolve_paths(app)?;
|
||||||
|
log::info!(
|
||||||
|
"[runtime] runtime_root={} managed_home={} transport={}",
|
||||||
|
runtime_root.display(),
|
||||||
|
paths.managed_home.display(),
|
||||||
|
paths.transport_path.display()
|
||||||
|
);
|
||||||
let manifest = load_runtime_manifest(&runtime_root)?;
|
let manifest = load_runtime_manifest(&runtime_root)?;
|
||||||
|
log::info!(
|
||||||
|
"[runtime] manifest: id={} version={}",
|
||||||
|
manifest.runtime_id,
|
||||||
|
manifest.runtime_version
|
||||||
|
);
|
||||||
if let Some(parent) = paths.transport_path.parent() {
|
if let Some(parent) = paths.transport_path.parent() {
|
||||||
fs::create_dir_all(parent)
|
fs::create_dir_all(parent)
|
||||||
.map_err(|error| format!("Failed to create {}: {error}", parent.display()))?;
|
.map_err(|error| format!("Failed to create {}: {error}", parent.display()))?;
|
||||||
@@ -905,6 +932,11 @@ fn ensure_runtime_ready_internal(app: &AppHandle) -> Result<ManagedRuntimeStatus
|
|||||||
.map_err(|error| format!("Failed to create {}: {error}", paths.managed_home.display()))?;
|
.map_err(|error| format!("Failed to create {}: {error}", paths.managed_home.display()))?;
|
||||||
let existing_state = read_state_file(&paths.state_file_path);
|
let existing_state = read_state_file(&paths.state_file_path);
|
||||||
let target = managed_transport_target(&paths, existing_state.as_ref())?;
|
let target = managed_transport_target(&paths, existing_state.as_ref())?;
|
||||||
|
log::info!(
|
||||||
|
"[runtime] transport target: type={} path={}",
|
||||||
|
target.transport_type,
|
||||||
|
target.transport_path
|
||||||
|
);
|
||||||
let state = ManagedStateFile {
|
let state = ManagedStateFile {
|
||||||
runtime_id: manifest.runtime_id.clone(),
|
runtime_id: manifest.runtime_id.clone(),
|
||||||
runtime_root: runtime_root.to_string_lossy().into_owned(),
|
runtime_root: runtime_root.to_string_lossy().into_owned(),
|
||||||
@@ -940,20 +972,32 @@ fn run_cli_json_command(
|
|||||||
args: &[&str],
|
args: &[&str],
|
||||||
paths: &ManagedPaths,
|
paths: &ManagedPaths,
|
||||||
) -> Result<serde_json::Value, String> {
|
) -> Result<serde_json::Value, String> {
|
||||||
|
log::info!("[cli] running: {:?}", args);
|
||||||
let output = cli_command(runtime_root, manifest, args, paths)?
|
let output = cli_command(runtime_root, manifest, args, paths)?
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
.output()
|
.output()
|
||||||
.map_err(|error| format!("Failed to run bundled CLI: {error}"))?;
|
.map_err(|error| {
|
||||||
|
log::error!("[cli] failed to spawn: {error}");
|
||||||
|
format!("Failed to run bundled CLI: {error}")
|
||||||
|
})?;
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
log::error!(
|
||||||
|
"[cli] exit={} stderr={}",
|
||||||
|
output.status.code().unwrap_or(-1),
|
||||||
|
stderr.trim()
|
||||||
|
);
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Bundled CLI failed (exit {}): {}",
|
"Bundled CLI failed (exit {}): {}",
|
||||||
output.status.code().unwrap_or(-1),
|
output.status.code().unwrap_or(-1),
|
||||||
String::from_utf8_lossy(&output.stderr).trim()
|
stderr.trim()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
log::info!("[cli] success, parsing JSON output");
|
||||||
serde_json::from_str(stdout.trim()).map_err(|error| {
|
serde_json::from_str(stdout.trim()).map_err(|error| {
|
||||||
|
log::error!("[cli] JSON parse error: {error}; stdout={}", stdout.trim());
|
||||||
format!(
|
format!(
|
||||||
"Failed to parse bundled CLI JSON output: {error}; stdout={}",
|
"Failed to parse bundled CLI JSON output: {error}; stdout={}",
|
||||||
stdout.trim()
|
stdout.trim()
|
||||||
@@ -1101,6 +1145,7 @@ fn remove_cli_shim_via_macos_prompt(shim_path: &Path) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn install_cli_shim_internal(app: &AppHandle) -> Result<CliShimResult, String> {
|
fn install_cli_shim_internal(app: &AppHandle) -> Result<CliShimResult, String> {
|
||||||
|
log::info!("[cli-shim] installing CLI shim");
|
||||||
let status = ensure_runtime_ready_internal(app)?;
|
let status = ensure_runtime_ready_internal(app)?;
|
||||||
let paths = resolve_paths(app)?;
|
let paths = resolve_paths(app)?;
|
||||||
let runtime_root = PathBuf::from(&status.runtime_root);
|
let runtime_root = PathBuf::from(&status.runtime_root);
|
||||||
@@ -1116,6 +1161,7 @@ fn install_cli_shim_internal(app: &AppHandle) -> Result<CliShimResult, String> {
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let install_result: Result<(), String> = Err("AUTOMATIC_INSTALL_UNAVAILABLE".to_string());
|
let install_result: Result<(), String> = Err("AUTOMATIC_INSTALL_UNAVAILABLE".to_string());
|
||||||
|
|
||||||
|
log::info!("[cli-shim] target path: {}", shim_path.display());
|
||||||
match install_result {
|
match install_result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let mut state = read_state_file(&paths.state_file_path).unwrap_or(ManagedStateFile {
|
let mut state = read_state_file(&paths.state_file_path).unwrap_or(ManagedStateFile {
|
||||||
@@ -1209,10 +1255,15 @@ fn uninstall_cli_shim_internal(app: &AppHandle) -> Result<CliShimResult, String>
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn start_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatus, String> {
|
fn start_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatus, String> {
|
||||||
|
log::info!("[daemon] starting managed daemon");
|
||||||
let status = ensure_runtime_ready_internal(app)?;
|
let status = ensure_runtime_ready_internal(app)?;
|
||||||
let paths = resolve_paths(app)?;
|
let paths = resolve_paths(app)?;
|
||||||
let existing_status = managed_daemon_status_internal(app)?;
|
let existing_status = managed_daemon_status_internal(app)?;
|
||||||
if existing_status.daemon_running {
|
if existing_status.daemon_running {
|
||||||
|
log::info!(
|
||||||
|
"[daemon] already running (pid={:?})",
|
||||||
|
existing_status.daemon_pid
|
||||||
|
);
|
||||||
return Ok(existing_status);
|
return Ok(existing_status);
|
||||||
}
|
}
|
||||||
if let Some(parent) = paths.transport_path.parent() {
|
if let Some(parent) = paths.transport_path.parent() {
|
||||||
@@ -1223,6 +1274,12 @@ fn start_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatus,
|
|||||||
let manifest = load_runtime_manifest(&runtime_root)?;
|
let manifest = load_runtime_manifest(&runtime_root)?;
|
||||||
let state = read_state_file(&paths.state_file_path);
|
let state = read_state_file(&paths.state_file_path);
|
||||||
let target = managed_transport_target(&paths, state.as_ref())?;
|
let target = managed_transport_target(&paths, state.as_ref())?;
|
||||||
|
log::info!(
|
||||||
|
"[daemon] spawning: home={} listen={} (type={})",
|
||||||
|
paths.managed_home.display(),
|
||||||
|
target.transport_path,
|
||||||
|
target.transport_type
|
||||||
|
);
|
||||||
let output = cli_command(
|
let output = cli_command(
|
||||||
&runtime_root,
|
&runtime_root,
|
||||||
&manifest,
|
&manifest,
|
||||||
@@ -1238,25 +1295,42 @@ fn start_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatus,
|
|||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
.output()
|
.output()
|
||||||
.map_err(|error| format!("Failed to launch managed daemon: {error}"))?;
|
.map_err(|error| {
|
||||||
|
log::error!("[daemon] failed to spawn: {error}");
|
||||||
|
format!("Failed to launch managed daemon: {error}")
|
||||||
|
})?;
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
|
let stderr = to_stdio_message(Some(&String::from_utf8_lossy(&output.stderr)));
|
||||||
|
log::error!(
|
||||||
|
"[daemon] start failed (exit {}): {}",
|
||||||
|
output.status.code().unwrap_or(-1),
|
||||||
|
stderr
|
||||||
|
);
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Managed daemon start failed (exit {}): {}",
|
"Managed daemon start failed (exit {}): {}",
|
||||||
output.status.code().unwrap_or(-1),
|
output.status.code().unwrap_or(-1),
|
||||||
to_stdio_message(Some(&String::from_utf8_lossy(&output.stderr)))
|
stderr
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
for _ in 0..30 {
|
log::info!("[daemon] start command succeeded, waiting for daemon to be ready");
|
||||||
|
for attempt in 0..30 {
|
||||||
let daemon_status = managed_daemon_status_internal(app)?;
|
let daemon_status = managed_daemon_status_internal(app)?;
|
||||||
if daemon_status.daemon_running {
|
if daemon_status.daemon_running {
|
||||||
|
log::info!(
|
||||||
|
"[daemon] ready after {} attempts (pid={:?})",
|
||||||
|
attempt + 1,
|
||||||
|
daemon_status.daemon_pid
|
||||||
|
);
|
||||||
return Ok(daemon_status);
|
return Ok(daemon_status);
|
||||||
}
|
}
|
||||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||||
}
|
}
|
||||||
|
log::warn!("[daemon] timed out waiting for daemon to become ready");
|
||||||
managed_daemon_status_internal(app)
|
managed_daemon_status_internal(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatus, String> {
|
fn stop_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatus, String> {
|
||||||
|
log::info!("[daemon] stopping managed daemon");
|
||||||
let status = ensure_runtime_ready_internal(app)?;
|
let status = ensure_runtime_ready_internal(app)?;
|
||||||
let paths = resolve_paths(app)?;
|
let paths = resolve_paths(app)?;
|
||||||
let runtime_root = PathBuf::from(&status.runtime_root);
|
let runtime_root = PathBuf::from(&status.runtime_root);
|
||||||
@@ -1276,24 +1350,40 @@ fn stop_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatus,
|
|||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
.output()
|
.output()
|
||||||
.map_err(|error| format!("Failed to stop managed daemon: {error}"))?;
|
.map_err(|error| {
|
||||||
|
log::error!("[daemon] failed to spawn stop command: {error}");
|
||||||
|
format!("Failed to stop managed daemon: {error}")
|
||||||
|
})?;
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
|
let stderr = to_stdio_message(Some(&String::from_utf8_lossy(&output.stderr)));
|
||||||
|
log::error!(
|
||||||
|
"[daemon] stop failed (exit {}): {}",
|
||||||
|
output.status.code().unwrap_or(-1),
|
||||||
|
stderr
|
||||||
|
);
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Managed daemon stop failed (exit {}): {}",
|
"Managed daemon stop failed (exit {}): {}",
|
||||||
output.status.code().unwrap_or(-1),
|
output.status.code().unwrap_or(-1),
|
||||||
to_stdio_message(Some(&String::from_utf8_lossy(&output.stderr)))
|
stderr
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
log::info!("[daemon] stop command succeeded");
|
||||||
managed_daemon_status_internal(app)
|
managed_daemon_status_internal(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn restart_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatus, String> {
|
fn restart_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatus, String> {
|
||||||
|
log::info!("[daemon] restarting managed daemon");
|
||||||
let status = ensure_runtime_ready_internal(app)?;
|
let status = ensure_runtime_ready_internal(app)?;
|
||||||
let paths = resolve_paths(app)?;
|
let paths = resolve_paths(app)?;
|
||||||
let runtime_root = PathBuf::from(&status.runtime_root);
|
let runtime_root = PathBuf::from(&status.runtime_root);
|
||||||
let manifest = load_runtime_manifest(&runtime_root)?;
|
let manifest = load_runtime_manifest(&runtime_root)?;
|
||||||
let state = read_state_file(&paths.state_file_path);
|
let state = read_state_file(&paths.state_file_path);
|
||||||
let target = managed_transport_target(&paths, state.as_ref())?;
|
let target = managed_transport_target(&paths, state.as_ref())?;
|
||||||
|
log::info!(
|
||||||
|
"[daemon] restart: listen={} (type={})",
|
||||||
|
target.transport_path,
|
||||||
|
target.transport_type
|
||||||
|
);
|
||||||
let output = cli_command(
|
let output = cli_command(
|
||||||
&runtime_root,
|
&runtime_root,
|
||||||
&manifest,
|
&manifest,
|
||||||
@@ -1311,14 +1401,24 @@ fn restart_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatu
|
|||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
.output()
|
.output()
|
||||||
.map_err(|error| format!("Failed to restart managed daemon: {error}"))?;
|
.map_err(|error| {
|
||||||
|
log::error!("[daemon] failed to spawn restart command: {error}");
|
||||||
|
format!("Failed to restart managed daemon: {error}")
|
||||||
|
})?;
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
|
let stderr = to_stdio_message(Some(&String::from_utf8_lossy(&output.stderr)));
|
||||||
|
log::error!(
|
||||||
|
"[daemon] restart failed (exit {}): {}",
|
||||||
|
output.status.code().unwrap_or(-1),
|
||||||
|
stderr
|
||||||
|
);
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Managed daemon restart failed (exit {}): {}",
|
"Managed daemon restart failed (exit {}): {}",
|
||||||
output.status.code().unwrap_or(-1),
|
output.status.code().unwrap_or(-1),
|
||||||
to_stdio_message(Some(&String::from_utf8_lossy(&output.stderr)))
|
stderr
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
log::info!("[daemon] restart command succeeded");
|
||||||
managed_daemon_status_internal(app)
|
managed_daemon_status_internal(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1326,6 +1426,12 @@ fn update_managed_tcp_settings_internal(
|
|||||||
app: &AppHandle,
|
app: &AppHandle,
|
||||||
settings: ManagedTcpSettings,
|
settings: ManagedTcpSettings,
|
||||||
) -> Result<ManagedDaemonStatus, String> {
|
) -> Result<ManagedDaemonStatus, String> {
|
||||||
|
log::info!(
|
||||||
|
"[tcp] updating settings: enabled={} host={} port={}",
|
||||||
|
settings.enabled,
|
||||||
|
settings.host,
|
||||||
|
settings.port
|
||||||
|
);
|
||||||
if settings.enabled {
|
if settings.enabled {
|
||||||
parse_tcp_listen(&format!("{}:{}", settings.host.trim(), settings.port))?;
|
parse_tcp_listen(&format!("{}:{}", settings.host.trim(), settings.port))?;
|
||||||
}
|
}
|
||||||
@@ -1596,6 +1702,12 @@ pub async fn open_local_daemon_transport(
|
|||||||
transport_path: String,
|
transport_path: String,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let session_id = transport_state.alloc_session_id();
|
let session_id = transport_state.alloc_session_id();
|
||||||
|
log::info!(
|
||||||
|
"[transport] opening session {} type={} path={}",
|
||||||
|
session_id,
|
||||||
|
transport_type,
|
||||||
|
transport_path
|
||||||
|
);
|
||||||
let _ = app;
|
let _ = app;
|
||||||
match transport_type.as_str() {
|
match transport_type.as_str() {
|
||||||
"pipe" => {
|
"pipe" => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync, execSync } from "node:child_process";
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
|
import { platform } from "node:os";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { AgentProvider } from "./agent-sdk-types.js";
|
import type { AgentProvider } from "./agent-sdk-types.js";
|
||||||
@@ -88,24 +89,63 @@ export function applyProviderEnv(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isCommandAvailable(command: string): boolean {
|
/**
|
||||||
const normalized = command.trim();
|
* Resolve an executable name to its absolute path the way the user's shell would.
|
||||||
if (!normalized) {
|
*
|
||||||
return false;
|
* On Unix we first try `$SHELL -lic "which <name>"` so that rc-file PATH
|
||||||
|
* additions (asdf, nvm, homebrew, nix, etc.) are visible — exactly as if the
|
||||||
|
* user opened a terminal and typed the command. If that fails (e.g. the login
|
||||||
|
* shell itself errors) we fall back to a plain `which`.
|
||||||
|
*
|
||||||
|
* On Windows the system PATH is always available, so `where.exe` is sufficient.
|
||||||
|
*/
|
||||||
|
export function findExecutable(name: string): string | null {
|
||||||
|
const trimmed = name.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized.includes("/") || normalized.includes("\\")) {
|
if (trimmed.includes("/") || trimmed.includes("\\")) {
|
||||||
return existsSync(normalized);
|
return existsSync(trimmed) ? trimmed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform() === "win32") {
|
||||||
|
try {
|
||||||
|
const out = execSync(`where.exe ${trimmed}`, { encoding: "utf8" }).trim();
|
||||||
|
const firstLine = out.split(/\r?\n/)[0]?.trim();
|
||||||
|
return firstLine || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unix: try the user's login shell so rc-file PATH entries are visible.
|
||||||
|
const shell = process.env["SHELL"];
|
||||||
|
if (shell) {
|
||||||
|
try {
|
||||||
|
const out = execSync(`${shell} -lic "which ${trimmed}"`, {
|
||||||
|
encoding: "utf8",
|
||||||
|
timeout: 5000,
|
||||||
|
}).trim();
|
||||||
|
if (out) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Login shell failed (broken rc, etc.) — fall through to plain which.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const output = execFileSync("which", [normalized], { encoding: "utf8" }).trim();
|
return execFileSync("which", [trimmed], { encoding: "utf8" }).trim() || null;
|
||||||
return output.length > 0;
|
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isCommandAvailable(command: string): boolean {
|
||||||
|
return findExecutable(command) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
export function isProviderCommandAvailable(
|
export function isProviderCommandAvailable(
|
||||||
commandConfig: ProviderCommand | undefined,
|
commandConfig: ProviderCommand | undefined,
|
||||||
resolveDefaultCommand: () => string
|
resolveDefaultCommand: () => string
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ import type {
|
|||||||
} from "../agent-sdk-types.js";
|
} from "../agent-sdk-types.js";
|
||||||
import {
|
import {
|
||||||
applyProviderEnv,
|
applyProviderEnv,
|
||||||
|
findExecutable,
|
||||||
isProviderCommandAvailable,
|
isProviderCommandAvailable,
|
||||||
type ProviderRuntimeSettings,
|
type ProviderRuntimeSettings,
|
||||||
} from "../provider-launch-config.js";
|
} from "../provider-launch-config.js";
|
||||||
@@ -363,16 +364,8 @@ type ClaudeAgentSessionOptions = {
|
|||||||
logger: Logger;
|
logger: Logger;
|
||||||
};
|
};
|
||||||
|
|
||||||
function whichClaude(): string | null {
|
|
||||||
try {
|
|
||||||
return execSync("which claude", { encoding: "utf8" }).trim() || null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveClaudeBinary(): string {
|
function resolveClaudeBinary(): string {
|
||||||
const claudePath = whichClaude();
|
const claudePath = findExecutable("claude");
|
||||||
if (claudePath) {
|
if (claudePath) {
|
||||||
return claudePath;
|
return claudePath;
|
||||||
}
|
}
|
||||||
@@ -1414,7 +1407,7 @@ export class ClaudeAgentClient implements AgentClient {
|
|||||||
this.defaults = options.defaults;
|
this.defaults = options.defaults;
|
||||||
this.logger = options.logger.child({ module: "agent", provider: "claude" });
|
this.logger = options.logger.child({ module: "agent", provider: "claude" });
|
||||||
this.runtimeSettings = options.runtimeSettings;
|
this.runtimeSettings = options.runtimeSettings;
|
||||||
this.claudePath = whichClaude();
|
this.claudePath = findExecutable("claude");
|
||||||
if (this.claudePath) {
|
if (this.claudePath) {
|
||||||
try {
|
try {
|
||||||
const version = execSync(`${this.claudePath} --version`, { encoding: "utf8" }).trim();
|
const version = execSync(`${this.claudePath} --version`, { encoding: "utf8" }).trim();
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
} from "./codex/tool-call-mapper.js";
|
} from "./codex/tool-call-mapper.js";
|
||||||
import {
|
import {
|
||||||
applyProviderEnv,
|
applyProviderEnv,
|
||||||
|
findExecutable,
|
||||||
isProviderCommandAvailable,
|
isProviderCommandAvailable,
|
||||||
resolveProviderCommandPrefix,
|
resolveProviderCommandPrefix,
|
||||||
type ProviderRuntimeSettings,
|
type ProviderRuntimeSettings,
|
||||||
@@ -150,14 +151,10 @@ function mergeCodexConfiguredDefaults(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveCodexBinary(): string {
|
function resolveCodexBinary(): string {
|
||||||
try {
|
const codexPath = findExecutable("codex");
|
||||||
const codexPath = execSync("which codex", { encoding: "utf8" }).trim();
|
|
||||||
if (codexPath) {
|
if (codexPath) {
|
||||||
return codexPath;
|
return codexPath;
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// Fall through to error
|
|
||||||
}
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Codex CLI not found. Please install codex globally: npm install -g @openai/codex"
|
"Codex CLI not found. Please install codex globally: npm install -g @openai/codex"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { execSync, spawn, type ChildProcess } from "node:child_process";
|
import { spawn, type ChildProcess } from "node:child_process";
|
||||||
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2/client";
|
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2/client";
|
||||||
import net from "node:net";
|
import net from "node:net";
|
||||||
import type { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
@@ -29,6 +29,7 @@ import type {
|
|||||||
} from "../agent-sdk-types.js";
|
} from "../agent-sdk-types.js";
|
||||||
import {
|
import {
|
||||||
applyProviderEnv,
|
applyProviderEnv,
|
||||||
|
findExecutable,
|
||||||
isProviderCommandAvailable,
|
isProviderCommandAvailable,
|
||||||
resolveProviderCommandPrefix,
|
resolveProviderCommandPrefix,
|
||||||
type ProviderRuntimeSettings,
|
type ProviderRuntimeSettings,
|
||||||
@@ -150,14 +151,10 @@ const OpencodeToolPartToTimelineItemSchema = OpencodeToolPartTimelineEnvelopeSch
|
|||||||
);
|
);
|
||||||
|
|
||||||
function resolveOpenCodeBinary(): string {
|
function resolveOpenCodeBinary(): string {
|
||||||
try {
|
const opencodePath = findExecutable("opencode");
|
||||||
const opencodePath = execSync("which opencode", { encoding: "utf8" }).trim();
|
|
||||||
if (opencodePath) {
|
if (opencodePath) {
|
||||||
return opencodePath;
|
return opencodePath;
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// fall through
|
|
||||||
}
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"OpenCode CLI not found. Please install opencode globally so Paseo can launch the provider."
|
"OpenCode CLI not found. Please install opencode globally so Paseo can launch the provider."
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user