verstak/internal/core/config/appconfig.go

174 lines
5.2 KiB
Go

package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
const (
AppConfigVersion = 1
ConfigDirName = "verstak"
AppConfigFileName = "config.json"
)
// AppConfig is the global application config stored at ~/.config/verstak/config.json.
type AppConfig struct {
Version int `json:"version"`
VaultPath string `json:"vault_path"`
Theme string `json:"theme"`
Language string `json:"language"`
EnabledTemplates []string `json:"enabled_templates"`
EnabledPlugins []string `json:"enabled_plugins"`
InstalledPlugins []string `json:"installed_plugins"`
FirstRunCompleted bool `json:"first_run_completed"`
Window WindowConfig `json:"window,omitempty"`
Vault VaultAppConfig `json:"vault,omitempty"`
}
type WindowConfig struct {
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
}
// VaultAppConfig holds per-vault settings in the global config.
//
// FileWatcher controls real-time filesystem monitoring:
// - Включается автоматически при открытии vault (snapshot scan ВСЕГДА запускается,
// real-time watcher только при FileWatcher=true)
// - Snapshot scan детектит missing/restored/modified/new файлы при загрузке
// - Real-time watcher (fsnotify) реагирует на CREATE/REMOVE/WRITE мгновенно
// - Отключение: Settings → "File Watcher" toggle, или правка config.json
// - VERSTAK_NO_WATCHER=1 — выключить watcher через env (переопределяет config)
// - --no-watcher — CLI флаг (переопределяет config и env)
//
// Bridge holds the local HTTP API for browser extension integration.
type VaultAppConfig struct {
VaultID string `json:"vault_id,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
Sync SyncSettings `json:"sync,omitempty"`
FileWatcher *bool `json:"file_watcher,omitempty"`
Bridge BridgeConfig `json:"bridge,omitempty"`
}
// SyncSettings holds sync configuration for the current vault.
type SyncSettings struct {
Enabled bool `json:"enabled"`
ServerURL string `json:"server_url"`
DeviceID string `json:"device_id"`
DeviceName string `json:"device_name"`
SyncInterval int `json:"sync_interval"`
LastStatus string `json:"last_status"`
LastSyncAt string `json:"last_sync_at"`
LastError string `json:"last_error,omitempty"`
}
// BridgeConfig holds local HTTP bridge settings for browser extension.
type BridgeConfig struct {
Enabled bool `json:"enabled"` // enable/disable bridge server
Port int `json:"port"` // listen port (default 9786)
Secret string `json:"secret,omitempty"` // shared secret for extension auth
AutoGenPort bool `json:"auto_gen_port"` // pick random port if port taken
}
func DefaultAppConfig() *AppConfig {
return &AppConfig{
Version: AppConfigVersion,
Theme: "system",
Language: "ru",
EnabledTemplates: []string{"folder.default", "project.default", "client.default", "document.default", "recipe.default"},
EnabledPlugins: []string{},
Vault: VaultAppConfig{
FileWatcher: BoolPtr(true),
Bridge: BridgeConfig{
Enabled: true,
Port: 9786,
},
},
}
}
// ConfigDir returns ~/.config/verstak (or platform equivalent).
func ConfigDir() (string, error) {
cfgDir, err := os.UserConfigDir()
if err != nil {
return "", err
}
return filepath.Join(cfgDir, ConfigDirName), nil
}
// AppConfigPath returns the path to config.json.
func AppConfigPath() (string, error) {
dir, err := ConfigDir()
if err != nil {
return "", err
}
return filepath.Join(dir, AppConfigFileName), nil
}
// EnsureConfigDir creates the config directory if it doesn't exist.
func EnsureConfigDir() (string, error) {
dir, err := ConfigDir()
if err != nil {
return "", err
}
if err := os.MkdirAll(dir, 0o750); err != nil {
return "", err
}
return dir, nil
}
// LoadAppConfig reads the global config from disk. Returns nil if the file doesn't exist.
func LoadAppConfig() (*AppConfig, error) {
path, err := AppConfigPath()
if err != nil {
return nil, err
}
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("read config: %w", err)
}
if len(data) == 0 {
return nil, nil
}
var cfg AppConfig
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config: %w", err)
}
return &cfg, nil
}
// SaveAppConfig writes the global config to disk.
func SaveAppConfig(cfg *AppConfig) error {
path, err := AppConfigPath()
if err != nil {
return err
}
if _, err := EnsureConfigDir(); err != nil {
return err
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0o640)
}
// DefaultVaultPath returns the default vault location.
func DefaultVaultPath() (string, error) {
dir, err := ConfigDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "vault"), nil
}
// BoolPtr returns a pointer to the given bool value.
func BoolPtr(b bool) *bool {
return &b
}