mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
winreg from main
This commit is contained in:
@@ -200,6 +200,8 @@ jobs:
|
||||
ls -la target/x86_64-pc-windows-gnu/release/
|
||||
"
|
||||
|
||||
sudo chown -R $USER:$USER target/
|
||||
|
||||
# Verify build succeeded
|
||||
if [ ! -f "./target/x86_64-pc-windows-gnu/release/goose.exe" ]; then
|
||||
echo "❌ Windows CLI binary not found."
|
||||
|
||||
Generated
+56
-9
@@ -281,9 +281,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.87"
|
||||
version = "0.1.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97"
|
||||
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2739,7 +2739,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest 0.12.12",
|
||||
"rmcp",
|
||||
"rmcp 0.9.0",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2789,7 +2789,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"paste",
|
||||
"regex",
|
||||
"rmcp",
|
||||
"rmcp 0.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -2828,7 +2828,7 @@ dependencies = [
|
||||
"open",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"rmcp",
|
||||
"rmcp 0.9.0",
|
||||
"rustyline",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2883,7 +2883,7 @@ dependencies = [
|
||||
"rayon",
|
||||
"regex",
|
||||
"reqwest 0.11.27",
|
||||
"rmcp",
|
||||
"rmcp 0.8.5",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2937,7 +2937,7 @@ dependencies = [
|
||||
"http 1.2.0",
|
||||
"rand 0.9.2",
|
||||
"reqwest 0.12.12",
|
||||
"rmcp",
|
||||
"rmcp 0.9.0",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2959,6 +2959,7 @@ dependencies = [
|
||||
"url",
|
||||
"utoipa",
|
||||
"uuid",
|
||||
"winreg 0.55.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5602,7 +5603,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"winreg",
|
||||
"winreg 0.50.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5687,6 +5688,29 @@ version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5947688160b56fb6c827e3c20a72c90392a1d7e9dec74749197aa1780ac42ca"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"futures",
|
||||
"paste",
|
||||
"pin-project-lite",
|
||||
"rmcp-macros 0.8.5",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmcp"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acc36ea743d4bbc97e9f3c33bf0b97765a5cf338de3d9c3d2f321a6e38095615"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"futures",
|
||||
@@ -5696,7 +5720,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"process-wrap",
|
||||
"reqwest 0.12.12",
|
||||
"rmcp-macros",
|
||||
"rmcp-macros 0.9.0",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -5722,6 +5746,19 @@ dependencies = [
|
||||
"syn 2.0.99",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmcp-macros"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "263caba1c96f2941efca0fdcd97b03f42bcde52d2347d05e5d77c93ab18c5b58"
|
||||
dependencies = [
|
||||
"darling 0.21.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_json",
|
||||
"syn 2.0.99",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ron"
|
||||
version = "0.8.1"
|
||||
@@ -8612,6 +8649,16 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.55.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winsafe"
|
||||
version = "0.0.19"
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ uninlined_format_args = "allow"
|
||||
string_slice = "warn"
|
||||
|
||||
[workspace.dependencies]
|
||||
rmcp = { version = "0.8.5", features = ["schemars", "auth"] }
|
||||
rmcp = { version = "0.9.0", features = ["schemars", "auth"] }
|
||||
|
||||
# Patch for Windows cross-compilation issue with crunchy
|
||||
[patch.crates-io]
|
||||
|
||||
@@ -122,6 +122,7 @@ If a Core Maintainer leaves for any reason:
|
||||
|
||||
* The remaining Core Maintainers should appoint a replacement within 30 days.
|
||||
* If Core Maintainer count falls below three, appointment of new Core Maintainers becomes a priority and appointment must happen within 15 days. No major decisions will be made until a new Core Maintainer is appointed.
|
||||
* If there is no qualified developer available who is willing to serve as a Core Maintainer, the remaining Core Maintainer(s) shall instead create and adopt a plan for recruiting and mentoring a new Core Maintainer.
|
||||
* Until a replacement is appointed, remaining Core Maintainers continue governance responsibilities
|
||||
|
||||
## Communication
|
||||
|
||||
@@ -16,7 +16,7 @@ paste = "1.0"
|
||||
ctor = "0.2.7"
|
||||
goose = { path = "../goose" }
|
||||
rmcp = { workspace = true }
|
||||
async-trait = "0.1.86"
|
||||
async-trait = "0.1.89"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
@@ -43,7 +43,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json",
|
||||
tracing-appender = "0.2"
|
||||
once_cell = "1.20.2"
|
||||
shlex = "1.3.0"
|
||||
async-trait = "0.1.86"
|
||||
async-trait = "0.1.89"
|
||||
base64 = "0.22.1"
|
||||
regex = "1.11.1"
|
||||
nix = { version = "0.30.1", features = ["process", "signal"] }
|
||||
|
||||
@@ -33,7 +33,7 @@ reqwest = { version = "0.11", features = [
|
||||
"json",
|
||||
"rustls-tls-native-roots",
|
||||
], default-features = false }
|
||||
async-trait = "0.1"
|
||||
async-trait = "0.1.89"
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
etcetera = "0.8.0"
|
||||
tempfile = "3.8"
|
||||
|
||||
@@ -17,7 +17,7 @@ pub async fn run_mcp_server(name: &str) -> Result<()> {
|
||||
|
||||
tracing::info!("Starting MCP server");
|
||||
|
||||
match name {
|
||||
match name.to_lowercase().replace(' ', "").as_str() {
|
||||
"autovisualiser" => serve_and_wait(AutoVisualiserRouter::new()).await,
|
||||
"computercontroller" => serve_and_wait(ComputerControllerServer::new()).await,
|
||||
"developer" => serve_and_wait(DeveloperServer::new()).await,
|
||||
|
||||
@@ -47,6 +47,9 @@ socket2 = "0.6.1"
|
||||
ed25519-dalek = "2.1"
|
||||
sha2 = "0.10.9"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = { version = "0.55.0" }
|
||||
|
||||
[[bin]]
|
||||
name = "goosed"
|
||||
path = "src/main.rs"
|
||||
@@ -57,5 +60,5 @@ path = "src/bin/generate_schema.rs"
|
||||
|
||||
[dev-dependencies]
|
||||
tower = "0.5"
|
||||
async-trait = "0.1"
|
||||
async-trait = "0.1.89"
|
||||
tempfile = "3.15.0"
|
||||
|
||||
@@ -482,13 +482,45 @@ async fn agent_add_extension(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(request): Json<AddExtensionRequest>,
|
||||
) -> Result<StatusCode, ErrorResponse> {
|
||||
if cfg!(target_os = "windows") {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use winreg::enums::{HKEY_LOCAL_MACHINE, KEY_READ};
|
||||
use winreg::RegKey;
|
||||
|
||||
if let ExtensionConfig::Stdio { cmd, .. } = &request.config {
|
||||
if cmd.ends_with("npx.cmd") || cmd.ends_with("npx") {
|
||||
let node_exists = std::path::Path::new(r"C:\Program Files\nodejs\node.exe")
|
||||
let mut node_exists = std::path::Path::new(r"C:\Program Files\nodejs\node.exe")
|
||||
.exists()
|
||||
|| std::path::Path::new(r"C:\Program Files (x86)\nodejs\node.exe").exists();
|
||||
|
||||
// Also check Windows registry: HKEY_LOCAL_MACHINE\\SOFTWARE\\Node.js InstallPath
|
||||
if !node_exists {
|
||||
// Try 64-bit view first, then 32-bit. Use open_subkey_with_flags to avoid WOW64 redirection issues.
|
||||
let install_path_from_reg: Option<String> = (|| {
|
||||
let hk_local_machine = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
// Common keys to try
|
||||
let keys = vec!["SOFTWARE\\Node.js", "SOFTWARE\\WOW6432Node\\Node.js"];
|
||||
for k in keys.iter() {
|
||||
if let Ok(subkey) = hk_local_machine.open_subkey_with_flags(k, KEY_READ)
|
||||
{
|
||||
if let Ok(val) = subkey.get_value::<String, _>("InstallPath") {
|
||||
if !val.trim().is_empty() {
|
||||
return Some(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})();
|
||||
|
||||
if let Some(path_str) = install_path_from_reg {
|
||||
let node_path = std::path::Path::new(&path_str).join("node.exe");
|
||||
if node_path.exists() {
|
||||
node_exists = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !node_exists {
|
||||
let cmd_path = std::path::Path::new(&cmd);
|
||||
let script_dir = cmd_path
|
||||
@@ -497,7 +529,7 @@ async fn agent_add_extension(
|
||||
let install_script = script_dir.join("install-node.cmd");
|
||||
|
||||
if install_script.exists() {
|
||||
eprintln!("Installing Node.js...");
|
||||
eprintln!("Node.js not found on the system, installing Node.js...");
|
||||
let output = std::process::Command::new(&install_script)
|
||||
.arg("https://nodejs.org/dist/v23.10.0/node-v23.10.0-x64.msi")
|
||||
.output()
|
||||
@@ -513,7 +545,7 @@ async fn agent_add_extension(
|
||||
}
|
||||
} else {
|
||||
return Err(ErrorResponse::internal(format!(
|
||||
"Node.js not detected and no installer script not found at: {}",
|
||||
"Node.js not found on the system, and no installer script found at: {}",
|
||||
install_script.display()
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ serde_urlencoded = "0.7"
|
||||
jsonschema = "0.30.0"
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
regex = "1.11.1"
|
||||
async-trait = "0.1"
|
||||
async-trait = "0.1.89"
|
||||
async-stream = "0.3"
|
||||
minijinja = { version = "2.10.2", features = ["loader"] }
|
||||
include_dir = "0.7.4"
|
||||
|
||||
@@ -674,6 +674,7 @@ impl ExtensionManager {
|
||||
output_schema: tool.output_schema,
|
||||
icons: None,
|
||||
title: None,
|
||||
meta: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
mod persist;
|
||||
|
||||
use axum::extract::{Query, State};
|
||||
use axum::response::Html;
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use minijinja::render;
|
||||
use rmcp::transport::auth::OAuthState;
|
||||
use rmcp::transport::auth::{CredentialStore, OAuthState, StoredCredentials};
|
||||
use rmcp::transport::AuthorizationManager;
|
||||
use serde::Deserialize;
|
||||
use std::net::SocketAddr;
|
||||
@@ -11,9 +13,7 @@ use std::sync::Arc;
|
||||
use tokio::sync::{oneshot, Mutex};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::oauth::persist::{clear_credentials, load_cached_state, save_credentials};
|
||||
|
||||
mod persist;
|
||||
use crate::oauth::persist::GooseCredentialStore;
|
||||
|
||||
const CALLBACK_TEMPLATE: &str = include_str!("oauth_callback.html");
|
||||
|
||||
@@ -32,18 +32,21 @@ pub async fn oauth_flow(
|
||||
mcp_server_url: &String,
|
||||
name: &String,
|
||||
) -> Result<AuthorizationManager, anyhow::Error> {
|
||||
if let Ok(oauth_state) = load_cached_state(mcp_server_url, name).await {
|
||||
if let Some(authorization_manager) = oauth_state.into_authorization_manager() {
|
||||
if authorization_manager.refresh_token().await.is_ok() {
|
||||
return Ok(authorization_manager);
|
||||
}
|
||||
let credential_store = GooseCredentialStore::new(name.clone());
|
||||
let mut auth_manager = AuthorizationManager::new(mcp_server_url).await?;
|
||||
auth_manager.set_credential_store(credential_store.clone());
|
||||
|
||||
if auth_manager.initialize_from_store().await? {
|
||||
if auth_manager.refresh_token().await.is_ok() {
|
||||
return Ok(auth_manager);
|
||||
}
|
||||
|
||||
if let Err(e) = clear_credentials(name) {
|
||||
if let Err(e) = credential_store.clear().await {
|
||||
warn!("error clearing bad credentials: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// No existing credentials or they were invalid - need to do the full oauth flow
|
||||
let (code_sender, code_receiver) = oneshot::channel::<CallbackParams>();
|
||||
let app_state = AppState {
|
||||
code_receiver: Arc::new(Mutex::new(Some(code_sender))),
|
||||
@@ -74,6 +77,7 @@ pub async fn oauth_flow(
|
||||
});
|
||||
|
||||
let mut oauth_state = OAuthState::new(mcp_server_url, None).await?;
|
||||
|
||||
let redirect_uri = format!("http://localhost:{}/oauth_callback", used_addr.port());
|
||||
oauth_state
|
||||
.start_authorization(&[], redirect_uri.as_str(), Some("goose"))
|
||||
@@ -91,13 +95,20 @@ pub async fn oauth_flow(
|
||||
} = code_receiver.await?;
|
||||
oauth_state.handle_callback(&auth_code, &csrf_token).await?;
|
||||
|
||||
if let Err(e) = save_credentials(name, &oauth_state).await {
|
||||
warn!("Failed to save credentials: {}", e);
|
||||
}
|
||||
let (client_id, token_response) = oauth_state.get_credentials().await?;
|
||||
|
||||
let auth_manager = oauth_state
|
||||
let mut auth_manager = oauth_state
|
||||
.into_authorization_manager()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get authorization manager"))?;
|
||||
|
||||
credential_store
|
||||
.save(StoredCredentials {
|
||||
client_id,
|
||||
token_response,
|
||||
})
|
||||
.await?;
|
||||
|
||||
auth_manager.set_credential_store(credential_store);
|
||||
|
||||
Ok(auth_manager)
|
||||
}
|
||||
|
||||
@@ -1,71 +1,54 @@
|
||||
use oauth2::{basic::BasicTokenType, EmptyExtraTokenFields, StandardTokenResponse};
|
||||
use reqwest::IntoUrl;
|
||||
use rmcp::transport::{auth::OAuthState, AuthError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use rmcp::transport::auth::{AuthError, CredentialStore, StoredCredentials};
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SerializableCredentials {
|
||||
pub client_id: String,
|
||||
pub token_response: Option<StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>>,
|
||||
/// Goose-specific credential store that uses the Config system
|
||||
///
|
||||
/// This implementation stores OAuth credentials in the goose configuration
|
||||
/// system, which handles secure storage (e.g., keychain integration).
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GooseCredentialStore {
|
||||
name: String,
|
||||
}
|
||||
|
||||
fn secret_key(name: &str) -> String {
|
||||
format!("oauth_creds_{name}")
|
||||
}
|
||||
impl GooseCredentialStore {
|
||||
pub fn new(name: String) -> Self {
|
||||
Self { name }
|
||||
}
|
||||
|
||||
pub async fn save_credentials(
|
||||
name: &str,
|
||||
oauth_state: &OAuthState,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = Config::global();
|
||||
let (client_id, token_response) = oauth_state.get_credentials().await?;
|
||||
|
||||
let credentials = SerializableCredentials {
|
||||
client_id,
|
||||
token_response,
|
||||
};
|
||||
|
||||
let key = secret_key(name);
|
||||
config.set_secret(&key, &credentials)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_credentials(
|
||||
name: &str,
|
||||
) -> Result<SerializableCredentials, Box<dyn std::error::Error>> {
|
||||
let config = Config::global();
|
||||
let key = secret_key(name);
|
||||
let credentials: SerializableCredentials = config.get_secret(&key)?;
|
||||
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
pub fn clear_credentials(name: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = Config::global();
|
||||
|
||||
Ok(config.delete_secret(&secret_key(name))?)
|
||||
}
|
||||
|
||||
pub async fn load_cached_state<U: IntoUrl>(
|
||||
base_url: U,
|
||||
name: &str,
|
||||
) -> Result<OAuthState, AuthError> {
|
||||
let credentials = load_credentials(name)
|
||||
.await
|
||||
.map_err(|e| AuthError::InternalError(format!("Failed to load credentials: {}", e)))?;
|
||||
|
||||
if let Some(token_response) = credentials.token_response {
|
||||
let mut oauth_state = OAuthState::new(base_url, None).await?;
|
||||
oauth_state
|
||||
.set_credentials(&credentials.client_id, token_response)
|
||||
.await?;
|
||||
Ok(oauth_state)
|
||||
} else {
|
||||
Err(AuthError::InternalError(
|
||||
"No token response in cached credentials".to_string(),
|
||||
))
|
||||
fn secret_key(&self) -> String {
|
||||
format!("oauth_creds_{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl CredentialStore for GooseCredentialStore {
|
||||
async fn load(&self) -> Result<Option<StoredCredentials>, AuthError> {
|
||||
let config = Config::global();
|
||||
let key = self.secret_key();
|
||||
|
||||
match config.get_secret::<StoredCredentials>(&key) {
|
||||
Ok(credentials) => Ok(Some(credentials)),
|
||||
Err(_) => Ok(None), // No credentials found
|
||||
}
|
||||
}
|
||||
|
||||
async fn save(&self, credentials: StoredCredentials) -> Result<(), AuthError> {
|
||||
let config = Config::global();
|
||||
let key = self.secret_key();
|
||||
|
||||
config
|
||||
.set_secret(&key, &credentials)
|
||||
.map_err(|e| AuthError::InternalError(format!("Failed to save credentials: {}", e)))
|
||||
}
|
||||
|
||||
async fn clear(&self) -> Result<(), AuthError> {
|
||||
let config = Config::global();
|
||||
let key = self.secret_key();
|
||||
|
||||
config
|
||||
.delete_secret(&key)
|
||||
.map_err(|e| AuthError::InternalError(format!("Failed to clear credentials: {}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
---
|
||||
title: How to Successfully Migrate Your App with an AI Agent
|
||||
description: A step-by-step prompt strategy for AI-assisted code migration with real examples from a Next.js refactor
|
||||
authors:
|
||||
- rizel
|
||||
---
|
||||
|
||||

|
||||
|
||||
"Migrate my app from x language to y language." You hit enter, watch your AI agent spin its wheels, and eventually every success story you've heard feels like a carefully orchestrated lie.
|
||||
|
||||
Most failures have less to do with the agent's capability and more to do with poor prompt and context strategy. Think about it: if someone dropped you into a complex, unfamiliar codebase and said "migrate this," you'd be lost without a plan. You'd need to explore the code, ask questions about its structure, and break the work into manageable steps.
|
||||
|
||||
Your AI agent needs the same approach: guided exploration, strategic questions, and decomposed tasks.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
I recently put this approach into practice with [goose](/), migrating a legacy LLM credit provisioning system split across two repositories (React/Vite frontend and Node/Express backend) into a unified Next.js framework.
|
||||
|
||||
## Why I Needed to Refactor
|
||||
|
||||
I originally built the app to distribute LLM API credits at a Boston meetup. It was a quick prototype that experienced unexpected adoption, exposing fundamental architectural problems. (And I have shiny toy syndrome, so I struggled to return to the app to improve it). I wanted to make the following improvements:
|
||||
|
||||
- Email-based provisioning
|
||||
- Dynamic credit allocation per event
|
||||
- Analytics infrastructure
|
||||
- Admin panel
|
||||
|
||||
I hopped on a livestream to tackle this huge refactor but seconds in I realized I realistically could not do this all in one hour. I focused on consolidating the fragmented codebase first. Two repositories (React/Vite frontend, Express backend) needed to become one monolithic Next.js application. But simply telling goose "Convert to Next.js" wouldn't work without proper context building.
|
||||
|
||||
## My Prompt Strategy
|
||||
|
||||
### Building a Shared Mental Model
|
||||
|
||||
Before I instructed goose to write any code, I prioritized helping it understand the codebase systematically with the following prompt:
|
||||
|
||||
> Can you get a lay of the land for the two projects found here and how they communicate?
|
||||
|
||||
goose employed the [analyze tool](/docs/mcp/developer-mcp#developer-extension-tools) to generate a high-level architectural flow. (The analyze tool is part of the [Developer extension](/docs/mcp/developer-mcp), an [MCP server](https://modelcontextprotocol.io/docs/learn/server-concepts) that's built into goose).
|
||||
|
||||
```
|
||||
User Browser
|
||||
↓
|
||||
[goose-access-gateway] (React SPA)
|
||||
↓ (HTTPS REST API)
|
||||
[goose-hacknight-backend] (Express API)
|
||||
↓ (HTTPS REST API)
|
||||
[OpenRouter API] (Third-party service)
|
||||
```
|
||||
|
||||
It also shared all the various endpoints and how to run the repos. This mapping served dual purposes: establishing the agent's contextual foundation and refreshing my own mental model of an eight-month-old implementation.
|
||||
|
||||
### Defining the Scope
|
||||
|
||||
With the landscape mapped, I needed to prevent scope creep. I deliberately focused the agent's attention on the frontend to avoid chaotic, uncontrolled changes across the entire codebase.
|
||||
|
||||
> Tell me the commands to run the frontend project.
|
||||
|
||||
Yes, I could have found the commands in the package.json, but asking goose to do it served a purpose: it grounded goose in the actual project setup and prevented it from hallucinating commands or ports.
|
||||
|
||||
:::tip Pro tip
|
||||
I always have goose tell me what commands to run (like npm run dev) rather than executing them itself. Long-running or blocking commands can halt goose's process.
|
||||
:::
|
||||
|
||||
### Verification Driven Development
|
||||
|
||||
One major pitfall of AI-assisted coding is that agents cannot validate their code beyond syntactic correctness.
|
||||
|
||||
To address this, I enabled the [Chrome Dev Tools extension](/docs/mcp/chrome-devtools-mcp), granting the agent browser-level inspection capabilities: DOM manipulation verification, CSS property validation, and performance profiling. This extension gave goose "eyes", which meant I could give it my most ambitious prompt yet:
|
||||
|
||||
> I have the frontend running right now on localhost:8080. I want to take this UI design and start from scratch a bit. I need all new logic especially for the backend. Can we create a new directory and create a Next.js project and for now let's just do the frontend, but don't add any of the API calls or anything. We just want to retain the design of the frontend page. Please recreate that. Use the Chrome Dev Tools extension to see how the UI looks so you can copy it and use the to do extension to help you plan. If there are interactive commands or you can run an install or something like that just tell me to do it...and give me the details of what I need to run.
|
||||
|
||||
This was a huge prompt, so let's break down what each part accomplished:
|
||||
|
||||
- **Isolation:** create a new directory
|
||||
- **Scope:** just do the frontend, but don't add any of the API calls
|
||||
- **Verification:** Use the Chrome Dev Tools extension to see how the UI looks
|
||||
- **Planning:** use the to do extension to help you plan
|
||||
- **Interaction:** just tell me to do it...and give me the details of what I need to run
|
||||
|
||||
:::note
|
||||
In retrospect, the instruction regarding blocking commands should have been codified in [persistent context files](/docs/guides/using-goosehints) ([AGENTS.md](https://agents.md/) or [goosehints](/docs/guides/using-goosehints)) rather than inline prompts.
|
||||
:::
|
||||
|
||||
But, I was so happy that goose generated a pixel perfect recreation of the app.
|
||||
|
||||
### Task Decomposition
|
||||
|
||||
The agent's successful, perfect recreation of the UI was largely due to the [Todo extension](/docs/mcp/todo-mcp), an MCP server that's built into goose. I find that this extension helps prevent scope drift, where agents autonomously expand into adjacent functionality after completing an objective.
|
||||
|
||||
The to do list included items like:
|
||||
|
||||
- Copy logo assets from old project
|
||||
- Create glass-morphism card component
|
||||
- Add logo with fade in animation
|
||||
- Verify theme toggle works
|
||||
|
||||
When I ran the app locally, I did encounter a Tailwind CSS v4/v3 syntax error, but goose used the Chrome Dev Tools extension and the Todo extension to quickly fix it.
|
||||
|
||||
### Automated Version Control
|
||||
|
||||
Because my UI was pixel-perfect, I felt confident enough to introduce some backend logic, but I knew introducing this level of complexity would require granular version control. When an agent makes a dozen changes, it's easy to end up with unwanted code buried in the history. Manually tracking and reverting these changes is tedious.
|
||||
|
||||
To solve this problem, I instituted an automated commit policy by adding a persistent directive to my .goosehints file:
|
||||
|
||||
> Every time you make a change, make a commit using the GitHub CLI or the GitHub MCP Server.
|
||||
|
||||
### Pattern Replication
|
||||
|
||||
The final step was to add the backend logic for emailing API keys. Instead of asking goose to invent this from scratch, I had it learn from a known-working system: a separate app with similar provisioning logic.
|
||||
|
||||
I gave goose the following prompt:
|
||||
|
||||
> There's a recipe cookbook. To submit people have to open up a PR and then it sends them an email with an API key. Are you able to find the logic where it sends the API key?
|
||||
|
||||
Once it analyzed that code, I gave the final instruction:
|
||||
|
||||
> Use what you learned from the recipe project logic to make this happen in goose-credits... send the API key to their email using the SendGrid API.
|
||||
|
||||
This "copy-and-adapt" strategy was incredibly effective. goose successfully implemented the necessary API routes and clearly identified the environment variables I needed to supply. I manually added those variables. I didn't give them to goose for security purposes.
|
||||
|
||||
## The Lesson
|
||||
|
||||
I shared my messy, tedious conversation with goose (using Claude Sonnet 4.5) so that readers can confidently ditch one-shot prompts for complex tasks and work incrementally with agents. Just like coding, collaborating with an agent requires patience, but it doesn't have to feel stressful.
|
||||
|
||||
I hope this clarifies how to converse with an agent and accomplish complex tasks like migrations. If you want to see this in action, you're in luck; below is a VOD livestream of me navigating the project in real-time.
|
||||
|
||||
<iframe class="aspect-ratio" src="https://www.youtube.com/embed/zGyXfA3kKTk" title="How to Successfully Migrate Your App with an AI Agent" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
*Ready to try AI-assisted migration with goose? Get started with our [quickstart guide](/docs/quickstart) and share your experience in our [Discord community](http://discord.gg/goose-oss).*
|
||||
|
||||
|
||||
<head>
|
||||
<meta property="og:title" content="How to Successfully Migrate Your App with an AI Agent" />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content="https://block.github.io/goose/blog/2025/11/17/migrate-app-with-ai-agent" />
|
||||
<meta property="og:description" content="A step-by-step prompt strategy for AI-assisted code migration with real examples from a Next.js refactor" />
|
||||
<meta property="og:image" content="https://block.github.io/goose/assets/images/migrate-app-ai-agent-e8e3dcddf74909b6f84f85c8c776aaed.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:domain" content="block.github.io/goose" />
|
||||
<meta name="twitter:title" content="How to Successfully Migrate Your App with an AI Agent" />
|
||||
<meta name="twitter:description" content="A step-by-step prompt strategy for AI-assisted code migration with real examples from a Next.js refactor" />
|
||||
<meta name="twitter:image" content="https://block.github.io/goose/assets/images/migrate-app-ai-agent-e8e3dcddf74909b6f84f85c8c776aaed.png"/>
|
||||
</head>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
@@ -49,15 +49,25 @@ In your first session, goose prompts you to [set up an LLM (Large Language Model
|
||||
On macOS, you can drag and drop a folder onto the goose icon in the dock to open a new session in that directory.
|
||||
:::
|
||||
|
||||
You can also use keyboard shortcuts to start a new session or bring focus to open goose windows.
|
||||
You can also use keyboard shortcuts to start a new session or manage goose windows.
|
||||
|
||||
| Action | macOS | Windows/Linux |
|
||||
|--------|-------|---------------|
|
||||
| New Session in Current Directory | `Cmd+N` | `Ctrl+N` |
|
||||
| New Session in Different Directory | `Cmd+O` | `Ctrl+O` |
|
||||
| Focus goose Window | `Cmd+Option+Shift+G` | `Ctrl+Alt+Shift+G` |
|
||||
| New Session with [Quick Launcher](#quick-launcher) | `Cmd+Option+Shift+G` | `Ctrl+Alt+Shift+G` |
|
||||
| New Session in Current Directory | `Cmd+N` | `Ctrl+N` |
|
||||
| New Session in Current Directory (Same Window) | `Cmd+T` | `Ctrl+T` |
|
||||
| New Session in Different Directory | `Cmd+O` | `Ctrl+O` |
|
||||
| Toggle Sidebar | `Cmd+B` | `Ctrl+B` |
|
||||
| Open Settings | `Cmd+,` | `Ctrl+,` |
|
||||
| Keep goose Window Always on Top | `Cmd+Shift+T` | `Ctrl+Shift+T` |
|
||||
|
||||
<br />
|
||||
#### Quick Launcher
|
||||
Start a new session by typing your prompt into a popup:
|
||||
1. Press `Cmd+Option+Shift+G` (macOS) or `Ctrl+Alt+Shift+G` (Windows/Linux) to open the popup
|
||||
2. Type your prompt and press `Enter`
|
||||
|
||||
The session opens to your most recently opened directory in a new goose window.
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="goose CLI">
|
||||
|
||||
@@ -1,42 +1,45 @@
|
||||
---
|
||||
title: Quick Goose Tips
|
||||
title: Quick goose Tips
|
||||
sidebar_position: 30
|
||||
sidebar_label: Quick Tips
|
||||
description: Best practices for working with Goose
|
||||
description: Best practices for working with goose
|
||||
---
|
||||
|
||||
### Goose works on your behalf
|
||||
Goose is an AI agent, which means you can prompt Goose to perform tasks for you like opening applications, running shell commands, automating workflows, writing code, browsing the web, and more.
|
||||
### goose works on your behalf
|
||||
goose is an AI agent, which means you can prompt goose to perform tasks for you like opening applications, running shell commands, automating workflows, writing code, browsing the web, and more.
|
||||
|
||||
### Prompt Goose using natural language
|
||||
You don't need fancy language or special syntax to prompt Goose. Talk with Goose like you would talk to a friend. You can even use slang or say please and thank you; Goose will understand.
|
||||
### Prompt goose using natural language
|
||||
You don't need fancy language or special syntax to prompt goose. Talk with goose like you would talk to a friend. You can even use slang or say please and thank you; goose will understand.
|
||||
|
||||
### Extend Goose's capabilities to any application
|
||||
Goose's capabilities are extensible. As an [MCP](https://modelcontextprotocol.io/) client, Goose can connect to your apps and services through [extensions](/extensions), allowing it to work across your entire workflow.
|
||||
### Extend goose's capabilities to any application
|
||||
goose's capabilities are extensible. As an [MCP](https://modelcontextprotocol.io/) client, goose can connect to your apps and services through [extensions](/extensions), allowing it to work across your entire workflow.
|
||||
|
||||
### Choose how much control Goose has
|
||||
You can customize how much [supervision](/docs/guides/goose-permissions) Goose needs. Choose between full autonomy, requiring approval before actions, or simply chatting without any actions.
|
||||
### Choose how much control goose has
|
||||
You can customize how much [supervision](/docs/guides/goose-permissions) goose needs. Choose between full autonomy, requiring approval before actions, or simply chatting without any actions.
|
||||
|
||||
### Choose the right LLM
|
||||
Your experience with Goose is shaped by your [choice of LLM](/blog/2025/03/31/goose-benchmark), as it handles all the planning while Goose manages the execution. When choosing an LLM, consider its tool support, specific capabilities, and associated costs.
|
||||
Your experience with goose is shaped by your [choice of LLM](/blog/2025/03/31/goose-benchmark), as it handles all the planning while goose manages the execution. When choosing an LLM, consider its tool support, specific capabilities, and associated costs.
|
||||
|
||||
### Keep sessions short
|
||||
LLMs have context windows, which are limits on how much conversation history they can retain. Once exceeded, they may forget earlier parts of the conversation. Monitor your token usage and [start new sessions](/docs/guides/sessions/session-management) as needed.
|
||||
|
||||
### Use Quick Launcher for faster session starts
|
||||
Press `Cmd+Option+Shift+G` (macOS) or `Ctrl+Alt+Shift+G` (Windows/Linux) and send a prompt to start a new session instantly.
|
||||
|
||||
### Turn off unnecessary extensions or tool
|
||||
Turning on too many extensions can degrade performance. Enable only essential [extensions and tools](/docs/guides/managing-tools/tool-permissions) to improve tool selection accuracy, save context window space, and stay within provider tool limits.
|
||||
|
||||
### Teach Goose your preferences
|
||||
Help Goose remember how you like to work by using [`.goosehints` or other context files](/docs/guides/using-goosehints/) for permanent project preferences and the [Memory extension](/docs/mcp/memory-mcp) for things you want Goose to dynamically recall later. Both can help save valuable context window space while keeping your preferences available.
|
||||
### Teach goose your preferences
|
||||
Help goose remember how you like to work by using [`.goosehints` or other context files](/docs/guides/using-goosehints/) for permanent project preferences and the [Memory extension](/docs/mcp/memory-mcp) for things you want goose to dynamically recall later. Both can help save valuable context window space while keeping your preferences available.
|
||||
|
||||
### Protect sensitive files
|
||||
Goose is often eager to make changes. You can stop it from changing specific files by creating a [.gooseignore](/docs/guides/using-gooseignore) file. In this file, you can list all the file paths you want it to avoid.
|
||||
goose is often eager to make changes. You can stop it from changing specific files by creating a [.gooseignore](/docs/guides/using-gooseignore) file. In this file, you can list all the file paths you want it to avoid.
|
||||
|
||||
### Version Control
|
||||
Commit your code changes early and often. This allows you to rollback any unexpected changes.
|
||||
|
||||
### Control which extensions Goose can use
|
||||
Administrators can use an [allowlist](/docs/guides/allowlist) to restrict Goose to approved extensions only. This helps prevent risky installs from unknown MCP servers.
|
||||
### Control which extensions goose can use
|
||||
Administrators can use an [allowlist](/docs/guides/allowlist) to restrict goose to approved extensions only. This helps prevent risky installs from unknown MCP servers.
|
||||
|
||||
### Set up starter templates
|
||||
You can turn a successful session into a reusable "[recipe](/docs/guides/recipes/session-recipes)" to share with others or use again later—no need to start from scratch.
|
||||
@@ -44,8 +47,8 @@ You can turn a successful session into a reusable "[recipe](/docs/guides/recipes
|
||||
### Embrace an experimental mindset
|
||||
You don’t need to get it right the first time. Iterating on prompts and tools is part of the workflow.
|
||||
|
||||
### Keep Goose updated
|
||||
Regularly [update](/docs/guides/updating-goose) Goose to benefit from the latest features, bug fixes, and performance improvements.
|
||||
### Keep goose updated
|
||||
Regularly [update](/docs/guides/updating-goose) goose to benefit from the latest features, bug fixes, and performance improvements.
|
||||
|
||||
### Pair Two Models to Save Money
|
||||
Use [lead/worker model](/docs/tutorials/lead-worker/) to have goose use a "lead" model for early planning before handing the task to a lower-cost "worker" model for execution.
|
||||
|
||||
@@ -4729,6 +4729,10 @@
|
||||
"inputSchema"
|
||||
],
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"annotations": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -810,6 +810,9 @@ export type TokenState = {
|
||||
};
|
||||
|
||||
export type Tool = {
|
||||
_meta?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
annotations?: ToolAnnotations | {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
@@ -23,6 +23,18 @@ trap 'log "An error occurred. Exiting with status $?."' ERR
|
||||
|
||||
log "Starting node setup (common)."
|
||||
|
||||
# One-time cleanup for existing Linux users to fix locking issues
|
||||
CLEANUP_MARKER="~/.config/goose/.mcp-hermit-cleanup-v1"
|
||||
if [[ "$(uname -s)" == "Linux" ]] && [ ! -f "$CLEANUP_MARKER" ]; then
|
||||
log "Performing one-time cleanup of old mcp-hermit directory to fix locking issues."
|
||||
if [ -d ~/.config/goose/mcp-hermit ]; then
|
||||
rm -rf ~/.config/goose/mcp-hermit
|
||||
log "Removed old mcp-hermit directory."
|
||||
fi
|
||||
touch "$CLEANUP_MARKER"
|
||||
log "Cleanup completed. Marker file created."
|
||||
fi
|
||||
|
||||
# Ensure ~/.config/goose/mcp-hermit/bin exists
|
||||
log "Creating directory ~/.config/goose/mcp-hermit/bin if it does not exist."
|
||||
mkdir -p ~/.config/goose/mcp-hermit/bin
|
||||
|
||||
@@ -40,14 +40,6 @@ export default function ChatSettingsSection() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="pb-2 rounded-lg">
|
||||
<CardHeader className="pb-0">
|
||||
<CardTitle className="">Scheduling Engine</CardTitle>
|
||||
<CardDescription>
|
||||
Choose which scheduling backend to use for scheduled recipes and tasks
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card className="pb-2 rounded-lg">
|
||||
<CardHeader className="pb-0">
|
||||
<CardTitle className="">Tool Selection Strategy (preview)</CardTitle>
|
||||
|
||||
@@ -12,16 +12,6 @@ import {
|
||||
} from './utils';
|
||||
import type { FixedExtensionEntry } from '../../ConfigContext';
|
||||
|
||||
// Mock window.electron
|
||||
const mockElectron = {
|
||||
getBinaryPath: vi.fn(),
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'electron', {
|
||||
value: mockElectron,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
describe('Extension Utils', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -46,7 +46,7 @@ const ScrollArea = React.forwardRef<ScrollAreaHandle, ScrollAreaProps>(
|
||||
const isActivelyScrollingRef = React.useRef(false);
|
||||
const scrollTimeoutRef = React.useRef<number | null>(null);
|
||||
|
||||
const BOTTOM_SCROLL_THRESHOLD = 100;
|
||||
const BOTTOM_SCROLL_THRESHOLD = 200;
|
||||
|
||||
const isAtBottom = React.useCallback(() => {
|
||||
if (!viewportRef.current) return false;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Electron from 'electron';
|
||||
import fs from 'node:fs';
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { createServer } from 'net';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { getBinaryPath } from './utils/pathUtils';
|
||||
import log from './utils/logger';
|
||||
import { App } from 'electron';
|
||||
import { Buffer } from 'node:buffer';
|
||||
@@ -27,7 +28,7 @@ export const findAvailablePort = (): Promise<number> => {
|
||||
// Check if goosed server is ready by polling the status endpoint
|
||||
export const checkServerStatus = async (client: Client, errorLog: string[]): Promise<boolean> => {
|
||||
const interval = 100; // ms
|
||||
const maxAttempts = 30; // 3s
|
||||
const maxAttempts = 100; // 10s
|
||||
|
||||
const fatal = (line: string) => {
|
||||
const trimmed = line.trim().toLowerCase();
|
||||
@@ -94,7 +95,7 @@ export const startGoosed = async (
|
||||
return connectToExternalBackend(dir, 3000);
|
||||
}
|
||||
|
||||
let goosedPath = getBinaryPath(app, 'goosed');
|
||||
let goosedPath = getGoosedBinaryPath(app);
|
||||
|
||||
const resolvedGoosedPath = path.resolve(goosedPath);
|
||||
|
||||
@@ -215,3 +216,42 @@ export const startGoosed = async (
|
||||
log.info(`Goosed server successfully started on port ${port}`);
|
||||
return [port, dir, goosedProcess, stderrLines];
|
||||
};
|
||||
|
||||
const getGoosedBinaryPath = (app: Electron.App): string => {
|
||||
let executableName = process.platform === 'win32' ? 'goosed.exe' : 'goosed';
|
||||
|
||||
let possiblePaths: string[];
|
||||
if (!app.isPackaged) {
|
||||
possiblePaths = [
|
||||
path.join(process.cwd(), 'src', 'bin', executableName),
|
||||
path.join(process.cwd(), 'bin', executableName),
|
||||
path.join(process.cwd(), '..', '..', 'target', 'debug', executableName),
|
||||
path.join(process.cwd(), '..', '..', 'target', 'release', executableName),
|
||||
];
|
||||
} else {
|
||||
possiblePaths = [path.join(process.resourcesPath, 'bin', executableName)];
|
||||
}
|
||||
|
||||
for (const binPath of possiblePaths) {
|
||||
try {
|
||||
const resolvedPath = path.resolve(binPath);
|
||||
|
||||
if (fs.existsSync(resolvedPath)) {
|
||||
const stats = fs.statSync(resolvedPath);
|
||||
if (stats.isFile()) {
|
||||
return resolvedPath;
|
||||
} else {
|
||||
log.error(`Path exists but is not a regular file: ${resolvedPath}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Error checking path ${binPath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Could not find ${executableName} binary in any of the expected locations: ${possiblePaths.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ import os from 'node:os';
|
||||
import { spawn } from 'child_process';
|
||||
import 'dotenv/config';
|
||||
import { checkServerStatus, startGoosed } from './goosed';
|
||||
import { expandTilde, getBinaryPath } from './utils/pathUtils';
|
||||
import { expandTilde } from './utils/pathUtils';
|
||||
import log from './utils/logger';
|
||||
import { ensureWinShims } from './utils/winShims';
|
||||
import { addRecentDir, loadRecentDirs } from './utils/recentDirs';
|
||||
@@ -1599,11 +1599,6 @@ ipcMain.handle('check-ollama', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Handle binary path requests
|
||||
ipcMain.handle('get-binary-path', (_event, binaryName) => {
|
||||
return getBinaryPath(app, binaryName);
|
||||
});
|
||||
|
||||
ipcMain.handle('read-file', async (_event, filePath) => {
|
||||
try {
|
||||
const expandedPath = expandTilde(filePath);
|
||||
|
||||
@@ -66,7 +66,6 @@ type ElectronAPI = {
|
||||
selectFileOrDirectory: (defaultPath?: string) => Promise<string | null>;
|
||||
startPowerSaveBlocker: () => Promise<number>;
|
||||
stopPowerSaveBlocker: () => Promise<void>;
|
||||
getBinaryPath: (binaryName: string) => Promise<string>;
|
||||
readFile: (directory: string) => Promise<FileResponse>;
|
||||
writeFile: (directory: string, content: string) => Promise<boolean>;
|
||||
ensureDirectory: (dirPath: string) => Promise<boolean>;
|
||||
@@ -169,7 +168,6 @@ const electronAPI: ElectronAPI = {
|
||||
ipcRenderer.invoke('select-file-or-directory', defaultPath),
|
||||
startPowerSaveBlocker: () => ipcRenderer.invoke('start-power-save-blocker'),
|
||||
stopPowerSaveBlocker: () => ipcRenderer.invoke('stop-power-save-blocker'),
|
||||
getBinaryPath: (binaryName: string) => ipcRenderer.invoke('get-binary-path', binaryName),
|
||||
readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath),
|
||||
writeFile: (filePath: string, content: string) =>
|
||||
ipcRenderer.invoke('write-file', filePath, content),
|
||||
|
||||
@@ -469,6 +469,10 @@ function sendStatusToWindow(event: string, data?: unknown) {
|
||||
function updateTrayIcon(hasUpdate: boolean) {
|
||||
if (!trayRef) return;
|
||||
|
||||
if (process.env.GOOSE_VERSION) {
|
||||
hasUpdate = false;
|
||||
}
|
||||
|
||||
const isDev = !app.isPackaged;
|
||||
let iconPath: string;
|
||||
|
||||
|
||||
@@ -1,97 +1,5 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import Electron from 'electron';
|
||||
import log from './logger';
|
||||
|
||||
export const getBinaryPath = (app: Electron.App, binaryName: string): string => {
|
||||
// On Windows, rely on PATH we just patched in ensureWinShims for command-line tools
|
||||
// but use explicit resources/bin path for goosed.exe
|
||||
if (process.platform === 'win32') {
|
||||
// For goosed.exe, always use the explicit resources/bin path
|
||||
if (binaryName === 'goosed') {
|
||||
return path.join(process.resourcesPath, 'bin', 'goosed.exe');
|
||||
}
|
||||
|
||||
// Map binary names to their Windows equivalents
|
||||
const windowsBinaryMap: Record<string, string> = {
|
||||
npx: 'npx.cmd',
|
||||
uvx: 'uvx.exe',
|
||||
};
|
||||
|
||||
// For other binaries, use Windows-specific extensions if available
|
||||
const windowsBinary = windowsBinaryMap[binaryName] || binaryName;
|
||||
return windowsBinary;
|
||||
}
|
||||
|
||||
// For non-Windows platforms, use the original logic
|
||||
const possiblePaths: string[] = [];
|
||||
addPaths(false, possiblePaths, binaryName, app);
|
||||
|
||||
for (const binPath of possiblePaths) {
|
||||
try {
|
||||
// Security: Resolve the path and validate it's within expected directories
|
||||
const resolvedPath = path.resolve(binPath);
|
||||
|
||||
// Ensure the resolved path doesn't contain suspicious sequences
|
||||
if (
|
||||
resolvedPath.includes('..') ||
|
||||
resolvedPath.includes(';') ||
|
||||
resolvedPath.includes('|') ||
|
||||
resolvedPath.includes('&')
|
||||
) {
|
||||
log.error(`Suspicious path detected, skipping: ${resolvedPath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fs.existsSync(resolvedPath)) {
|
||||
// Additional security check: ensure it's a regular file
|
||||
const stats = fs.statSync(resolvedPath);
|
||||
if (stats.isFile()) {
|
||||
return resolvedPath;
|
||||
} else {
|
||||
log.error(`Path exists but is not a regular file: ${resolvedPath}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Error checking path ${binPath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Could not find ${binaryName} binary in any of the expected locations: ${possiblePaths.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
};
|
||||
|
||||
const addPaths = (
|
||||
isWindows: boolean,
|
||||
possiblePaths: string[],
|
||||
executableName: string,
|
||||
app: Electron.App
|
||||
): void => {
|
||||
if (!app.isPackaged) {
|
||||
possiblePaths.push(
|
||||
path.join(process.cwd(), 'src', 'bin', executableName),
|
||||
path.join(process.cwd(), 'bin', executableName),
|
||||
path.join(process.cwd(), '..', '..', 'target', 'release', executableName)
|
||||
);
|
||||
} else {
|
||||
possiblePaths.push(
|
||||
path.join(process.resourcesPath, 'bin', executableName),
|
||||
path.join(app.getAppPath(), 'resources', 'bin', executableName)
|
||||
);
|
||||
|
||||
if (isWindows) {
|
||||
possiblePaths.push(
|
||||
path.join(process.resourcesPath, executableName),
|
||||
path.join(app.getAppPath(), 'resources', executableName),
|
||||
path.join(app.getPath('exe'), '..', 'bin', executableName)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Expands tilde (~) to the user's home directory
|
||||
|
||||
Reference in New Issue
Block a user