diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index 692a704d..ae8bc4d5 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -12,6 +12,7 @@ use utoipa::OpenApi; #[derive(OpenApi)] #[openapi( paths( + super::routes::config_management::init_config, super::routes::config_management::upsert_config, super::routes::config_management::remove_config, super::routes::config_management::read_config, diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index 0313be74..7d529603 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -14,6 +14,7 @@ use goose::providers::providers as get_providers; use http::{HeaderMap, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use serde_yaml; use std::collections::HashMap; use utoipa::ToSchema; @@ -320,6 +321,75 @@ pub async fn providers( Ok(Json(providers_response)) } +#[utoipa::path( + post, + path = "/config/init", + responses( + (status = 200, description = "Config initialization check completed", body = String), + (status = 500, description = "Internal server error") + ) +)] +pub async fn init_config( + State(state): State, + headers: HeaderMap, +) -> Result, StatusCode> { + verify_secret_key(&headers, &state)?; + + let config = Config::global(); + + // 200 if config already exists + if config.exists() { + return Ok(Json("Config already exists".to_string())); + } + + // Find the workspace root (where the top-level Cargo.toml with [workspace] is) + let workspace_root = match std::env::current_exe() { + Ok(mut exe_path) => { + // Start from the executable's directory and traverse up + while let Some(parent) = exe_path.parent() { + let cargo_toml = parent.join("Cargo.toml"); + if cargo_toml.exists() { + // Read the Cargo.toml file + if let Ok(content) = std::fs::read_to_string(&cargo_toml) { + // Check if it contains [workspace] + if content.contains("[workspace]") { + exe_path = parent.to_path_buf(); + break; + } + } + } + exe_path = parent.to_path_buf(); + } + exe_path + } + Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), + }; + + // Check if init-config.yaml exists at workspace root + let init_config_path = workspace_root.join("init-config.yaml"); + if !init_config_path.exists() { + return Ok(Json( + "No init-config.yaml found, using default configuration".to_string(), + )); + } + + // Read init-config.yaml and validate + let init_content = match std::fs::read_to_string(&init_config_path) { + Ok(content) => content, + Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), + }; + let init_values: HashMap = match serde_yaml::from_str(&init_content) { + Ok(values) => values, + Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), + }; + + // Save init-config.yaml to ~/.config/goose/config.yaml + match config.save_values(init_values) { + Ok(_) => Ok(Json("Config initialized successfully".to_string())), + Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), + } +} + pub fn routes(state: AppState) -> Router { Router::new() .route("/config", get(read_all_config)) @@ -330,5 +400,6 @@ pub fn routes(state: AppState) -> Router { .route("/config/extensions", post(add_extension)) .route("/config/extensions/:name", delete(remove_extension)) .route("/config/providers", get(providers)) + .route("/config/init", post(init_config)) .with_state(state) } diff --git a/crates/goose/src/config/base.rs b/crates/goose/src/config/base.rs index 8367c4f5..4f11f3f5 100644 --- a/crates/goose/src/config/base.rs +++ b/crates/goose/src/config/base.rs @@ -210,7 +210,7 @@ impl Config { } // Save current values to the config file - fn save_values(&self, values: HashMap) -> Result<(), ConfigError> { + pub fn save_values(&self, values: HashMap) -> Result<(), ConfigError> { // Convert to YAML for storage let yaml_value = serde_yaml::to_string(&values)?; diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index c69d0f92..fb07b085 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -173,6 +173,29 @@ } } }, + "/config/init": { + "post": { + "tags": [ + "super::routes::config_management" + ], + "operationId": "init_config", + "responses": { + "200": { + "description": "Config initialization check completed", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "500": { + "description": "Internal server error" + } + } + } + }, "/config/providers": { "get": { "tags": [ diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 82862c3d..f23a4a79 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -31,6 +31,7 @@ import { useChat } from './hooks/useChat'; import 'react-toastify/dist/ReactToastify.css'; import { useConfig, MalformedConfigError } from './components/ConfigContext'; import { addExtensionFromDeepLink as addExtensionFromDeepLinkV2 } from './components/settings_v2/extensions'; +import { initConfig } from './api/sdk.gen'; // Views and their options export type View = @@ -91,6 +92,9 @@ export default function App() { const initializeApp = async () => { try { + // Initialize config first + await initConfig(); + const config = window.electron.getConfig(); const provider = (await read('GOOSE_PROVIDER', false)) ?? config.GOOSE_DEFAULT_PROVIDER; diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 94058a79..66010de1 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch'; -import type { GetToolsData, GetToolsResponse, ReadAllConfigData, ReadAllConfigResponse, GetExtensionsData, GetExtensionsResponse, AddExtensionData, AddExtensionResponse, RemoveExtensionData, RemoveExtensionResponse, ProvidersData, ProvidersResponse2, ReadConfigData, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse } from './types.gen'; +import type { GetToolsData, GetToolsResponse, ReadAllConfigData, ReadAllConfigResponse, GetExtensionsData, GetExtensionsResponse, AddExtensionData, AddExtensionResponse, RemoveExtensionData, RemoveExtensionResponse, InitConfigData, InitConfigResponse, ProvidersData, ProvidersResponse2, ReadConfigData, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse } from './types.gen'; import { client as _heyApiClient } from './client.gen'; export type Options = ClientOptions & { @@ -57,6 +57,13 @@ export const removeExtension = (options: O }); }; +export const initConfig = (options?: Options) => { + return (options?.client ?? _heyApiClient).post({ + url: '/config/init', + ...options + }); +}; + export const providers = (options?: Options) => { return (options?.client ?? _heyApiClient).get({ url: '/config/providers', diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 3392d5e7..75e2e016 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -360,6 +360,29 @@ export type RemoveExtensionResponses = { export type RemoveExtensionResponse = RemoveExtensionResponses[keyof RemoveExtensionResponses]; +export type InitConfigData = { + body?: never; + path?: never; + query?: never; + url: '/config/init'; +}; + +export type InitConfigErrors = { + /** + * Internal server error + */ + 500: unknown; +}; + +export type InitConfigResponses = { + /** + * Config initialization check completed + */ + 200: string; +}; + +export type InitConfigResponse = InitConfigResponses[keyof InitConfigResponses]; + export type ProvidersData = { body?: never; path?: never;