| package launcher |
|
|
| import ( |
| "bufio" |
| "context" |
| "encoding/json" |
| "fmt" |
| "io" |
| "log" |
| "net/url" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strings" |
| "sync" |
| "syscall" |
| "time" |
|
|
| "fyne.io/fyne/v2" |
| "fyne.io/fyne/v2/container" |
| "fyne.io/fyne/v2/dialog" |
| "fyne.io/fyne/v2/widget" |
| ) |
|
|
| |
| type Config struct { |
| ModelsPath string `json:"models_path"` |
| BackendsPath string `json:"backends_path"` |
| Address string `json:"address"` |
| AutoStart bool `json:"auto_start"` |
| StartOnBoot bool `json:"start_on_boot"` |
| LogLevel string `json:"log_level"` |
| EnvironmentVars map[string]string `json:"environment_vars"` |
| ShowWelcome *bool `json:"show_welcome"` |
| } |
|
|
| |
| type Launcher struct { |
| |
| releaseManager *ReleaseManager |
| config *Config |
| ui *LauncherUI |
| systray *SystrayManager |
| ctx context.Context |
| window fyne.Window |
| app fyne.App |
|
|
| |
| localaiCmd *exec.Cmd |
| isRunning bool |
| logBuffer *strings.Builder |
| logMutex sync.RWMutex |
| statusChannel chan string |
|
|
| |
| logFile *os.File |
| logPath string |
|
|
| |
| lastUpdateCheck time.Time |
| } |
|
|
| |
| func NewLauncher(ui *LauncherUI, window fyne.Window, app fyne.App) *Launcher { |
| return &Launcher{ |
| releaseManager: NewReleaseManager(), |
| config: &Config{}, |
| logBuffer: &strings.Builder{}, |
| statusChannel: make(chan string, 100), |
| ctx: context.Background(), |
| ui: ui, |
| window: window, |
| app: app, |
| } |
| } |
|
|
| |
| func (l *Launcher) setupLogging() error { |
| |
| dataPath := l.GetDataPath() |
| logsDir := filepath.Join(dataPath, "logs") |
| if err := os.MkdirAll(logsDir, 0755); err != nil { |
| return fmt.Errorf("failed to create logs directory: %w", err) |
| } |
|
|
| |
| timestamp := time.Now().Format("2006-01-02_15-04-05") |
| l.logPath = filepath.Join(logsDir, fmt.Sprintf("localai_%s.log", timestamp)) |
|
|
| logFile, err := os.Create(l.logPath) |
| if err != nil { |
| return fmt.Errorf("failed to create log file: %w", err) |
| } |
|
|
| l.logFile = logFile |
| return nil |
| } |
|
|
| |
| func (l *Launcher) Initialize() error { |
| if l.app == nil { |
| return fmt.Errorf("app is nil") |
| } |
| log.Printf("Initializing launcher...") |
|
|
| |
| if err := l.setupLogging(); err != nil { |
| return fmt.Errorf("failed to setup logging: %w", err) |
| } |
|
|
| |
| log.Printf("Loading configuration...") |
| if err := l.loadConfig(); err != nil { |
| return fmt.Errorf("failed to load config: %w", err) |
| } |
| log.Printf("Configuration loaded, current state: ModelsPath=%s, BackendsPath=%s, Address=%s, LogLevel=%s", |
| l.config.ModelsPath, l.config.BackendsPath, l.config.Address, l.config.LogLevel) |
|
|
| |
| log.Printf("Cleaning up partial downloads...") |
| if err := l.releaseManager.CleanupPartialDownloads(); err != nil { |
| log.Printf("Warning: failed to cleanup partial downloads: %v", err) |
| } |
|
|
| if l.config.StartOnBoot { |
| l.StartLocalAI() |
| } |
| |
| if l.config.ModelsPath == "" { |
| homeDir, _ := os.UserHomeDir() |
| l.config.ModelsPath = filepath.Join(homeDir, ".localai", "models") |
| log.Printf("Setting default ModelsPath: %s", l.config.ModelsPath) |
| } |
| if l.config.BackendsPath == "" { |
| homeDir, _ := os.UserHomeDir() |
| l.config.BackendsPath = filepath.Join(homeDir, ".localai", "backends") |
| log.Printf("Setting default BackendsPath: %s", l.config.BackendsPath) |
| } |
| if l.config.Address == "" { |
| l.config.Address = "127.0.0.1:8080" |
| log.Printf("Setting default Address: %s", l.config.Address) |
| } |
| if l.config.LogLevel == "" { |
| l.config.LogLevel = "info" |
| log.Printf("Setting default LogLevel: %s", l.config.LogLevel) |
| } |
| if l.config.EnvironmentVars == nil { |
| l.config.EnvironmentVars = make(map[string]string) |
| log.Printf("Initializing empty EnvironmentVars map") |
| } |
|
|
| |
| if l.config.ShowWelcome == nil { |
| true := true |
| l.config.ShowWelcome = &true |
| log.Printf("Setting default ShowWelcome: true") |
| } |
|
|
| |
| os.MkdirAll(l.config.ModelsPath, 0755) |
| os.MkdirAll(l.config.BackendsPath, 0755) |
|
|
| |
| if err := l.saveConfig(); err != nil { |
| log.Printf("Warning: failed to save default configuration: %v", err) |
| } |
|
|
| |
|
|
| |
| if !l.releaseManager.IsLocalAIInstalled() { |
| log.Printf("No LocalAI installation found") |
| fyne.Do(func() { |
| l.updateStatus("No LocalAI installation found") |
| if l.ui != nil { |
| |
| l.showDownloadLocalAIDialog() |
| } |
| }) |
| } |
|
|
| |
| go l.periodicUpdateCheck() |
|
|
| return nil |
| } |
|
|
| |
| func (l *Launcher) StartLocalAI() error { |
| if l.isRunning { |
| return fmt.Errorf("LocalAI is already running") |
| } |
|
|
| |
| if err := l.releaseManager.VerifyInstalledBinary(); err != nil { |
| |
| binaryPath := l.releaseManager.GetBinaryPath() |
| if removeErr := os.Remove(binaryPath); removeErr != nil { |
| log.Printf("Failed to remove corrupted binary: %v", removeErr) |
| } |
| return fmt.Errorf("LocalAI binary is corrupted: %v. Please reinstall LocalAI", err) |
| } |
|
|
| binaryPath := l.releaseManager.GetBinaryPath() |
| if _, err := os.Stat(binaryPath); os.IsNotExist(err) { |
| return fmt.Errorf("LocalAI binary not found. Please download a release first") |
| } |
|
|
| |
| args := []string{ |
| "run", |
| "--models-path", l.config.ModelsPath, |
| "--backends-path", l.config.BackendsPath, |
| "--address", l.config.Address, |
| "--log-level", l.config.LogLevel, |
| } |
|
|
| l.localaiCmd = exec.CommandContext(l.ctx, binaryPath, args...) |
|
|
| |
| if len(l.config.EnvironmentVars) > 0 { |
| env := os.Environ() |
| for key, value := range l.config.EnvironmentVars { |
| env = append(env, fmt.Sprintf("%s=%s", key, value)) |
| } |
| l.localaiCmd.Env = env |
| } |
|
|
| |
| stdout, err := l.localaiCmd.StdoutPipe() |
| if err != nil { |
| return fmt.Errorf("failed to create stdout pipe: %w", err) |
| } |
|
|
| stderr, err := l.localaiCmd.StderrPipe() |
| if err != nil { |
| return fmt.Errorf("failed to create stderr pipe: %w", err) |
| } |
|
|
| |
| if err := l.localaiCmd.Start(); err != nil { |
| return fmt.Errorf("failed to start LocalAI: %w", err) |
| } |
|
|
| l.isRunning = true |
|
|
| fyne.Do(func() { |
| l.updateStatus("LocalAI is starting...") |
| l.updateRunningState(true) |
| }) |
|
|
| |
| go l.monitorLogs(stdout, "STDOUT") |
| go l.monitorLogs(stderr, "STDERR") |
|
|
| |
| go func() { |
| |
| err := l.localaiCmd.Wait() |
| l.isRunning = false |
| fyne.Do(func() { |
| l.updateRunningState(false) |
| if err != nil { |
| l.updateStatus(fmt.Sprintf("LocalAI stopped with error: %v", err)) |
| } else { |
| l.updateStatus("LocalAI stopped") |
| } |
| }) |
| }() |
|
|
| |
| go func() { |
| time.Sleep(10 * time.Second) |
| if l.isRunning { |
| |
| if l.localaiCmd.Process != nil { |
| if err := l.localaiCmd.Process.Signal(syscall.Signal(0)); err != nil { |
| |
| l.isRunning = false |
| fyne.Do(func() { |
| l.updateRunningState(false) |
| l.updateStatus("LocalAI failed to start properly") |
| }) |
| } |
| } |
| } |
| }() |
|
|
| return nil |
| } |
|
|
| |
| func (l *Launcher) StopLocalAI() error { |
| if !l.isRunning || l.localaiCmd == nil { |
| return fmt.Errorf("LocalAI is not running") |
| } |
|
|
| |
| if err := l.localaiCmd.Process.Signal(os.Interrupt); err != nil { |
| |
| if killErr := l.localaiCmd.Process.Kill(); killErr != nil { |
| return fmt.Errorf("failed to kill LocalAI process: %w", killErr) |
| } |
| } |
|
|
| l.isRunning = false |
| fyne.Do(func() { |
| l.updateRunningState(false) |
| l.updateStatus("LocalAI stopped") |
| }) |
| return nil |
| } |
|
|
| |
| func (l *Launcher) IsRunning() bool { |
| return l.isRunning |
| } |
|
|
| |
| func (l *Launcher) Shutdown() error { |
| log.Printf("Launcher shutting down, stopping LocalAI...") |
|
|
| |
| if l.isRunning { |
| if err := l.StopLocalAI(); err != nil { |
| log.Printf("Error stopping LocalAI during shutdown: %v", err) |
| } |
| } |
|
|
| |
| if l.logFile != nil { |
| if err := l.logFile.Close(); err != nil { |
| log.Printf("Error closing log file: %v", err) |
| } |
| l.logFile = nil |
| } |
|
|
| log.Printf("Launcher shutdown complete") |
| return nil |
| } |
|
|
| |
| func (l *Launcher) GetLogs() string { |
| l.logMutex.RLock() |
| defer l.logMutex.RUnlock() |
| return l.logBuffer.String() |
| } |
|
|
| |
| func (l *Launcher) GetRecentLogs() string { |
| l.logMutex.RLock() |
| defer l.logMutex.RUnlock() |
|
|
| content := l.logBuffer.String() |
| lines := strings.Split(content, "\n") |
|
|
| |
| if len(lines) > 50 { |
| lines = lines[len(lines)-50:] |
| } |
|
|
| return strings.Join(lines, "\n") |
| } |
|
|
| |
| func (l *Launcher) GetConfig() *Config { |
| return l.config |
| } |
|
|
| |
| func (l *Launcher) SetConfig(config *Config) error { |
| l.config = config |
| return l.saveConfig() |
| } |
|
|
| func (l *Launcher) GetUI() *LauncherUI { |
| return l.ui |
| } |
|
|
| func (l *Launcher) SetSystray(systray *SystrayManager) { |
| l.systray = systray |
| } |
|
|
| |
| func (l *Launcher) GetReleaseManager() *ReleaseManager { |
| return l.releaseManager |
| } |
|
|
| |
| func (l *Launcher) GetWebUIURL() string { |
| address := l.config.Address |
| if strings.HasPrefix(address, ":") { |
| address = "localhost" + address |
| } |
| if !strings.HasPrefix(address, "http") { |
| address = "http://" + address |
| } |
| return address |
| } |
|
|
| |
| func (l *Launcher) GetDataPath() string { |
| |
| |
| if l.config != nil && l.config.ModelsPath != "" { |
| |
| return filepath.Dir(l.config.ModelsPath) |
| } |
|
|
| |
| homeDir, err := os.UserHomeDir() |
| if err != nil { |
| return "." |
| } |
| return filepath.Join(homeDir, ".localai") |
| } |
|
|
| |
| func (l *Launcher) CheckForUpdates() (bool, string, error) { |
| log.Printf("CheckForUpdates: checking for available updates...") |
| available, version, err := l.releaseManager.IsUpdateAvailable() |
| if err != nil { |
| log.Printf("CheckForUpdates: error occurred: %v", err) |
| return false, "", err |
| } |
| log.Printf("CheckForUpdates: result - available=%v, version=%s", available, version) |
| l.lastUpdateCheck = time.Now() |
| return available, version, nil |
| } |
|
|
| |
| func (l *Launcher) DownloadUpdate(version string, progressCallback func(float64)) error { |
| return l.releaseManager.DownloadRelease(version, progressCallback) |
| } |
|
|
| |
| func (l *Launcher) GetCurrentVersion() string { |
| return l.releaseManager.GetInstalledVersion() |
| } |
|
|
| |
| func (l *Launcher) GetCurrentStatus() string { |
| select { |
| case status := <-l.statusChannel: |
| return status |
| default: |
| if l.isRunning { |
| return "LocalAI is running" |
| } |
| return "Ready" |
| } |
| } |
|
|
| |
| func (l *Launcher) GetLastStatus() string { |
| if l.isRunning { |
| return "LocalAI is running" |
| } |
|
|
| |
| if !l.releaseManager.IsLocalAIInstalled() { |
| return "LocalAI not installed" |
| } |
|
|
| return "Ready" |
| } |
|
|
| func (l *Launcher) githubReleaseNotesURL(version string) (*url.URL, error) { |
| |
| releaseURL := fmt.Sprintf("https://github.com/%s/%s/releases/tag/%s", |
| l.releaseManager.GitHubOwner, |
| l.releaseManager.GitHubRepo, |
| version) |
|
|
| |
| return url.Parse(releaseURL) |
| } |
|
|
| |
| func (l *Launcher) showDownloadLocalAIDialog() { |
| if l.app == nil { |
| log.Printf("Cannot show download dialog: app is nil") |
| return |
| } |
|
|
| fyne.DoAndWait(func() { |
| |
| dialogWindow := l.app.NewWindow("LocalAI Installation Required") |
| dialogWindow.Resize(fyne.NewSize(500, 350)) |
| dialogWindow.CenterOnScreen() |
| dialogWindow.SetCloseIntercept(func() { |
| dialogWindow.Close() |
| }) |
|
|
| |
| titleLabel := widget.NewLabel("LocalAI Not Found") |
| titleLabel.TextStyle = fyne.TextStyle{Bold: true} |
| titleLabel.Alignment = fyne.TextAlignCenter |
|
|
| messageLabel := widget.NewLabel("LocalAI is not installed on your system.\n\nWould you like to download and install the latest version?") |
| messageLabel.Wrapping = fyne.TextWrapWord |
| messageLabel.Alignment = fyne.TextAlignCenter |
|
|
| |
| downloadButton := widget.NewButton("Download & Install", func() { |
| dialogWindow.Close() |
| l.downloadAndInstallLocalAI() |
| if l.systray != nil { |
| l.systray.recreateMenu() |
| } |
| }) |
| downloadButton.Importance = widget.HighImportance |
|
|
| |
| releaseNotesButton := widget.NewButton("View Release Notes", func() { |
| |
| go func() { |
| release, err := l.releaseManager.GetLatestRelease() |
| if err != nil { |
| log.Printf("Failed to get latest release info: %v", err) |
| return |
| } |
|
|
| releaseNotesURL, err := l.githubReleaseNotesURL(release.Version) |
| if err != nil { |
| log.Printf("Failed to parse URL: %v", err) |
| return |
| } |
|
|
| l.app.OpenURL(releaseNotesURL) |
| }() |
| }) |
|
|
| skipButton := widget.NewButton("Skip for Now", func() { |
| dialogWindow.Close() |
| }) |
|
|
| |
| actionButtons := container.NewHBox(skipButton, downloadButton) |
| content := container.NewVBox( |
| titleLabel, |
| widget.NewSeparator(), |
| messageLabel, |
| widget.NewSeparator(), |
| releaseNotesButton, |
| widget.NewSeparator(), |
| actionButtons, |
| ) |
|
|
| dialogWindow.SetContent(content) |
| dialogWindow.Show() |
| }) |
| } |
|
|
| |
| func (l *Launcher) downloadAndInstallLocalAI() { |
| if l.app == nil { |
| log.Printf("Cannot download LocalAI: app is nil") |
| return |
| } |
|
|
| |
| go func() { |
| log.Printf("Checking for latest LocalAI version...") |
| available, version, err := l.CheckForUpdates() |
| if err != nil { |
| log.Printf("Failed to check for updates: %v", err) |
| l.showDownloadError("Failed to check for latest version", err.Error()) |
| return |
| } |
|
|
| if !available { |
| log.Printf("No updates available, but LocalAI is not installed") |
| l.showDownloadError("No Version Available", "Could not determine the latest LocalAI version. Please check your internet connection and try again.") |
| return |
| } |
|
|
| log.Printf("Latest version available: %s", version) |
| |
| l.showDownloadProgress(version, fmt.Sprintf("Downloading LocalAI %s...", version)) |
| }() |
| } |
|
|
| |
| func (l *Launcher) showDownloadError(title, message string) { |
| fyne.DoAndWait(func() { |
| |
| errorWindow := l.app.NewWindow("Download Error") |
| errorWindow.Resize(fyne.NewSize(400, 200)) |
| errorWindow.CenterOnScreen() |
| errorWindow.SetCloseIntercept(func() { |
| errorWindow.Close() |
| }) |
|
|
| |
| titleLabel := widget.NewLabel(title) |
| titleLabel.TextStyle = fyne.TextStyle{Bold: true} |
| titleLabel.Alignment = fyne.TextAlignCenter |
|
|
| messageLabel := widget.NewLabel(message) |
| messageLabel.Wrapping = fyne.TextWrapWord |
| messageLabel.Alignment = fyne.TextAlignCenter |
|
|
| |
| closeButton := widget.NewButton("Close", func() { |
| errorWindow.Close() |
| }) |
|
|
| |
| content := container.NewVBox( |
| titleLabel, |
| widget.NewSeparator(), |
| messageLabel, |
| widget.NewSeparator(), |
| closeButton, |
| ) |
|
|
| errorWindow.SetContent(content) |
| errorWindow.Show() |
| }) |
| } |
|
|
| |
| func (l *Launcher) showDownloadProgress(version, title string) { |
| fyne.DoAndWait(func() { |
| |
| progressWindow := l.app.NewWindow("Downloading LocalAI") |
| progressWindow.Resize(fyne.NewSize(400, 250)) |
| progressWindow.CenterOnScreen() |
| progressWindow.SetCloseIntercept(func() { |
| progressWindow.Close() |
| }) |
|
|
| |
| progressBar := widget.NewProgressBar() |
| progressBar.SetValue(0) |
|
|
| |
| statusLabel := widget.NewLabel("Preparing download...") |
|
|
| |
| releaseNotesButton := widget.NewButton("View Release Notes", func() { |
| releaseNotesURL, err := l.githubReleaseNotesURL(version) |
| if err != nil { |
| log.Printf("Failed to parse URL: %v", err) |
| return |
| } |
|
|
| l.app.OpenURL(releaseNotesURL) |
| }) |
|
|
| |
| progressContainer := container.NewVBox( |
| widget.NewLabel(title), |
| progressBar, |
| statusLabel, |
| widget.NewSeparator(), |
| releaseNotesButton, |
| ) |
|
|
| progressWindow.SetContent(progressContainer) |
| progressWindow.Show() |
|
|
| |
| go func() { |
| err := l.DownloadUpdate(version, func(progress float64) { |
| |
| fyne.Do(func() { |
| progressBar.SetValue(progress) |
| percentage := int(progress * 100) |
| statusLabel.SetText(fmt.Sprintf("Downloading... %d%%", percentage)) |
| }) |
| }) |
|
|
| |
| fyne.Do(func() { |
| if err != nil { |
| statusLabel.SetText(fmt.Sprintf("Download failed: %v", err)) |
| |
| dialog.ShowError(err, progressWindow) |
| } else { |
| statusLabel.SetText("Download completed successfully!") |
| progressBar.SetValue(1.0) |
|
|
| |
| dialog.ShowConfirm("Installation Complete", |
| "LocalAI has been downloaded and installed successfully. You can now start LocalAI from the launcher.", |
| func(close bool) { |
| progressWindow.Close() |
| |
| l.updateStatus("LocalAI installed successfully") |
|
|
| if l.systray != nil { |
| l.systray.recreateMenu() |
| } |
| }, progressWindow) |
| } |
| }) |
| }() |
| }) |
| } |
|
|
| |
| func (l *Launcher) monitorLogs(reader io.Reader, prefix string) { |
| scanner := bufio.NewScanner(reader) |
| for scanner.Scan() { |
| line := scanner.Text() |
| timestamp := time.Now().Format("15:04:05") |
| logLine := fmt.Sprintf("[%s] %s: %s\n", timestamp, prefix, line) |
|
|
| l.logMutex.Lock() |
| l.logBuffer.WriteString(logLine) |
| |
| if l.logBuffer.Len() > 100000 { |
| content := l.logBuffer.String() |
| |
| if len(content) > 50000 { |
| l.logBuffer.Reset() |
| l.logBuffer.WriteString(content[len(content)-50000:]) |
| } |
| } |
| l.logMutex.Unlock() |
|
|
| |
| if l.logFile != nil { |
| if _, err := l.logFile.WriteString(logLine); err != nil { |
| log.Printf("Failed to write to log file: %v", err) |
| } |
| } |
|
|
| fyne.Do(func() { |
| |
| if l.ui != nil { |
| l.ui.OnLogUpdate(logLine) |
| } |
|
|
| |
| if strings.Contains(line, "API server listening") { |
| l.updateStatus("LocalAI is running") |
| } |
| }) |
| } |
| } |
|
|
| |
| func (l *Launcher) updateStatus(status string) { |
| select { |
| case l.statusChannel <- status: |
| default: |
| |
| } |
|
|
| if l.ui != nil { |
| l.ui.UpdateStatus(status) |
| } |
|
|
| if l.systray != nil { |
| l.systray.UpdateStatus(status) |
| } |
| } |
|
|
| |
| func (l *Launcher) updateRunningState(isRunning bool) { |
| if l.ui != nil { |
| l.ui.UpdateRunningState(isRunning) |
| } |
|
|
| if l.systray != nil { |
| l.systray.UpdateRunningState(isRunning) |
| } |
| } |
|
|
| |
| func (l *Launcher) periodicUpdateCheck() { |
| ticker := time.NewTicker(1 * time.Hour) |
| defer ticker.Stop() |
|
|
| for { |
| select { |
| case <-ticker.C: |
| available, version, err := l.CheckForUpdates() |
| if err == nil && available { |
| fyne.Do(func() { |
| l.updateStatus(fmt.Sprintf("Update available: %s", version)) |
| if l.systray != nil { |
| l.systray.NotifyUpdateAvailable(version) |
| } |
| if l.ui != nil { |
| l.ui.NotifyUpdateAvailable(version) |
| } |
| }) |
| } |
| case <-l.ctx.Done(): |
| return |
| } |
| } |
| } |
|
|
| |
| func (l *Launcher) loadConfig() error { |
| homeDir, err := os.UserHomeDir() |
| if err != nil { |
| return fmt.Errorf("failed to get home directory: %w", err) |
| } |
|
|
| configPath := filepath.Join(homeDir, ".localai", "launcher.json") |
| log.Printf("Loading config from: %s", configPath) |
|
|
| if _, err := os.Stat(configPath); os.IsNotExist(err) { |
| log.Printf("Config file not found, creating default config") |
| |
| return l.saveConfig() |
| } |
|
|
| |
| configData, err := os.ReadFile(configPath) |
| if err != nil { |
| return fmt.Errorf("failed to read config file: %w", err) |
| } |
|
|
| log.Printf("Config file content: %s", string(configData)) |
|
|
| log.Printf("loadConfig: about to unmarshal JSON data") |
| if err := json.Unmarshal(configData, l.config); err != nil { |
| return fmt.Errorf("failed to parse config file: %w", err) |
| } |
| log.Printf("loadConfig: JSON unmarshaled successfully") |
|
|
| log.Printf("Loaded config: ModelsPath=%s, BackendsPath=%s, Address=%s, LogLevel=%s", |
| l.config.ModelsPath, l.config.BackendsPath, l.config.Address, l.config.LogLevel) |
| log.Printf("Environment vars: %v", l.config.EnvironmentVars) |
|
|
| return nil |
| } |
|
|
| |
| func (l *Launcher) saveConfig() error { |
| homeDir, err := os.UserHomeDir() |
| if err != nil { |
| return fmt.Errorf("failed to get home directory: %w", err) |
| } |
|
|
| configDir := filepath.Join(homeDir, ".localai") |
| if err := os.MkdirAll(configDir, 0755); err != nil { |
| return fmt.Errorf("failed to create config directory: %w", err) |
| } |
|
|
| |
| log.Printf("saveConfig: marshaling config with EnvironmentVars: %v", l.config.EnvironmentVars) |
| configData, err := json.MarshalIndent(l.config, "", " ") |
| if err != nil { |
| return fmt.Errorf("failed to marshal config: %w", err) |
| } |
| log.Printf("saveConfig: JSON marshaled successfully, length: %d", len(configData)) |
|
|
| configPath := filepath.Join(configDir, "launcher.json") |
| log.Printf("Saving config to: %s", configPath) |
| log.Printf("Config content: %s", string(configData)) |
|
|
| if err := os.WriteFile(configPath, configData, 0644); err != nil { |
| return fmt.Errorf("failed to write config file: %w", err) |
| } |
|
|
| log.Printf("Config saved successfully") |
| return nil |
| } |
|
|