diff --git a/cmd/app/frontend/dist/assets/app.js b/cmd/app/frontend/dist/assets/app.js deleted file mode 100644 index 28abaa3..0000000 --- a/cmd/app/frontend/dist/assets/app.js +++ /dev/null @@ -1 +0,0 @@ -console.log("Hello from app.js!"); diff --git a/cmd/app/frontend/dist/index.html b/cmd/app/frontend/dist/index.html deleted file mode 100644 index 916c9c4..0000000 --- a/cmd/app/frontend/dist/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - Core - - -

Core

- - - diff --git a/cmd/app/main.go b/cmd/app/main.go deleted file mode 100644 index 01fd1d1..0000000 --- a/cmd/app/main.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "embed" - "fmt" - - "core" - "github.com/wailsapp/wails/v3/pkg/application" -) - -//go:embed all:frontend/dist -var assets embed.FS - -func main() { - app := application.New(application.Options{ - Services: []application.Service{ - application.NewService(core.New(assets)), - }, - }) - - core.Setup(app) - - app.Event.On("setup-done", func(e *application.CustomEvent) { - fmt.Println("Setup done!") - }) - - err := app.Run() - if err != nil { - panic(err) - } -} diff --git a/config/config.go b/config/config.go deleted file mode 100644 index 72bea66..0000000 --- a/config/config.go +++ /dev/null @@ -1,52 +0,0 @@ -package config - -import ( - "fmt" - "reflect" - "strings" -) - -// Config holds the resolved paths and user-configurable settings for the application. -type Config struct { - // --- Dynamic Paths (not stored in config.json) --- - DataDir string `json:"-"` - ConfigDir string `json:"-"` - CacheDir string `json:"-"` - WorkspacesDir string `json:"-"` - RootDir string `json:"-"` - UserHomeDir string `json:"-"` - IsNew bool `json:"-"` // Flag indicating if the config was newly created. - - // --- Storable Settings (persisted in config.json) --- - DefaultRoute string `json:"defaultRoute,omitempty"` - Features []string `json:"features,omitempty"` - Language string `json:"language,omitempty"` -} - -// Key retrieves a configuration value by its key. It checks JSON tags and field names (case-insensitive). -func (c *Config) Key(key string) (interface{}, error) { - // Use reflection to inspect the struct fields. - val := reflect.ValueOf(c).Elem() - typ := val.Type() - - for i := 0; i < val.NumField(); i++ { - field := typ.Field(i) - fieldName := field.Name - - // Check the field name first. - if strings.EqualFold(fieldName, key) { - return val.Field(i).Interface(), nil - } - - // Then check the `json` tag. - jsonTag := field.Tag.Get("json") - if jsonTag != "" && jsonTag != "-" { - jsonName := strings.Split(jsonTag, ",")[0] - if strings.EqualFold(jsonName, key) { - return val.Field(i).Interface(), nil - } - } - } - - return nil, fmt.Errorf("key '%s' not found in config", key) -} diff --git a/config/service.go b/config/service.go deleted file mode 100644 index 745f358..0000000 --- a/config/service.go +++ /dev/null @@ -1,175 +0,0 @@ -package config - -import ( - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/adrg/xdg" -) - -const appName = "lethean" -const configFileName = "config.json" - -// ErrSetupRequired is returned by ServiceStartup if config.json is missing. -var ErrSetupRequired = errors.New("setup required: config.json not found") - -// Service provides access to the application's configuration. -type Service struct { - config *Config -} - -// NewService creates and initializes a new configuration service. -// It loads an existing configuration or creates a default one if not found. -func NewService() (*Service, error) { - // 1. Determine the config directory path to check for an existing file. - homeDir, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("could not resolve user home directory: %w", err) - } - userHomeDir := filepath.Join(homeDir, appName) - configDir := filepath.Join(userHomeDir, "config") - configPath := filepath.Join(configDir, configFileName) - - var cfg *Config - configNeedsSaving := false - - // 2. Check if the config file exists. - if _, err := os.Stat(configPath); err == nil { - // --- Config file EXISTS --- - - // First, get the base config with all the dynamic paths and directory structures. - cfg, err = newDefaultConfig() - if err != nil { - return nil, fmt.Errorf("failed to create base config structure: %w", err) - } - cfg.IsNew = false // Mark that we are loading an existing config. - - // Now, load the storable values from the existing file, which will override the defaults. - fileData, err := os.ReadFile(configPath) - if err != nil { - return nil, fmt.Errorf("failed to read existing config file at %s: %w", configPath, err) - } - - if err := json.Unmarshal(fileData, cfg); err != nil { - // If unmarshalling fails, we log a warning but proceed with the default config. - // This prevents a corrupted config.json from crashing the app. - fmt.Fprintf(os.Stderr, "Warning: Failed to unmarshal config.json at %s, using defaults: %v\n", configPath, err) - } - - } else if errors.Is(err, os.ErrNotExist) { - // --- Config file DOES NOT EXIST --- - configNeedsSaving = true - - // Create a fresh default config. This sets up paths and a default "en" language. - cfg, err = newDefaultConfig() - if err != nil { - return nil, fmt.Errorf("failed to create default config: %w", err) - } - cfg.IsNew = true // Mark that this is a new config. - - } else { - // Another error occurred (e.g., permissions). - return nil, fmt.Errorf("failed to check for config file at %s: %w", configPath, err) - } - - service := &Service{config: cfg} - - // If the config file didn't exist, save the newly generated one. - if configNeedsSaving { - if err := service.Save(); err != nil { - return nil, fmt.Errorf("failed to save initial config: %w", err) - } - } - - return service, nil -} - -// newDefaultConfig creates a default configuration with resolved paths and ensures directories exist. -func newDefaultConfig() (*Config, error) { - if strings.Contains(appName, "..") || strings.Contains(appName, string(filepath.Separator)) { - return nil, fmt.Errorf("invalid app name '%s': contains path traversal characters", appName) - } - - homeDir, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("could not resolve user home directory: %w", err) - } - userHomeDir := filepath.Join(homeDir, appName) - - rootDir, err := xdg.DataFile(appName) - if err != nil { - return nil, fmt.Errorf("could not resolve data directory: %w", err) - } - - cacheDir, err := xdg.CacheFile(appName) - if err != nil { - return nil, fmt.Errorf("could not resolve cache directory: %w", err) - } - - cfg := &Config{ - UserHomeDir: userHomeDir, - RootDir: rootDir, - CacheDir: cacheDir, - ConfigDir: filepath.Join(userHomeDir, "config"), - DataDir: filepath.Join(userHomeDir, "data"), - WorkspacesDir: filepath.Join(userHomeDir, "workspaces"), - DefaultRoute: "/", - Features: []string{}, - Language: "en", // Hardcoded default, will be overridden if loaded or detected - } - - dirs := []string{cfg.RootDir, cfg.ConfigDir, cfg.DataDir, cfg.CacheDir, cfg.WorkspacesDir, cfg.UserHomeDir} - for _, dir := range dirs { - if err := os.MkdirAll(dir, os.ModePerm); err != nil { - return nil, fmt.Errorf("could not create directory %s: %w", dir, err) - } - } - - return cfg, nil -} - -// Get returns the loaded configuration. -func (s *Service) Get() *Config { - return s.config -} - -// Save writes the current configuration to config.json. -func (s *Service) Save() error { - configPath := filepath.Join(s.config.ConfigDir, configFileName) - - data, err := json.MarshalIndent(s.config, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal config: %w", err) - } - - if err := os.WriteFile(configPath, data, 0644); err != nil { - return fmt.Errorf("failed to write config file: %w", err) - } - return nil -} - -// IsFeatureEnabled checks if a given feature is enabled in the configuration. -func (s *Service) IsFeatureEnabled(feature string) bool { - for _, f := range s.config.Features { - if f == feature { - return true - } - } - return false -} - -// EnableFeature adds a feature to the list of enabled features and saves the config. -func (s *Service) EnableFeature(feature string) error { - if s.IsFeatureEnabled(feature) { - return nil - } - s.config.Features = append(s.config.Features, feature) - if err := s.Save(); err != nil { - return fmt.Errorf("failed to save config after enabling feature %s: %w", feature, err) - } - return nil -} diff --git a/config/service_test.go b/config/service_test.go deleted file mode 100644 index c3484ee..0000000 --- a/config/service_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package config - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/adrg/xdg" -) - -// setupTestEnv creates temporary directories and sets environment variables -// to simulate a specific user home and XDG base directories for testing. -// It returns the path to the temporary home directory and a cleanup function. -func setupTestEnv(t *testing.T) (string, func()) { - // Create a temporary directory for the user's home - tempHomeDir, err := os.MkdirTemp("", "test_home") - if err != nil { - t.Fatalf("Failed to create temp home directory: %v", err) - } - - // Store original HOME environment variable to restore it later - oldHome := os.Getenv("HOME") - - // Set HOME environment variable for the test - os.Setenv("HOME", tempHomeDir) - - cleanup := func() { - // Restore original HOME environment variable - os.Setenv("HOME", oldHome) - // Clean up temporary directories - os.RemoveAll(tempHomeDir) - } - - return tempHomeDir, cleanup -} - -func TestNewService(t *testing.T) { - tempHomeDir, cleanup := setupTestEnv(t) - defer cleanup() - - service, err := NewService() - if err != nil { - t.Fatalf("NewService() failed: %v", err) - } - - cfg := service.Get() - - // These paths are based on the mocked HOME directory - expectedUserHomeDir := filepath.Join(tempHomeDir, appName) - expectedConfigDir := filepath.Join(expectedUserHomeDir, "config") - expectedDataDir := filepath.Join(expectedUserHomeDir, "data") - expectedWorkspacesDir := filepath.Join(expectedUserHomeDir, "workspaces") - - // For RootDir and CacheDir, xdg library's init() might have already run - // before our test's os.Setenv calls take effect for xdg. So, we calculate - // the *expected* values based on what xdg *actually* returns in the - // current process, which will likely be the system defaults or whatever - // was set before the test started. - actualXDGDataFile, err := xdg.DataFile(appName) - if err != nil { - t.Fatalf("xdg.DataFile failed: %v", err) - } - actualXDGCacheFile, err := xdg.CacheFile(appName) - if err != nil { - t.Fatalf("xdg.CacheFile failed: %v", err) - } - - expectedRootDir := actualXDGDataFile - expectedCacheDir := actualXDGCacheFile - - tests := []struct { - name string - actual string - expected string - }{ - {"UserHomeDir", cfg.UserHomeDir, expectedUserHomeDir}, - {"RootDir", cfg.RootDir, expectedRootDir}, - {"ConfigDir", cfg.ConfigDir, expectedConfigDir}, - {"DataDir", cfg.DataDir, expectedDataDir}, - {"CacheDir", cfg.CacheDir, expectedCacheDir}, - {"WorkspacesDir", cfg.WorkspacesDir, expectedWorkspacesDir}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.actual != tt.expected { - t.Errorf("Mismatch for %s: got %q, want %q", tt.name, tt.actual, tt.expected) - } - // Also check if the directory was actually created - if info, err := os.Stat(tt.actual); err != nil { - t.Errorf("Directory %q for %s was not created: %v", tt.actual, tt.name, err) - } else if !info.IsDir() { - t.Errorf("Path %q for %s is not a directory", tt.actual, tt.name) - } - }) - } -} - -func TestNewService_DirectoryCreationFails(t *testing.T) { - // Create a temporary directory that we will make read-only - tempHomeDir, err := os.MkdirTemp("", "test_readonly_home") - if err != nil { - t.Fatalf("Failed to create temp home directory: %v", err) - } - // Ensure cleanup happens, and restore permissions before removing - defer func() { - os.Chmod(tempHomeDir, 0755) // Restore write permissions for os.RemoveAll - os.RemoveAll(tempHomeDir) - }() - - // Make the temporary home directory read-only - if err := os.Chmod(tempHomeDir, 0555); err != nil { // r-xr-xr-x - t.Fatalf("Failed to make temp home directory read-only: %v", err) - } - - // Store original HOME environment variable to restore it later - oldHome := os.Getenv("HOME") - os.Setenv("HOME", tempHomeDir) - defer os.Setenv("HOME", oldHome) - - // NewService should now fail because it cannot create subdirectories in tempHomeDir - _, err = NewService() - if err == nil { - t.Errorf("NewService() expected to fail when directory creation is impossible, but it succeeded") - } - // Optionally, check for a specific error message or type - if err != nil && !strings.Contains(err.Error(), "could not create directory") { - t.Errorf("NewService() failed with unexpected error: %v", err) - } -} - -func TestNewService_PathTraversalAttempt(t *testing.T) { - - problematicAppName := "../lethean" - - // Simulate the validation logic from NewService - if !strings.Contains(problematicAppName, "..") && !strings.Contains(problematicAppName, string(filepath.Separator)) { - t.Errorf("Expected problematicAppName to contain path traversal characters, but it didn't") - } - - // We'll create a temporary function to simulate the validation within NewService - validateAppName := func(name string) error { - if strings.Contains(name, "..") || strings.Contains(name, string(filepath.Separator)) { - return fmt.Errorf("invalid app name '%s': contains path traversal characters", name) - } - return nil - } - - // Test with a problematic app name - err := validateAppName(problematicAppName) - if err == nil { - t.Errorf("validateAppName expected to fail for %q, but it succeeded", problematicAppName) - } - if err != nil && !strings.Contains(err.Error(), "path traversal characters") { - t.Errorf("validateAppName failed for %q with unexpected error: %v", problematicAppName, err) - } - // Test with a safe app name - safeAppName := "lethean" - err = validateAppName(safeAppName) - if err != nil { - t.Errorf("validateAppName expected to succeed for %q, but it failed with error: %v", safeAppName, err) - } -} diff --git a/core.go b/core.go deleted file mode 100644 index 973702f..0000000 --- a/core.go +++ /dev/null @@ -1,148 +0,0 @@ -package core - -import ( - "embed" - "fmt" - "sync" - - "core/config" - "core/crypt" - "core/display" - "core/docs" - "core/filesystem" - "core/filesystem/local" - "core/workspace" - "github.com/wailsapp/wails/v3/pkg/application" -) - -// Service provides access to all core application services. -type Service struct { - app *application.App - configService *config.Service - displayService *display.Service - docsService *docs.Service - cryptService *crypt.Service - workspaceService *workspace.Service -} - -var ( - instance *Service - once sync.Once - initErr error -) - -// New performs Phase 1 of initialization: Instantiation. -// It creates the raw service objects without wiring them together. -func New(assets embed.FS) *Service { - once.Do(func() { - // Instantiate services in the correct order of dependency. - configService, err := config.NewService() - if err != nil { - initErr = fmt.Errorf("failed to initialize config service: %w", err) - return - } - - // Initialize the local filesystem medium - filesystem.Local, err = local.New(configService.Get().RootDir) - if err != nil { - initErr = fmt.Errorf("failed to initialize local filesystem: %w", err) - return - } - - displayService := display.NewService(display.ClientHub, assets) - docsService := docs.NewService(assets) - cryptService := crypt.NewService(configService.Get()) - workspaceService := workspace.NewService(configService.Get(), workspace.NewLocalMedium()) - - instance = &Service{ - configService: configService, - displayService: displayService, - docsService: docsService, - cryptService: cryptService, - workspaceService: workspaceService, - } - }) - - if initErr != nil { - panic(initErr) // A failure in a core service is fatal. - } - - return instance -} - -// Setup performs Phase 2 of initialization: Wiring. -// It injects the required dependencies into each service. -func Setup(app *application.App) { - if instance == nil { - panic("core.Setup() called before core.New() was successfully initialized") - } - instance.app = app - - // Wire the services with their dependencies. - instance.displayService.Setup(app, instance.configService, nil) - instance.docsService.Setup(app, instance.displayService) -} - -// App returns the global application instance. -func App() *application.App { - if instance == nil || instance.app == nil { - panic("core.App() called before core.Setup() was successfully initialized") - } - return instance.app -} - -// Config returns the singleton instance of the ConfigService. -func Config() *config.Service { - if instance == nil { - panic("core.Config() called before core.New() was successfully initialized") - } - return instance.configService -} - -// Display returns the singleton instance of the display.Service. -func Display() *display.Service { - if instance == nil { - panic("core.Display() called before core.New() was successfully initialized") - } - return instance.displayService -} - -// Docs returns the singleton instance of the DocsService. -func Docs() *docs.Service { - if instance == nil { - panic("core.Docs() called before core.New() was successfully initialized") - } - return instance.docsService -} - -// Crypt returns the singleton instance of the CryptService. -func Crypt() *crypt.Service { - if instance == nil { - panic("core.Crypt() called before core.New() was successfully initialized") - } - return instance.cryptService -} - -// Filesystem returns the singleton instance of the FilesystemService. -func Filesystem() filesystem.Medium { - if instance == nil { - panic("core.Filesystem() called before core.New() was successfully initialized") - } - return filesystem.Local -} - -// Workspace returns the singleton instance of the WorkspaceService. -func Workspace() *workspace.Service { - if instance == nil { - panic("core.Workspace() called before core.New() was successfully initialized") - } - return instance.workspaceService -} - -// Runtime returns the singleton instance of the Service. -func Runtime() *Service { - if instance == nil { - panic("core.Runtime() called before core.New() was successfully initialized") - } - return instance -} diff --git a/crypt/crypt.go b/crypt/crypt.go deleted file mode 100644 index b6cff01..0000000 --- a/crypt/crypt.go +++ /dev/null @@ -1,23 +0,0 @@ -package crypt - -import ( - "core/config" -) - -// HashType defines the supported hashing algorithms. -type HashType string - -const ( - LTHN HashType = "lthn" - SHA512 HashType = "sha512" - SHA256 HashType = "sha256" - SHA1 HashType = "sha1" - MD5 HashType = "md5" -) - -// Service provides cryptographic functions. -// It is the main entry point for all cryptographic operations -// and is bound to the frontend. -type Service struct { - config *config.Config -} diff --git a/crypt/crypt_test.go b/crypt/crypt_test.go deleted file mode 100644 index 2cde507..0000000 --- a/crypt/crypt_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package crypt - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestHash(t *testing.T) { - s := &Service{} - payload := "hello" - hash := s.Hash(LTHN, payload) - assert.NotEmpty(t, hash) -} - -func TestLuhn(t *testing.T) { - s := &Service{} - assert.True(t, s.Luhn("79927398713")) - assert.False(t, s.Luhn("79927398714")) -} diff --git a/crypt/hash.go b/crypt/hash.go deleted file mode 100644 index 723471d..0000000 --- a/crypt/hash.go +++ /dev/null @@ -1,33 +0,0 @@ -package crypt - -import ( - "crypto/md5" - "crypto/sha1" - "crypto/sha256" - "crypto/sha512" - "encoding/hex" - - "core/crypt/lib/lthn" -) - -// Hash computes a hash of the payload using the specified algorithm. -func (s *Service) Hash(lib HashType, payload string) string { - switch lib { - case LTHN: - return lthn.Hash(payload) - case SHA512: - hash := sha512.Sum512([]byte(payload)) - return hex.EncodeToString(hash[:]) - case SHA1: - hash := sha1.Sum([]byte(payload)) - return hex.EncodeToString(hash[:]) - case MD5: - hash := md5.Sum([]byte(payload)) - return hex.EncodeToString(hash[:]) - case SHA256: - fallthrough - default: - hash := sha256.Sum256([]byte(payload)) - return hex.EncodeToString(hash[:]) - } -} diff --git a/crypt/lib/lthn/hash.go b/crypt/lib/lthn/hash.go deleted file mode 100644 index c9f0ac0..0000000 --- a/crypt/lib/lthn/hash.go +++ /dev/null @@ -1,46 +0,0 @@ -package lthn - -import ( - "crypto/sha256" - "encoding/hex" -) - -// SetKeyMap sets the key map for the notarisation process. -func SetKeyMap(newKeyMap map[rune]rune) { - keyMap = newKeyMap -} - -// GetKeyMap gets the current key map. -func GetKeyMap() map[rune]rune { - return keyMap -} - -// Hash creates a reproducible hash from a string. -func Hash(input string) string { - salt := createSalt(input) - hash := sha256.Sum256([]byte(input + salt)) - return hex.EncodeToString(hash[:]) -} - -// createSalt creates a quasi-salt from a string by reversing it and swapping characters. -func createSalt(input string) string { - if input == "" { - return "" - } - runes := []rune(input) - salt := make([]rune, len(runes)) - for i := 0; i < len(runes); i++ { - char := runes[len(runes)-1-i] - if replacement, ok := keyMap[char]; ok { - salt[i] = replacement - } else { - salt[i] = char - } - } - return string(salt) -} - -// Verify checks if an input string matches a given hash. -func Verifyf(input string, hash string) bool { - return Hash(input) == hash -} diff --git a/crypt/lib/lthn/hash_test.go b/crypt/lib/lthn/hash_test.go deleted file mode 100644 index 463ea5d..0000000 --- a/crypt/lib/lthn/hash_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package lthn - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestHash(t *testing.T) { - input := "test_string" - expectedHash := "45d4027179b17265c38732fb1e7089a0b1adfe1d3ba4105fce66f7d46ba42f7d" - - hashed := Hash(input) - fmt.Printf("Hash for \"%s\": %s\n", input, hashed) - - assert.Equal(t, expectedHash, hashed, "The hash should match the expected value") -} - -func TestCreateSalt(t *testing.T) { - // Test with default keyMap - SetKeyMap(map[rune]rune{}) - assert.Equal(t, "gnirts_tset", createSalt("test_string")) - assert.Equal(t, "", createSalt("")) - assert.Equal(t, "A", createSalt("A")) - - // Test with a custom keyMap - customKeyMap := map[rune]rune{ - 'a': 'x', - 'b': 'y', - 'c': 'z', - } - SetKeyMap(customKeyMap) - assert.Equal(t, "zyx", createSalt("abc")) - assert.Equal(t, "gnirts_tset", createSalt("test_string")) // 'test_string' doesn't have 'a', 'b', 'c' - - // Reset keyMap to default for other tests - SetKeyMap(map[rune]rune{}) -} - -func TestVerify(t *testing.T) { - input := "another_test_string" - hashed := Hash(input) - - assert.True(t, Verifyf(input, hashed), "Verifyf should return true for a matching hash") - assert.False(t, Verifyf(input, "wrong_hash"), "Verifyf should return false for a non-matching hash") - assert.False(t, Verifyf("different_input", hashed), "Verifyf should return false for different input") -} diff --git a/crypt/lib/lthn/lthn.go b/crypt/lib/lthn/lthn.go deleted file mode 100644 index 5a1f6e1..0000000 --- a/crypt/lib/lthn/lthn.go +++ /dev/null @@ -1,16 +0,0 @@ -package lthn - -// keyMap is the default character-swapping map used for the quasi-salting process. -var keyMap = map[rune]rune{ - 'o': '0', - 'l': '1', - 'e': '3', - 'a': '4', - 's': 'z', - 't': '7', - '0': 'o', - '1': 'l', - '3': 'e', - '4': 'a', - '7': 't', -} diff --git a/crypt/lib/openpgp/encrypt.go b/crypt/lib/openpgp/encrypt.go deleted file mode 100644 index 9da18e7..0000000 --- a/crypt/lib/openpgp/encrypt.go +++ /dev/null @@ -1,106 +0,0 @@ -package openpgp - -import ( - "bytes" - "fmt" - "io" - "strings" - - "core/filesystem" - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/armor" -) - -// EncryptPGP encrypts data for a recipient, optionally signing it. -func EncryptPGP(medium filesystem.Medium, recipientPath, data string, signerPath, signerPassphrase *string) (string, error) { - recipient, err := GetPublicKey(medium, recipientPath) - if err != nil { - return "", fmt.Errorf("failed to get recipient public key: %w", err) - } - - var signer *openpgp.Entity - if signerPath != nil && signerPassphrase != nil { - signer, err = GetPrivateKey(medium, *signerPath, *signerPassphrase) - if err != nil { - return "", fmt.Errorf("could not get private key for signing: %w", err) - } - } - - buf := new(bytes.Buffer) - armoredWriter, err := armor.Encode(buf, pgpMessageHeader, nil) - if err != nil { - return "", fmt.Errorf("failed to create armored writer: %w", err) - } - - plaintextWriter, err := openpgp.Encrypt(armoredWriter, []*openpgp.Entity{recipient}, signer, nil, nil) - if err != nil { - return "", fmt.Errorf("failed to encrypt: %w", err) - } - - if _, err := plaintextWriter.Write([]byte(data)); err != nil { - return "", fmt.Errorf("failed to write plaintext data: %w", err) - } - - if err := plaintextWriter.Close(); err != nil { - return "", fmt.Errorf("failed to close plaintext writer: %w", err) - } - if err := armoredWriter.Close(); err != nil { - return "", fmt.Errorf("failed to close armored writer: %w", err) - } - - // Debug print the encrypted message - fmt.Printf("Encrypted Message:\n%s\n", buf.String()) - - return buf.String(), nil -} - -// DecryptPGP decrypts a PGP message, optionally verifying the signature. -func DecryptPGP(medium filesystem.Medium, recipientPath, message, passphrase string, signerPath *string) (string, error) { - privateKeyEntity, err := GetPrivateKey(medium, recipientPath, passphrase) - if err != nil { - return "", fmt.Errorf("failed to get private key: %w", err) - } - - // For this API version, the keyring must contain all keys for decryption and verification. - keyring := openpgp.EntityList{privateKeyEntity} - var expectedSigner *openpgp.Entity - - if signerPath != nil { - publicKeyEntity, err := GetPublicKey(medium, *signerPath) - if err != nil { - return "", fmt.Errorf("could not get public key for verification: %w", err) - } - keyring = append(keyring, publicKeyEntity) - expectedSigner = publicKeyEntity - } - - // Debug print the message before decryption - fmt.Printf("Message to Decrypt:\n%s\n", message) - - // We pass the combined keyring, and nil for the prompt function because the private key is already decrypted. - md, err := openpgp.ReadMessage(strings.NewReader(message), keyring, nil, nil) - if err != nil { - return "", fmt.Errorf("failed to read PGP message: %w", err) - } - - decrypted, err := io.ReadAll(md.UnverifiedBody) - if err != nil { - return "", fmt.Errorf("failed to read decrypted body: %w", err) - } - - // The signature is checked automatically if the public key is in the keyring. - // We still need to check for errors and that the signer was who we expected. - if signerPath != nil { - if md.SignatureError != nil { - return "", fmt.Errorf("signature verification failed: %w", md.SignatureError) - } - if md.SignedBy == nil { - return "", fmt.Errorf("message is not signed, but signature verification was requested") - } - if expectedSigner.PrimaryKey.KeyId != md.SignedBy.PublicKey.KeyId { - return "", fmt.Errorf("signature from unexpected key id: got %X, want %X", md.SignedBy.PublicKey.KeyId, expectedSigner.PrimaryKey.KeyId) - } - } - - return string(decrypted), nil -} diff --git a/crypt/lib/openpgp/key.go b/crypt/lib/openpgp/key.go deleted file mode 100644 index 7173444..0000000 --- a/crypt/lib/openpgp/key.go +++ /dev/null @@ -1,226 +0,0 @@ -package openpgp - -import ( - "bytes" - "crypto" - "fmt" - "path/filepath" - "strings" - "time" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/armor" - "github.com/ProtonMail/go-crypto/openpgp/packet" - "core/crypt/lib/lthn" - "core/filesystem" -) - -// CreateKeyPair generates a new OpenPGP key pair. -// The password parameter is optional. If not provided, the private key will not be encrypted. -func CreateKeyPair(username string, passwords ...string) (*KeyPair, error) { - var password string - if len(passwords) > 0 { - password = passwords[0] - } - - entity, err := openpgp.NewEntity(username, "Lethean Desktop", "", &packet.Config{ - RSABits: 4096, - DefaultHash: crypto.SHA256, - }) - if err != nil { - return nil, fmt.Errorf("failed to create new entity: %w", err) - } - - // The private key is initially unencrypted after NewEntity. - // Generate revocation certificate while the private key is unencrypted. - revocationCert, err := createRevocationCertificate(entity) - if err != nil { - revocationCert = "" // Non-critical, proceed without it if it fails - } - - // Encrypt the private key only if a password is provided, after revocation cert generation. - if password != "" { - if err := entity.PrivateKey.Encrypt([]byte(password)); err != nil { - return nil, fmt.Errorf("failed to encrypt private key: %w", err) - } - } - - publicKey, err := serializeEntity(entity, openpgp.PublicKeyType, "") // Public key doesn't need password - if err != nil { - return nil, err - } - - // Private key serialization. The key is already in its final encrypted/unencrypted state. - privateKey, err := serializeEntity(entity, openpgp.PrivateKeyType, "") // No password needed here for serialization - if err != nil { - return nil, err - } - - return &KeyPair{ - PublicKey: publicKey, - PrivateKey: privateKey, - RevocationCertificate: revocationCert, - }, nil -} - -// CreateServerKeyPair creates and stores a key pair for the server in a specific directory. -func CreateServerKeyPair(keysDir string) error { - serverKeyPath := filepath.Join(keysDir, "server.lthn.pub") - // Passphrase is derived from the path itself, consistent with original logic. - passphrase := lthn.Hash(serverKeyPath) - return createAndStoreKeyPair("server", passphrase, keysDir) -} - -// GetPublicKey retrieves an armored public key for a given ID. -func GetPublicKey(medium filesystem.Medium, path string) (*openpgp.Entity, error) { - return readEntity(medium, path) -} - -// GetPrivateKey retrieves and decrypts an armored private key. -func GetPrivateKey(medium filesystem.Medium, path, passphrase string) (*openpgp.Entity, error) { - entity, err := readEntity(medium, path) - if err != nil { - return nil, err - } - - if entity.PrivateKey == nil { - return nil, fmt.Errorf("no private key found for path %s", path) - } - - if entity.PrivateKey.Encrypted { - if err := entity.PrivateKey.Decrypt([]byte(passphrase)); err != nil { - return nil, fmt.Errorf("failed to decrypt private key for path %s: %w", path, err) - } - } - - var primaryIdentity *openpgp.Identity - for _, identity := range entity.Identities { - if identity.SelfSignature.IsPrimaryId != nil && *identity.SelfSignature.IsPrimaryId { - primaryIdentity = identity - break - } - } - if primaryIdentity == nil { - for _, identity := range entity.Identities { - primaryIdentity = identity - break - } - } - - if primaryIdentity == nil { - return nil, fmt.Errorf("key for %s has no identity", path) - } - - if primaryIdentity.SelfSignature.KeyLifetimeSecs != nil { - if primaryIdentity.SelfSignature.CreationTime.Add(time.Duration(*primaryIdentity.SelfSignature.KeyLifetimeSecs) * time.Second).Before(time.Now()) { - return nil, fmt.Errorf("key for %s has expired", path) - } - } - - return entity, nil -} - -// --- Helper Functions --- - -func createAndStoreKeyPair(id, password, dir string) error { - var keyPair *KeyPair - var err error - - if password != "" { - keyPair, err = CreateKeyPair(id, password) - } else { - keyPair, err = CreateKeyPair(id) - } - - if err != nil { - return fmt.Errorf("failed to create key pair for id %s: %w", id, err) - } - - if err := filesystem.Local.EnsureDir(dir); err != nil { - return fmt.Errorf("failed to ensure key directory exists: %w", err) - } - - files := map[string]string{ - filepath.Join(dir, fmt.Sprintf("%s.lthn.pub", id)): keyPair.PublicKey, - filepath.Join(dir, fmt.Sprintf("%s.lthn.key", id)): keyPair.PrivateKey, - filepath.Join(dir, fmt.Sprintf("%s.lthn.rev", id)): keyPair.RevocationCertificate, // Re-enabled - } - - for path, content := range files { - if content == "" { - continue - } - if err := filesystem.Local.Write(path, content); err != nil { - return fmt.Errorf("failed to write key file %s: %w", path, err) - } - } - return nil -} - -func readEntity(m filesystem.Medium, path string) (*openpgp.Entity, error) { - keyArmored, err := m.Read(path) - if err != nil { - return nil, fmt.Errorf("failed to read key file %s: %w", path, err) - } - - entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(keyArmored)) - if err != nil { - return nil, fmt.Errorf("failed to parse key file %s: %w", path, err) - } - if len(entityList) == 0 { - return nil, fmt.Errorf("no entity found in key file %s", path) - } - return entityList[0], nil -} - -func serializeEntity(entity *openpgp.Entity, keyType string, password string) (string, error) { - buf := new(bytes.Buffer) - writer, err := armor.Encode(buf, keyType, nil) - if err != nil { - return "", fmt.Errorf("failed to create armor encoder: %w", err) - } - - if keyType == openpgp.PrivateKeyType { - // Serialize the private key in its current in-memory state. - // Encryption is handled by CreateKeyPair before this function is called. - err = entity.SerializePrivateWithoutSigning(writer, nil) - } else { - err = entity.Serialize(writer) - } - - if err != nil { - return "", fmt.Errorf("failed to serialize entity: %w", err) - } - if err := writer.Close(); err != nil { - return "", fmt.Errorf("failed to close armor writer: %w", err) - } - return buf.String(), nil -} - -func createRevocationCertificate(entity *openpgp.Entity) (string, error) { - buf := new(bytes.Buffer) - writer, err := armor.Encode(buf, openpgp.SignatureType, nil) - if err != nil { - return "", fmt.Errorf("failed to create armor encoder for revocation: %w", err) - } - - sig := &packet.Signature{ - SigType: packet.SigTypeKeyRevocation, - PubKeyAlgo: entity.PrimaryKey.PubKeyAlgo, - Hash: crypto.SHA256, - CreationTime: time.Now(), - IssuerKeyId: &entity.PrimaryKey.KeyId, - } - - // SignKey requires an unencrypted private key. - if err := sig.SignKey(entity.PrimaryKey, entity.PrivateKey, nil); err != nil { - return "", fmt.Errorf("failed to sign revocation: %w", err) - } - if err := sig.Serialize(writer); err != nil { - return "", fmt.Errorf("failed to serialize revocation signature: %w", err) - } - if err := writer.Close(); err != nil { - return "", fmt.Errorf("failed to close revocation writer: %w", err) - } - return buf.String(), nil -} diff --git a/crypt/lib/openpgp/openpgp.go b/crypt/lib/openpgp/openpgp.go deleted file mode 100644 index 1e604a5..0000000 --- a/crypt/lib/openpgp/openpgp.go +++ /dev/null @@ -1,12 +0,0 @@ -package openpgp - -// pgpMessageHeader is the standard armor header for PGP messages. -const pgpMessageHeader = "PGP MESSAGE" - -// KeyPair holds the generated armored keys and revocation certificate. -// This is the primary data structure representing a user's PGP identity within the system. -type KeyPair struct { - PublicKey string - PrivateKey string - RevocationCertificate string -} diff --git a/crypt/lib/openpgp/sign.go b/crypt/lib/openpgp/sign.go deleted file mode 100644 index 2fd8b90..0000000 --- a/crypt/lib/openpgp/sign.go +++ /dev/null @@ -1,39 +0,0 @@ -package openpgp - -import ( - "bytes" - "fmt" - "strings" - - "core/filesystem" - "github.com/ProtonMail/go-crypto/openpgp" -) - -// Sign creates a detached signature for the data. -func Sign(medium filesystem.Medium, data, privateKeyPath, passphrase string) (string, error) { - signer, err := GetPrivateKey(medium, privateKeyPath, passphrase) - if err != nil { - return "", fmt.Errorf("failed to get private key for signing: %w", err) - } - - buf := new(bytes.Buffer) - if err := openpgp.ArmoredDetachSign(buf, signer, strings.NewReader(data), nil); err != nil { - return "", fmt.Errorf("failed to create detached signature: %w", err) - } - - return buf.String(), nil -} - -// Verify checks a detached signature. -func Verify(medium filesystem.Medium, data, signature, publicKeyPath string) (bool, error) { - keyring, err := GetPublicKey(medium, publicKeyPath) - if err != nil { - return false, fmt.Errorf("failed to get public key for verification: %w", err) - } - - _, err = openpgp.CheckArmoredDetachedSignature(openpgp.EntityList{keyring}, strings.NewReader(data), strings.NewReader(signature), nil) - if err != nil { - return false, fmt.Errorf("signature verification failed: %w", err) - } - return true, nil -} diff --git a/crypt/service.go b/crypt/service.go deleted file mode 100644 index 728cc88..0000000 --- a/crypt/service.go +++ /dev/null @@ -1,43 +0,0 @@ -package crypt - -import ( - "context" - "fmt" - "log" - "path/filepath" - - "core/config" - "core/crypt/lib/openpgp" - "core/filesystem" - "github.com/wailsapp/wails/v3/pkg/application" -) - -// createServerKeyPair is a package-level variable that can be swapped for testing. -var createServerKeyPair = openpgp.CreateServerKeyPair - -// NewService creates a new crypt.Service, accepting a config service instance. -func NewService(cfg *config.Config) *Service { - return &Service{ - config: cfg, - } -} - -// ServiceStartup Startup is called when the app starts. It handles one-time cryptographic setup. -func (s *Service) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { - // Define the directory for server keys based on the central config. - serverKeysDir := filepath.Join(s.config.DataDir, "server_keys") - if err := filesystem.EnsureDir(filesystem.Local, serverKeysDir); err != nil { - return fmt.Errorf("failed to create server keys directory: %w", err) - } - - // Check for server key pair using the configured path. - serverKeyPath := filepath.Join(serverKeysDir, "server.lthn.pub") - if !filesystem.IsFile(filesystem.Local, serverKeyPath) { - log.Println("Creating server key pair...") - if err := createServerKeyPair(serverKeysDir); err != nil { - return fmt.Errorf("failed to create server key pair: %w", err) - } - log.Println("Server key pair created.") - } - return nil -} diff --git a/crypt/sum.go b/crypt/sum.go deleted file mode 100644 index 7453037..0000000 --- a/crypt/sum.go +++ /dev/null @@ -1,77 +0,0 @@ -package crypt - -import ( - "encoding/binary" - "strconv" - "strings" -) - -// Luhn validates a number using the Luhn algorithm. -func (s *Service) Luhn(payload string) bool { - payload = strings.ReplaceAll(payload, " ", "") - sum := 0 - isSecond := false - for i := len(payload) - 1; i >= 0; i-- { - digit, err := strconv.Atoi(string(payload[i])) - if err != nil { - return false // Contains non-digit - } - - if isSecond { - digit = digit * 2 - if digit > 9 { - digit = digit - 9 - } - } - - sum += digit - isSecond = !isSecond - } - return sum%10 == 0 -} - -// Fletcher16 computes the Fletcher-16 checksum. -func (s *Service) Fletcher16(payload string) uint16 { - data := []byte(payload) - var sum1, sum2 uint16 - for _, b := range data { - sum1 = (sum1 + uint16(b)) % 255 - sum2 = (sum2 + sum1) % 255 - } - return (sum2 << 8) | sum1 -} - -// Fletcher32 computes the Fletcher-32 checksum. -func (s *Service) Fletcher32(payload string) uint32 { - data := []byte(payload) - // Pad with 0 to make it even length for uint16 conversion - if len(data)%2 != 0 { - data = append(data, 0) - } - - var sum1, sum2 uint32 - for i := 0; i < len(data); i += 2 { - val := binary.LittleEndian.Uint16(data[i : i+2]) - sum1 = (sum1 + uint32(val)) % 65535 - sum2 = (sum2 + sum1) % 65535 - } - return (sum2 << 16) | sum1 -} - -// Fletcher64 computes the Fletcher-64 checksum. -func (s *Service) Fletcher64(payload string) uint64 { - data := []byte(payload) - // Pad to multiple of 4 - if len(data)%4 != 0 { - padding := 4 - (len(data) % 4) - data = append(data, make([]byte, padding)...) - } - - var sum1, sum2 uint64 - for i := 0; i < len(data); i += 4 { - val := binary.LittleEndian.Uint32(data[i : i+4]) - sum1 = (sum1 + uint64(val)) % 4294967295 - sum2 = (sum2 + sum1) % 4294967295 - } - return (sum2 << 32) | sum1 -} diff --git a/display/apptray.png b/display/apptray.png deleted file mode 100644 index 0778fc6..0000000 Binary files a/display/apptray.png and /dev/null differ diff --git a/display/display.go b/display/display.go deleted file mode 100644 index b50a0c7..0000000 --- a/display/display.go +++ /dev/null @@ -1,32 +0,0 @@ -package display - -import ( - "embed" - - "core/config" - "github.com/wailsapp/wails/v3/pkg/application" -) - -// Brand defines the type for different application brands. -type Brand string - -const ( - AdminHub Brand = "admin-hub" - ServerHub Brand = "server-hub" - GatewayHub Brand = "gateway-hub" - DeveloperHub Brand = "developer-hub" - ClientHub Brand = "client-hub" -) - -// Service manages all OS-level UI interactions (menus, windows, tray). -// It is the main entry point for all display-related operations. -type Service struct { - // --- Injected Dependencies --- - app *application.App - configService *config.Service - - // --- Internal State --- - brand Brand - assets embed.FS - windowHandles map[string]*application.WebviewWindow -} diff --git a/display/menu.go b/display/menu.go deleted file mode 100644 index 69059c1..0000000 --- a/display/menu.go +++ /dev/null @@ -1,32 +0,0 @@ -package display - -import ( - "runtime" - - "github.com/wailsapp/wails/v3/pkg/application" -) - -// buildMenu creates and sets the main application menu. -func (s *Service) buildMenu() { - appMenu := s.app.Menu.New() - if runtime.GOOS == "darwin" { - appMenu.AddRole(application.AppMenu) - } - appMenu.AddRole(application.FileMenu) - appMenu.AddRole(application.ViewMenu) - appMenu.AddRole(application.EditMenu) - - workspace := appMenu.AddSubmenu("Workspace") - workspace.Add("New").OnClick(func(ctx *application.Context) { /* TODO */ }) - workspace.Add("List").OnClick(func(ctx *application.Context) { /* TODO */ }) - - // Add brand-specific menu items - if s.brand == DeveloperHub { - appMenu.AddSubmenu("Developer") - } - - appMenu.AddRole(application.WindowMenu) - appMenu.AddRole(application.HelpMenu) - - s.app.Menu.Set(appMenu) -} diff --git a/display/service.go b/display/service.go deleted file mode 100644 index 5afd490..0000000 --- a/display/service.go +++ /dev/null @@ -1,146 +0,0 @@ -package display - -import ( - "context" - "embed" - "fmt" - - "core/config" - "github.com/wailsapp/wails/v3/pkg/application" - "github.com/wailsapp/wails/v3/pkg/events" -) - -// NewService creates a new DisplayService. -func NewService(brand Brand, assets embed.FS) *Service { - return &Service{ - brand: brand, - assets: assets, - windowHandles: make(map[string]*application.WebviewWindow), - } -} - -// Setup initializes the display service with the application instance and other core services. -func (s *Service) Setup(app *application.App, configService *config.Service, i18nService interface{}) { - s.app = app - s.configService = configService - - s.analyzeScreens() - s.monitorScreenChanges() - s.buildMenu() - s.setupTray() -} - -func (s *Service) analyzeScreens() { - s.app.Logger.Info("Screen analysis", "count", len(s.app.Screen.GetAll())) - - primary := s.app.Screen.GetPrimary() - if primary != nil { - s.app.Logger.Info("Primary screen", - "name", primary.Name, - "size", fmt.Sprintf("%dx%d", primary.Size.Width, primary.Size.Height), - "scaleFactor", primary.ScaleFactor, - "workArea", primary.WorkArea, - ) - scaleFactor := primary.ScaleFactor - - switch { - case scaleFactor == 1.0: - s.app.Logger.Info("Standard DPI display", "screen", primary.Name) - case scaleFactor == 1.25: - s.app.Logger.Info("125% scaled display", "screen", primary.Name) - case scaleFactor == 1.5: - s.app.Logger.Info("150% scaled display", "screen", primary.Name) - case scaleFactor == 2.0: - s.app.Logger.Info("High DPI display (200%)", "screen", primary.Name) - default: - s.app.Logger.Info("Custom scale display", - "screen", primary.Name, - "scale", scaleFactor, - ) - } - } else { - s.app.Logger.Info("No primary screen found") - } - - for i, screen := range s.app.Screen.GetAll() { - s.app.Logger.Info("Screen details", - "index", i, - "name", screen.Name, - "primary", screen.IsPrimary, - "bounds", screen.Bounds, - "scaleFactor", screen.ScaleFactor, - ) - } -} - -func (s *Service) monitorScreenChanges() { - // Monitor for screen configuration changes - s.app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(event *application.ApplicationEvent) { - s.app.Logger.Info("Screen configuration changed") - - // Re-analyze screens - s.app.Logger.Info("Updated screen count", "count", len(s.app.Screen.GetAll())) - - // Could reposition windows here if needed - }) -} - -func (s *Service) ShowEnvironmentDialog() { - envInfo := s.app.Env.Info() - - details := fmt.Sprintf(`Environment Information: - -Operating System: %s -Architecture: %s -Debug Mode: %t - -Dark Mode: %t - -Platform Information:`, - envInfo.OS, - envInfo.Arch, - envInfo.Debug, - s.app.Env.IsDarkMode()) // Use s.app - - // Add platform-specific details - for key, value := range envInfo.PlatformInfo { - details += fmt.Sprintf("\n%s: %v", key, value) - } - - if envInfo.OSInfo != nil { - details += fmt.Sprintf("\n\nOS Details:\nName: %s\nVersion: %s", - envInfo.OSInfo.Name, - envInfo.OSInfo.Version) - } - - dialog := s.app.Dialog.Info() - dialog.SetTitle("Environment Information") - dialog.SetMessage(details) - dialog.Show() -} - -func (s *Service) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { - // Check the IsNew flag from the config service, which is the single source of truth. - if s.configService.Get().IsNew { - // If the config was just created, open the setup window. - s.OpenWindow("main", application.WebviewWindowOptions{ - Title: "Setup", - URL: "#/setup", - }) - } else { - // If the config already existed, open the main window with the default route. - defaultRoute, err := s.configService.Get().Key("DefaultRoute") - if err != nil { - defaultRoute = "/" // Fallback to a safe default if the key is somehow missing. - s.app.Logger.Error("Could not get DefaultRoute from config, using fallback", "error", err) - } - - s.OpenWindow("main", application.WebviewWindowOptions{ - Title: "Core", - Height: 900, - Width: 1280, - URL: "#" + defaultRoute.(string), - }) - } - return nil -} diff --git a/display/tray.go b/display/tray.go deleted file mode 100644 index d01c797..0000000 --- a/display/tray.go +++ /dev/null @@ -1,74 +0,0 @@ -package display - -import ( - "runtime" - - _ "embed" - - "github.com/wailsapp/wails/v3/pkg/application" -) - -//go:embed apptray.png -var appTrayIcon []byte - -// setupTray configures and creates the system tray icon and menu. -func (s *Service) setupTray() { - - systray := s.app.SystemTray.New() - systray.SetTooltip("Lethean Desktop") - - if runtime.GOOS == "darwin" { - systray.SetTemplateIcon(appTrayIcon) - } else { - // Support for light/dark mode icons - systray.SetDarkModeIcon(appTrayIcon) - systray.SetIcon(appTrayIcon) - } - // Create a hidden window for the system tray menu to interact with - trayWindow := s.app.Window.NewWithOptions(application.WebviewWindowOptions{ - Title: "System Tray Status", - URL: "/#/system-tray", - Width: 400, - Frameless: true, - Hidden: true, - }) - systray.AttachWindow(trayWindow).WindowOffset(5) - - // --- Build Tray Menu --- - trayMenu := s.app.Menu.New() - trayMenu.Add("Open Desktop").OnClick(func(ctx *application.Context) { - for _, window := range s.app.Window.GetAll() { - window.Show() - } - }) - trayMenu.Add("Close Desktop").OnClick(func(ctx *application.Context) { - for _, window := range s.app.Window.GetAll() { - window.Hide() - } - }) - - trayMenu.Add("Environment Info").OnClick(func(ctx *application.Context) { - s.ShowEnvironmentDialog() - }) - // Add brand-specific menu items - switch s.brand { - case AdminHub: - trayMenu.Add("Manage Workspace").OnClick(func(ctx *application.Context) { /* TODO */ }) - case ServerHub: - trayMenu.Add("Server Control").OnClick(func(ctx *application.Context) { /* TODO */ }) - case GatewayHub: - trayMenu.Add("Routing Table").OnClick(func(ctx *application.Context) { /* TODO */ }) - case DeveloperHub: - trayMenu.Add("Debug Console").OnClick(func(ctx *application.Context) { /* TODO */ }) - case ClientHub: - trayMenu.Add("Connect").OnClick(func(ctx *application.Context) { /* TODO */ }) - trayMenu.Add("Disconnect").OnClick(func(ctx *application.Context) { /* TODO */ }) - } - - trayMenu.AddSeparator() - trayMenu.Add("Quit").OnClick(func(ctx *application.Context) { - s.app.Quit() - }) - - systray.SetMenu(trayMenu) -} diff --git a/display/window.go b/display/window.go deleted file mode 100644 index d376cb8..0000000 --- a/display/window.go +++ /dev/null @@ -1,28 +0,0 @@ -package display - -import "github.com/wailsapp/wails/v3/pkg/application" - -// OpenWindow creates and shows a new webview window. -// This function is callable from the frontend. -func (s *Service) OpenWindow(name string, options application.WebviewWindowOptions) { - // Check if a window with that name already exists - if window, exists := s.app.Window.GetByName(name); exists { - window.Focus() - return - } - - window := s.app.Window.NewWithOptions(options) - s.windowHandles[name] = window - window.Show() -} - -// SelectDirectory opens a directory selection dialog and returns the selected path. -func (s *Service) SelectDirectory() (string, error) { - dialog := application.OpenFileDialog() - dialog.SetTitle("Select Project Directory") - if path, err := dialog.PromptForSingleSelection(); err == nil { - // Use selected directory path - return path, nil - } - return "", nil -} diff --git a/docs/docs.go b/docs/docs.go deleted file mode 100644 index 88c5efb..0000000 --- a/docs/docs.go +++ /dev/null @@ -1,27 +0,0 @@ -package docs - -import ( - "embed" - - "core/display" - "github.com/wailsapp/wails/v3/pkg/application" -) - -// displayer is an interface that defines the functionality docs needs from a display service. -// This avoids a direct dependency on the display package or the core package. -type displayer interface { - OpenWindow(name string, options application.WebviewWindowOptions) (*application.WebviewWindow, error) -} - -// Service manages the documentation display and serving of assets. -type Service struct { - // --- Injected Dependencies --- - app *application.App - displayService *display.Service // Depends on the local interface, not a concrete type from another package. - - // --- Internal State --- - assets embed.FS -} - -//go:embed all:static/**/* -var docsStatic embed.FS diff --git a/docs/service.go b/docs/service.go deleted file mode 100644 index 1fe41fa..0000000 --- a/docs/service.go +++ /dev/null @@ -1,54 +0,0 @@ -package docs - -import ( - "embed" - "net/http" - "strings" - - "core/display" - "github.com/wailsapp/wails/v3/pkg/application" -) - -// NewService creates a new, un-wired documentation service. -func NewService(assets embed.FS) *Service { - return &Service{ - assets: assets, - } -} - -// Setup injects the required dependencies into the service. -func (s *Service) Setup(app *application.App, displayService *display.Service) { - s.app = app - s.displayService = displayService -} - -// OpenDocsWindow opens a new window with the documentation. -func (s *Service) OpenDocsWindow(path ...string) { - url := "/docs/" - if len(path) > 0 { - fullPath := path[0] - if strings.Contains(fullPath, "#") { - parts := strings.SplitN(fullPath, "#", 2) - pagePath := parts[0] - fragment := parts[1] - url += pagePath + "/#" + fragment - } else { - url += fullPath - } - } - - // Use the injected displayService, which satisfies the local displayer interface. - s.displayService.OpenWindow("docs", application.WebviewWindowOptions{ - Title: "Lethean Documentation", - Height: 600, - Width: 1000, - URL: url, - AlwaysOnTop: true, - Frameless: false, - }) -} - -// ServeHTTP serves the embedded documentation assets. -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { - http.FileServerFS(docsStatic).ServeHTTP(w, r) -} diff --git a/filesystem/client.go b/filesystem/client.go deleted file mode 100644 index 6b22d19..0000000 --- a/filesystem/client.go +++ /dev/null @@ -1,45 +0,0 @@ -package filesystem - -import ( - "core/filesystem/sftp" - "core/filesystem/webdav" -) - -// NewSFTPMedium creates and returns a new SFTP medium. -func NewSFTPMedium(cfg sftp.ConnectionConfig) (Medium, error) { - return sftp.New(cfg) -} - -// NewWebDAVMedium creates and returns a new WebDAV medium. -func NewWebDAVMedium(cfg webdav.ConnectionConfig) (Medium, error) { - return webdav.New(cfg) -} - -// Read retrieves the content of a file from the given medium. -func Read(m Medium, path string) (string, error) { - return m.Read(path) -} - -// Write saves content to a file on the given medium. -func Write(m Medium, path, content string) error { - return m.Write(path, content) -} - -// EnsureDir ensures a directory exists on the given medium. -func EnsureDir(m Medium, path string) error { - return m.EnsureDir(path) -} - -// IsFile checks if a path is a file on the given medium. -func IsFile(m Medium, path string) bool { - return m.IsFile(path) -} - -// Copy copies a file from a source medium to a destination medium. -func Copy(sourceMedium Medium, sourcePath string, destMedium Medium, destPath string) error { - content, err := sourceMedium.Read(sourcePath) - if err != nil { - return err - } - return destMedium.Write(destPath, content) -} diff --git a/filesystem/client_test.go b/filesystem/client_test.go deleted file mode 100644 index 5bf1a5c..0000000 --- a/filesystem/client_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package filesystem - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRead(t *testing.T) { - m := NewMockMedium() - m.Files["test.txt"] = "hello" - content, err := Read(m, "test.txt") - assert.NoError(t, err) - assert.Equal(t, "hello", content) -} - -func TestWrite(t *testing.T) { - m := NewMockMedium() - err := Write(m, "test.txt", "hello") - assert.NoError(t, err) - assert.Equal(t, "hello", m.Files["test.txt"]) -} - -func TestCopy(t *testing.T) { - source := NewMockMedium() - dest := NewMockMedium() - source.Files["test.txt"] = "hello" - err := Copy(source, "test.txt", dest, "test.txt") - assert.NoError(t, err) - assert.Equal(t, "hello", dest.Files["test.txt"]) -} diff --git a/filesystem/filesystem.go b/filesystem/filesystem.go deleted file mode 100644 index a9beecd..0000000 --- a/filesystem/filesystem.go +++ /dev/null @@ -1,29 +0,0 @@ -package filesystem - -import () - -// Medium defines the standard interface for a storage backend. -// This allows for different implementations (e.g., local disk, S3, SFTP) -// to be used interchangeably. -type Medium interface { - // Read retrieves the content of a file as a string. - Read(path string) (string, error) - - // Write saves the given content to a file, overwriting it if it exists. - Write(path, content string) error - - // EnsureDir makes sure a directory exists, creating it if necessary. - EnsureDir(path string) error - - // IsFile checks if a path exists and is a regular file. - IsFile(path string) bool - - // FileGet is a convenience function that reads a file from the medium. - FileGet(path string) (string, error) - - // FileSet is a convenience function that writes a file to the medium. - FileSet(path, content string) error -} - -// Pre-initialized, sandboxed medium for the local filesystem. -var Local Medium diff --git a/filesystem/filesystem_test.go b/filesystem/filesystem_test.go deleted file mode 100644 index 8d6e6ae..0000000 --- a/filesystem/filesystem_test.go +++ /dev/null @@ -1,3 +0,0 @@ -package filesystem - -import () diff --git a/filesystem/local/client.go b/filesystem/local/client.go deleted file mode 100644 index 0efe171..0000000 --- a/filesystem/local/client.go +++ /dev/null @@ -1,83 +0,0 @@ -package local - -import ( - "fmt" - "os" - "path/filepath" - "strings" -) - -// New creates a new instance of the local storage medium. -// It requires a root path to sandbox all file operations. -func New(rootPath string) (*Medium, error) { - if err := os.MkdirAll(rootPath, os.ModePerm); err != nil { - return nil, fmt.Errorf("could not create root directory at %s: %w", rootPath, err) - } - return &Medium{root: rootPath}, nil -} - -// path returns a full, safe path within the medium's root. -func (m *Medium) path(subpath string) (string, error) { - if strings.Contains(subpath, "..") { - return "", fmt.Errorf("path traversal attempt detected") - } - return filepath.Join(m.root, subpath), nil -} - -// Read retrieves the content of a file from the local disk. -func (m *Medium) Read(path string) (string, error) { - safePath, err := m.path(path) - if err != nil { - return "", err - } - data, err := os.ReadFile(safePath) - if err != nil { - return "", err - } - return string(data), nil -} - -// Write saves the given content to a file on the local disk. -func (m *Medium) Write(path, content string) error { - safePath, err := m.path(path) - if err != nil { - return err - } - dir := filepath.Dir(safePath) - if err := os.MkdirAll(dir, os.ModePerm); err != nil { - return err - } - return os.WriteFile(safePath, []byte(content), 0644) -} - -// EnsureDir makes sure a directory exists on the local disk. -func (m *Medium) EnsureDir(path string) error { - safePath, err := m.path(path) - if err != nil { - return err - } - return os.MkdirAll(safePath, os.ModePerm) -} - -// IsFile checks if a path exists and is a regular file on the local disk. -func (m *Medium) IsFile(path string) bool { - safePath, err := m.path(path) - if err != nil { - return false - } - info, err := os.Stat(safePath) - if os.IsNotExist(err) { - return false - } - return !info.IsDir() -} - -// FileGet is a convenience function that reads a file from the medium. -func (m *Medium) FileGet(path string) (string, error) { - return m.Read(path) -} - -// FileSet is a convenience function that writes a file to the medium. -func (m *Medium) FileSet(path, content string) error { - return m.Write(path, content) -} diff --git a/filesystem/local/client_test.go b/filesystem/local/client_test.go deleted file mode 100644 index ff3dce7..0000000 --- a/filesystem/local/client_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package local - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNew(t *testing.T) { - // Create a temporary directory for testing - testRoot, err := os.MkdirTemp("", "local_test_root") - assert.NoError(t, err) - defer os.RemoveAll(testRoot) // Clean up after the test - - // Test successful creation - medium, err := New(testRoot) - assert.NoError(t, err) - assert.NotNil(t, medium) - assert.Equal(t, testRoot, medium.root) - - // Verify the root directory exists - info, err := os.Stat(testRoot) - assert.NoError(t, err) - assert.True(t, info.IsDir()) - - // Test creating a new instance with an existing directory (should not error) - medium2, err := New(testRoot) - assert.NoError(t, err) - assert.NotNil(t, medium2) -} - -func TestPath(t *testing.T) { - testRoot := "/tmp/test_root" - medium := &Medium{root: testRoot} - - // Valid path - validPath, err := medium.path("file.txt") - assert.NoError(t, err) - assert.Equal(t, filepath.Join(testRoot, "file.txt"), validPath) - - // Subdirectory path - subDirPath, err := medium.path("dir/sub/file.txt") - assert.NoError(t, err) - assert.Equal(t, filepath.Join(testRoot, "dir", "sub", "file.txt"), subDirPath) - - // Path traversal attempt - _, err = medium.path("../secret.txt") - assert.Error(t, err) - assert.Contains(t, err.Error(), "path traversal attempt detected") - - _, err = medium.path("dir/../../secret.txt") - assert.Error(t, err) - assert.Contains(t, err.Error(), "path traversal attempt detected") -} - -func TestReadWrite(t *testing.T) { - testRoot, err := os.MkdirTemp("", "local_read_write_test") - assert.NoError(t, err) - defer os.RemoveAll(testRoot) - - medium, err := New(testRoot) - assert.NoError(t, err) - - fileName := "testfile.txt" - filePath := filepath.Join("subdir", fileName) - content := "Hello, Gopher!\nThis is a test file." - - // Test Write - err = medium.Write(filePath, content) - assert.NoError(t, err) - - // Verify file content by reading directly from OS - readContent, err := os.ReadFile(filepath.Join(testRoot, filePath)) - assert.NoError(t, err) - assert.Equal(t, content, string(readContent)) - - // Test Read - readByMedium, err := medium.Read(filePath) - assert.NoError(t, err) - assert.Equal(t, content, readByMedium) - - // Test Read non-existent file - _, err = medium.Read("nonexistent.txt") - assert.Error(t, err) - assert.True(t, os.IsNotExist(err)) - - // Test Write to a path with traversal attempt - writeErr := medium.Write("../badfile.txt", "malicious content") - assert.Error(t, writeErr) - assert.Contains(t, writeErr.Error(), "path traversal attempt detected") -} - -func TestEnsureDir(t *testing.T) { - testRoot, err := os.MkdirTemp("", "local_ensure_dir_test") - assert.NoError(t, err) - defer os.RemoveAll(testRoot) - - medium, err := New(testRoot) - assert.NoError(t, err) - - dirName := "newdir/subdir" - dirPath := filepath.Join(testRoot, dirName) - - // Test creating a new directory - err = medium.EnsureDir(dirName) - assert.NoError(t, err) - info, err := os.Stat(dirPath) - assert.NoError(t, err) - assert.True(t, info.IsDir()) - - // Test ensuring an existing directory (should not error) - err = medium.EnsureDir(dirName) - assert.NoError(t, err) - - // Test ensuring a directory with path traversal attempt - err = medium.EnsureDir("../bad_dir") - assert.Error(t, err) - assert.Contains(t, err.Error(), "path traversal attempt detected") -} - -func TestIsFile(t *testing.T) { - testRoot, err := os.MkdirTemp("", "local_is_file_test") - assert.NoError(t, err) - defer os.RemoveAll(testRoot) - - medium, err := New(testRoot) - assert.NoError(t, err) - - // Create a test file - fileName := "existing_file.txt" - filePath := filepath.Join(testRoot, fileName) - err = os.WriteFile(filePath, []byte("content"), 0644) - assert.NoError(t, err) - - // Create a test directory - dirName := "existing_dir" - dirPath := filepath.Join(testRoot, dirName) - err = os.Mkdir(dirPath, 0755) - assert.NoError(t, err) - - // Test with an existing file - assert.True(t, medium.IsFile(fileName)) - - // Test with a non-existent file - assert.False(t, medium.IsFile("nonexistent_file.txt")) - - // Test with a directory - assert.False(t, medium.IsFile(dirName)) - - // Test with path traversal attempt - assert.False(t, medium.IsFile("../bad_file.txt")) -} diff --git a/filesystem/local/local.go b/filesystem/local/local.go deleted file mode 100644 index 61f2447..0000000 --- a/filesystem/local/local.go +++ /dev/null @@ -1,6 +0,0 @@ -package local - -// Medium implements the filesystem.Medium interface for the local disk. -type Medium struct { - root string -} diff --git a/filesystem/mock.go b/filesystem/mock.go deleted file mode 100644 index e97327b..0000000 --- a/filesystem/mock.go +++ /dev/null @@ -1,47 +0,0 @@ -package filesystem - -import "github.com/stretchr/testify/assert" - -// MockMedium implements the Medium interface for testing purposes. -type MockMedium struct { - Files map[string]string - Dirs map[string]bool -} - -func NewMockMedium() *MockMedium { - return &MockMedium{ - Files: make(map[string]string), - Dirs: make(map[string]bool), - } -} - -func (m *MockMedium) Read(path string) (string, error) { - content, ok := m.Files[path] - if !ok { - return "", assert.AnError // Simulate file not found error - } - return content, nil -} - -func (m *MockMedium) Write(path, content string) error { - m.Files[path] = content - return nil -} - -func (m *MockMedium) EnsureDir(path string) error { - m.Dirs[path] = true - return nil -} - -func (m *MockMedium) IsFile(path string) bool { - _, ok := m.Files[path] - return ok -} - -func (m *MockMedium) FileGet(path string) (string, error) { - return m.Read(path) -} - -func (m *MockMedium) FileSet(path, content string) error { - return m.Write(path, content) -} diff --git a/filesystem/sftp/client.go b/filesystem/sftp/client.go deleted file mode 100644 index a745a90..0000000 --- a/filesystem/sftp/client.go +++ /dev/null @@ -1,125 +0,0 @@ -package sftp - -import ( - "fmt" - "io" - "net" - "os" - "path/filepath" - - "github.com/pkg/sftp" - "github.com/skeema/knownhosts" - "golang.org/x/crypto/ssh" -) - -// New creates a new, connected instance of the SFTP storage medium. -func New(cfg ConnectionConfig) (*Medium, error) { - var authMethods []ssh.AuthMethod - - if cfg.KeyFile != "" { - key, err := os.ReadFile(cfg.KeyFile) - if err != nil { - return nil, fmt.Errorf("unable to read private key: %w", err) - } - signer, err := ssh.ParsePrivateKey(key) - if err != nil { - return nil, fmt.Errorf("unable to parse private key: %w", err) - } - authMethods = append(authMethods, ssh.PublicKeys(signer)) - } else if cfg.Password != "" { - authMethods = append(authMethods, ssh.Password(cfg.Password)) - } else { - return nil, fmt.Errorf("no authentication method provided (password or keyfile)") - } - - kh, err := knownhosts.New(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts")) - if err != nil { - return nil, fmt.Errorf("failed to read known_hosts: %w", err) - } - - sshConfig := &ssh.ClientConfig{ - User: cfg.User, - Auth: authMethods, - HostKeyCallback: kh.HostKeyCallback(), - } - - addr := net.JoinHostPort(cfg.Host, cfg.Port) - conn, err := ssh.Dial("tcp", addr, sshConfig) - if err != nil { - return nil, fmt.Errorf("failed to dial ssh: %w", err) - } - - sftpClient, err := sftp.NewClient(conn) - if err != nil { - // Ensure the underlying ssh connection is closed on failure - conn.Close() - return nil, fmt.Errorf("failed to create sftp client: %w", err) - } - - return &Medium{client: sftpClient}, nil -} - -// Read retrieves the content of a file from the SFTP server. -func (m *Medium) Read(path string) (string, error) { - file, err := m.client.Open(path) - if err != nil { - return "", fmt.Errorf("sftp: failed to open file %s: %w", path, err) - } - defer file.Close() - - data, err := io.ReadAll(file) - if err != nil { - return "", fmt.Errorf("sftp: failed to read file %s: %w", path, err) - } - - return string(data), nil -} - -// Write saves the given content to a file on the SFTP server. -func (m *Medium) Write(path, content string) error { - // Ensure the remote directory exists first. - dir := filepath.Dir(path) - if err := m.EnsureDir(dir); err != nil { - return err - } - - file, err := m.client.Create(path) - if err != nil { - return fmt.Errorf("sftp: failed to create file %s: %w", path, err) - } - defer file.Close() - - if _, err := file.Write([]byte(content)); err != nil { - return fmt.Errorf("sftp: failed to write to file %s: %w", path, err) - } - - return nil -} - -// EnsureDir makes sure a directory exists on the SFTP server. -func (m *Medium) EnsureDir(path string) error { - // MkdirAll is idempotent, so it won't error if the path already exists. - return m.client.MkdirAll(path) -} - -// IsFile checks if a path exists and is a regular file on the SFTP server. -func (m *Medium) IsFile(path string) bool { - info, err := m.client.Stat(path) - if err != nil { - // If the error is "not found", it's definitely not a file. - // For any other error, we also conservatively say it's not a file. - return false - } - // Return true only if it's not a directory. - return !info.IsDir() -} - -// FileGet is a convenience function that reads a file from the medium. -func (m *Medium) FileGet(path string) (string, error) { - return m.Read(path) -} - -// FileSet is a convenience function that writes a file to the medium. -func (m *Medium) FileSet(path, content string) error { - return m.Write(path, content) -} diff --git a/filesystem/sftp/sftp.go b/filesystem/sftp/sftp.go deleted file mode 100644 index cf9e2e1..0000000 --- a/filesystem/sftp/sftp.go +++ /dev/null @@ -1,19 +0,0 @@ -package sftp - -import ( - "github.com/pkg/sftp" -) - -// Medium implements the filesystem.Medium interface for the SFTP protocol. -type Medium struct { - client *sftp.Client -} - -// ConnectionConfig holds the necessary details to connect to an SFTP server. -type ConnectionConfig struct { - Host string - Port string - User string - Password string // For password-based auth - KeyFile string // Path to a private key for key-based auth -} diff --git a/filesystem/webdav/client.go b/filesystem/webdav/client.go deleted file mode 100644 index 7ed4f74..0000000 --- a/filesystem/webdav/client.go +++ /dev/null @@ -1,16 +0,0 @@ -package webdav - -import "net/http" - -// Medium implements the filesystem.Medium interface for the WebDAV protocol. -type Medium struct { - client *http.Client - baseURL string // e.g., https://dav.example.com/remote.php/dav/files/username/ -} - -// ConnectionConfig holds the necessary details to connect to a WebDAV server. -type ConnectionConfig struct { - URL string // The full base URL of the WebDAV share. - User string - Password string -} diff --git a/filesystem/webdav/webdav.go b/filesystem/webdav/webdav.go deleted file mode 100644 index db0ac66..0000000 --- a/filesystem/webdav/webdav.go +++ /dev/null @@ -1,183 +0,0 @@ -package webdav - -import ( - "bytes" - _ "context" - "fmt" - "io" - "net/http" - "path" - "strings" -) - -// New creates a new, connected instance of the WebDAV storage medium. -func New(cfg ConnectionConfig) (*Medium, error) { - transport := &authTransport{ - Username: cfg.User, - Password: cfg.Password, - Wrapped: http.DefaultTransport, - } - - httpClient := &http.Client{Transport: transport} - - // Ping the server to ensure the connection and credentials are valid. - // We do a PROPFIND on the root, which is a standard WebDAV operation. - req, err := http.NewRequest("PROPFIND", cfg.URL, nil) - if err != nil { - return nil, fmt.Errorf("webdav: failed to create ping request: %w", err) - } - req.Header.Set("Depth", "0") - resp, err := httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("webdav: connection test failed: %w", err) - } - resp.Body.Close() - if resp.StatusCode != http.StatusMultiStatus && resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("webdav: connection test failed with status %s", resp.Status) - } - - return &Medium{ - client: httpClient, - baseURL: cfg.URL, - }, nil -} - -// Read retrieves the content of a file from the WebDAV server. -func (m *Medium) Read(p string) (string, error) { - url := m.resolveURL(p) - resp, err := m.client.Get(url) - if err != nil { - return "", fmt.Errorf("webdav: GET request for %s failed: %w", p, err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("webdav: failed to read %s, status: %s", p, resp.Status) - } - - data, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("webdav: failed to read response body for %s: %w", p, err) - } - - return string(data), nil -} - -// Write saves the given content to a file on the WebDAV server. -func (m *Medium) Write(p, content string) error { - // Ensure the parent directory exists first. - dir := path.Dir(p) - if dir != "." && dir != "/" { - if err := m.EnsureDir(dir); err != nil { - return err // This will be a detailed error from EnsureDir - } - } - - url := m.resolveURL(p) - req, err := http.NewRequest("PUT", url, bytes.NewReader([]byte(content))) - if err != nil { - return fmt.Errorf("webdav: failed to create PUT request: %w", err) - } - - resp, err := m.client.Do(req) - if err != nil { - return fmt.Errorf("webdav: PUT request for %s failed: %w", p, err) - } - defer resp.Body.Close() - - // StatusCreated (201) or StatusNoContent (204) are success codes for PUT. - if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent { - return fmt.Errorf("webdav: failed to write %s, status: %s", p, resp.Status) - } - - return nil -} - -// EnsureDir makes sure a directory exists on the WebDAV server, creating parent dirs as needed. -func (m *Medium) EnsureDir(p string) error { - // To mimic MkdirAll, we create each part of the path sequentially. - parts := strings.Split(p, "/") - currentPath := "" - for _, part := range parts { - if part == "" { - continue - } - currentPath = path.Join(currentPath, part) - url := m.resolveURL(currentPath) + "/" // MKCOL needs a trailing slash - - req, err := http.NewRequest("MKCOL", url, nil) - if err != nil { - return fmt.Errorf("webdav: failed to create MKCOL request for %s: %w", currentPath, err) - } - - resp, err := m.client.Do(req) - if err != nil { - return fmt.Errorf("webdav: MKCOL request for %s failed: %w", currentPath, err) - } - resp.Body.Close() - - // 405 Method Not Allowed means it already exists, which is fine for us. - // 201 Created is a success. - if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusMethodNotAllowed { - return fmt.Errorf("webdav: failed to create directory %s, status: %s", currentPath, resp.Status) - } - } - return nil -} - -// IsFile checks if a path exists and is a regular file on the WebDAV server. -func (m *Medium) IsFile(p string) bool { - url := m.resolveURL(p) - req, err := http.NewRequest("PROPFIND", url, nil) - if err != nil { - return false - } - req.Header.Set("Depth", "0") - - resp, err := m.client.Do(req) - if err != nil { - return false - } - defer resp.Body.Close() - - // If we get anything other than a Multi-Status, it's probably not a file. - if resp.StatusCode != http.StatusMultiStatus { - return false - } - - // A simple check: if the response body contains the string for a collection, it's a directory. - // A more robust implementation would parse the XML response. - body, err := io.ReadAll(resp.Body) - if err != nil { - return false - } - - return !strings.Contains(string(body), "") -} - -// resolveURL joins the base URL with a path segment, ensuring correct slashes. -func (m *Medium) resolveURL(p string) string { - return strings.TrimSuffix(m.baseURL, "/") + "/" + strings.TrimPrefix(p, "/") -} - -// authTransport is a custom http.RoundTripper to inject Basic Auth. -type authTransport struct { - Username string - Password string - Wrapped http.RoundTripper -} - -func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) { - req.SetBasicAuth(t.Username, t.Password) - return t.Wrapped.RoundTrip(req) -} - -// FileGet is a convenience function that reads a file from the medium. -func (m *Medium) FileGet(path string) (string, error) { - return m.Read(path) -} - -// FileSet is a convenience function that writes a file to the medium. -func (m *Medium) FileSet(path, content string) error { - return m.Write(path, content) -} diff --git a/go.mod b/go.mod index d968f16..2b891e4 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,7 @@ module github.com/host-uk/core go 1.25.5 require ( -<<<<<<< HEAD code.gitea.io/sdk/gitea v0.23.2 -======= ->>>>>>> fix/consolidate-workflows github.com/Snider/Borg v0.2.0 github.com/getkin/kin-openapi v0.133.0 github.com/host-uk/core/internal/core-ide v0.0.0-20260204004957-989b7e1e6555 @@ -66,11 +63,8 @@ require ( github.com/ebitengine/purego v0.9.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/fatih/color v1.18.0 // indirect -<<<<<<< HEAD github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-fed/httpsig v1.1.0 // indirect -======= ->>>>>>> fix/consolidate-workflows github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.7.0 // indirect github.com/go-git/go-git/v5 v5.16.4 // indirect diff --git a/go.sum b/go.sum index 12837ea..5799357 100644 --- a/go.sum +++ b/go.sum @@ -16,17 +16,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= -<<<<<<< HEAD github.com/Snider/Borg v0.2.0 h1:iCyDhY4WTXi39+FexRwXbn2YpZ2U9FUXVXDZk9xRCXQ= github.com/Snider/Borg v0.2.0/go.mod h1:TqlKnfRo9okioHbgrZPfWjQsztBV0Nfskz4Om1/vdMY= -======= -github.com/Snider/Borg v0.1.0 h1:tLvrytPMIM2To0xByYP+KHLcT9pg9P9y9uRTyG6r9oc= -github.com/Snider/Borg v0.1.0/go.mod h1:0GMzdXYzdFZpR25IFne7ErqV/YFQHsX1THm1BbncMPo= -github.com/Snider/Borg v0.2.0 h1:iCyDhY4WTXi39+FexRwXbn2YpZ2U9FUXVXDZk9xRCXQ= -github.com/Snider/Borg v0.2.0/go.mod h1:TqlKnfRo9okioHbgrZPfWjQsztBV0Nfskz4Om1/vdMY= -github.com/Snider/Enchantrix v0.0.2 h1:ExZQiBhfS/p/AHFTKhY80TOd+BXZjK95EzByAEgwvjs= -github.com/Snider/Enchantrix v0.0.2/go.mod h1:CtFcLAvnDT1KcuF1JBb/DJj0KplY8jHryO06KzQ1hsQ= ->>>>>>> fix/consolidate-workflows github.com/TwiN/go-color v1.4.1 h1:mqG0P/KBgHKVqmtL5ye7K0/Gr4l6hTksPgTgMk3mUzc= github.com/TwiN/go-color v1.4.1/go.mod h1:WcPf/jtiW95WBIsEeY1Lc/b8aaWoiqQpu5cf8WFxu+s= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= @@ -88,11 +79,8 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -<<<<<<< HEAD github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -======= ->>>>>>> fix/consolidate-workflows github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= @@ -327,11 +315,7 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -<<<<<<< HEAD golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -======= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= ->>>>>>> fix/consolidate-workflows golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= @@ -341,10 +325,6 @@ golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHi golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -<<<<<<< HEAD -======= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= ->>>>>>> fix/consolidate-workflows golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= @@ -373,10 +353,6 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -<<<<<<< HEAD -======= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= ->>>>>>> fix/consolidate-workflows golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= diff --git a/internal/cmd/dev/cmd_apply.go b/internal/cmd/dev/cmd_apply.go index 52d6f33..e3655b0 100644 --- a/internal/cmd/dev/cmd_apply.go +++ b/internal/cmd/dev/cmd_apply.go @@ -15,11 +15,7 @@ import ( "strings" "github.com/host-uk/core/pkg/cli" -<<<<<<< HEAD - errors "github.com/host-uk/core/pkg/framework/core" -======= core "github.com/host-uk/core/pkg/framework/core" ->>>>>>> fix/consolidate-workflows "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/io" diff --git a/internal/cmd/dev/service.go b/internal/cmd/dev/service.go index 167410c..8c03569 100644 --- a/internal/cmd/dev/service.go +++ b/internal/cmd/dev/service.go @@ -174,7 +174,6 @@ func (s *Service) runWork(task TaskWork) error { cli.Print(" %s: %d commits\n", st.Name, st.Ahead) } -<<<<<<< HEAD if !task.AutoPush { cli.Blank() cli.Print("Push all? [y/N] ") @@ -184,15 +183,6 @@ func (s *Service) runWork(task TaskWork) error { cli.Println("Aborted") return nil } -======= - cli.Blank() - cli.Print("Push all? [y/N] ") - var answer string - _, _ = cli.Scanln(&answer) - if strings.ToLower(answer) != "y" { - cli.Println("Aborted") - return nil ->>>>>>> fix/consolidate-workflows } cli.Blank() diff --git a/internal/cmd/go/cmd_gotest.go b/internal/cmd/go/cmd_gotest.go index b7f7532..acc8af8 100644 --- a/internal/cmd/go/cmd_gotest.go +++ b/internal/cmd/go/cmd_gotest.go @@ -212,7 +212,6 @@ func addGoCovCommand(parent *cli.Command) { } covPath := covFile.Name() _ = covFile.Close() -<<<<<<< HEAD defer func() { if covOutput == "" { _ = os.Remove(covPath) @@ -228,9 +227,6 @@ func addGoCovCommand(parent *cli.Command) { _ = os.Remove(covPath) } }() -======= - defer func() { _ = os.Remove(covPath) }() ->>>>>>> fix/consolidate-workflows cli.Print("%s %s\n", dimStyle.Render(i18n.Label("coverage")), i18n.ProgressSubject("run", "tests")) // Truncate package list if too long for display @@ -273,11 +269,7 @@ func addGoCovCommand(parent *cli.Command) { parts := strings.Fields(lastLine) if len(parts) >= 3 { covStr := strings.TrimSuffix(parts[len(parts)-1], "%") -<<<<<<< HEAD _, _ = fmt.Sscanf(covStr, "%f", &statementCov) -======= - _, _ = fmt.Sscanf(covStr, "%f", &totalCov) ->>>>>>> fix/consolidate-workflows } } } diff --git a/internal/cmd/php/cmd_qa_runner.go b/internal/cmd/php/cmd_qa_runner.go index 3f51ff2..69c8a6e 100644 --- a/internal/cmd/php/cmd_qa_runner.go +++ b/internal/cmd/php/cmd_qa_runner.go @@ -150,11 +150,7 @@ func (r *QARunner) buildSpec(check string) *process.RunSpec { phpunitBin := filepath.Join(r.dir, "vendor", "bin", "phpunit") var cmd string -<<<<<<< HEAD if m.IsFile(pestBin) { -======= - if _, err := os.Stat(pestBin); err == nil { ->>>>>>> fix/consolidate-workflows cmd = pestBin } else if m.IsFile(phpunitBin) { cmd = phpunitBin diff --git a/internal/cmd/php/detect.go b/internal/cmd/php/detect.go index 0d62a75..c13da9d 100644 --- a/internal/cmd/php/detect.go +++ b/internal/cmd/php/detect.go @@ -174,10 +174,6 @@ func needsRedis(dir string) bool { if err != nil { return false } -<<<<<<< HEAD -======= - defer func() { _ = file.Close() }() ->>>>>>> fix/consolidate-workflows lines := strings.Split(content, "\n") for _, line := range lines { @@ -242,10 +238,6 @@ func GetLaravelAppName(dir string) string { if err != nil { return "" } -<<<<<<< HEAD -======= - defer func() { _ = file.Close() }() ->>>>>>> fix/consolidate-workflows lines := strings.Split(content, "\n") for _, line := range lines { @@ -269,10 +261,6 @@ func GetLaravelAppURL(dir string) string { if err != nil { return "" } -<<<<<<< HEAD -======= - defer func() { _ = file.Close() }() ->>>>>>> fix/consolidate-workflows lines := strings.Split(content, "\n") for _, line := range lines { diff --git a/main.go b/main.go deleted file mode 100644 index 1a85275..0000000 --- a/main.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import ( - "github.com/host-uk/core/pkg/cli" - - // Build variants import commands via self-registration. - // See internal/variants/ for available variants: full, ci, php, minimal. - _ "github.com/host-uk/core/internal/variants" -) - -func main() { - cli.Main() -} diff --git a/pkg/agentic/config.go b/pkg/agentic/config.go index 0ce973d..c621b08 100644 --- a/pkg/agentic/config.go +++ b/pkg/agentic/config.go @@ -99,10 +99,6 @@ func loadEnvFile(path string, cfg *Config) error { if err != nil { return err } -<<<<<<< HEAD -======= - defer func() { _ = file.Close() }() ->>>>>>> fix/consolidate-workflows for _, line := range strings.Split(content, "\n") { line = strings.TrimSpace(line) diff --git a/pkg/build/signing/codesign.go b/pkg/build/signing/codesign.go index 16c0ea8..11581c7 100644 --- a/pkg/build/signing/codesign.go +++ b/pkg/build/signing/codesign.go @@ -74,11 +74,7 @@ func (s *MacOSSigner) Notarize(ctx context.Context, fs io.Medium, binary string) if output, err := zipCmd.CombinedOutput(); err != nil { return fmt.Errorf("codesign.Notarize: failed to create zip: %w\nOutput: %s", err, string(output)) } -<<<<<<< HEAD defer func() { _ = fs.Delete(zipPath) }() -======= - defer func() { _ = os.Remove(zipPath) }() ->>>>>>> fix/consolidate-workflows // Submit to Apple and wait submitCmd := exec.CommandContext(ctx, "xcrun", "notarytool", "submit", diff --git a/pkg/config/config.go b/pkg/config/config.go index 5fdeb65..67ede68 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -13,15 +13,11 @@ package config import ( "fmt" "os" -<<<<<<< HEAD "path/filepath" -======= ->>>>>>> fix/consolidate-workflows "strings" "sync" core "github.com/host-uk/core/pkg/framework/core" -<<<<<<< HEAD coreio "github.com/host-uk/core/pkg/io" "github.com/spf13/viper" "gopkg.in/yaml.v3" @@ -34,29 +30,13 @@ type Config struct { v *viper.Viper medium coreio.Medium path string -======= - "github.com/host-uk/core/pkg/io" -) - -// Config implements the core.Config interface with layered resolution. -// Values are resolved in order: defaults -> file -> env -> flags. -type Config struct { - mu sync.RWMutex - medium io.Medium - path string - data map[string]any ->>>>>>> fix/consolidate-workflows } // Option is a functional option for configuring a Config instance. type Option func(*Config) // WithMedium sets the storage medium for configuration file operations. -<<<<<<< HEAD func WithMedium(m coreio.Medium) Option { -======= -func WithMedium(m io.Medium) Option { ->>>>>>> fix/consolidate-workflows return func(c *Config) { c.medium = m } @@ -69,7 +49,6 @@ func WithPath(path string) Option { } } -<<<<<<< HEAD // WithEnvPrefix sets the prefix for environment variables. func WithEnvPrefix(prefix string) Option { return func(c *Config) { @@ -77,14 +56,11 @@ func WithEnvPrefix(prefix string) Option { } } -======= ->>>>>>> fix/consolidate-workflows // New creates a new Config instance with the given options. // If no medium is provided, it defaults to io.Local. // If no path is provided, it defaults to ~/.core/config.yaml. func New(opts ...Option) (*Config, error) { c := &Config{ -<<<<<<< HEAD v: viper.New(), } @@ -92,21 +68,12 @@ func New(opts ...Option) (*Config, error) { c.v.SetEnvPrefix("CORE_CONFIG") c.v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) -======= - data: make(map[string]any), - } - ->>>>>>> fix/consolidate-workflows for _, opt := range opts { opt(c) } if c.medium == nil { -<<<<<<< HEAD c.medium = coreio.Local -======= - c.medium = io.Local ->>>>>>> fix/consolidate-workflows } if c.path == "" { @@ -114,7 +81,6 @@ func New(opts ...Option) (*Config, error) { if err != nil { return nil, core.E("config.New", "failed to determine home directory", err) } -<<<<<<< HEAD c.path = filepath.Join(home, ".core", "config.yaml") } @@ -125,30 +91,11 @@ func New(opts ...Option) (*Config, error) { if err := c.LoadFile(c.medium, c.path); err != nil { return nil, core.E("config.New", "failed to load config file", err) } -======= - c.path = home + "/.core/config.yaml" - } - - // Load existing config file if it exists - if c.medium.IsFile(c.path) { - loaded, err := Load(c.medium, c.path) - if err != nil { - return nil, core.E("config.New", "failed to load config file", err) - } - c.data = loaded - } - - // Overlay environment variables - envData := LoadEnv("CORE_CONFIG_") - for k, v := range envData { - setNested(c.data, k, v) ->>>>>>> fix/consolidate-workflows } return c, nil } -<<<<<<< HEAD // LoadFile reads a configuration file from the given medium and path and merges it into the current config. // It supports YAML and environment files (.env). func (c *Config) LoadFile(m coreio.Medium, path string) error { @@ -179,16 +126,10 @@ func (c *Config) LoadFile(m coreio.Medium, path string) error { // Get retrieves a configuration value by dot-notation key and stores it in out. // If key is empty, it unmarshals the entire configuration into out. // The out parameter must be a pointer to the target type. -======= -// Get retrieves a configuration value by dot-notation key and stores it in out. -// The out parameter must be a pointer to the target type. -// Returns an error if the key is not found. ->>>>>>> fix/consolidate-workflows func (c *Config) Get(key string, out any) error { c.mu.RLock() defer c.mu.RUnlock() -<<<<<<< HEAD if key == "" { return c.v.Unmarshal(out) } @@ -198,14 +139,6 @@ func (c *Config) Get(key string, out any) error { } return c.v.UnmarshalKey(key, out) -======= - val, ok := getNested(c.data, key) - if !ok { - return core.E("config.Get", fmt.Sprintf("key not found: %s", key), nil) - } - - return assign(val, out) ->>>>>>> fix/consolidate-workflows } // Set stores a configuration value by dot-notation key and persists to disk. @@ -213,16 +146,10 @@ func (c *Config) Set(key string, v any) error { c.mu.Lock() defer c.mu.Unlock() -<<<<<<< HEAD c.v.Set(key, v) // Persist to disk if err := Save(c.medium, c.path, c.v.AllSettings()); err != nil { -======= - setNested(c.data, key, v) - - if err := Save(c.medium, c.path, c.data); err != nil { ->>>>>>> fix/consolidate-workflows return core.E("config.Set", "failed to save config", err) } @@ -234,29 +161,7 @@ func (c *Config) All() map[string]any { c.mu.RLock() defer c.mu.RUnlock() -<<<<<<< HEAD return c.v.AllSettings() -======= - return deepCopyMap(c.data) -} - -// deepCopyMap recursively copies a map[string]any. -func deepCopyMap(src map[string]any) map[string]any { - result := make(map[string]any, len(src)) - for k, v := range src { - switch val := v.(type) { - case map[string]any: - result[k] = deepCopyMap(val) - case []any: - cp := make([]any, len(val)) - copy(cp, val) - result[k] = cp - default: - result[k] = v - } - } - return result ->>>>>>> fix/consolidate-workflows } // Path returns the path to the configuration file. @@ -264,7 +169,6 @@ func (c *Config) Path() string { return c.path } -<<<<<<< HEAD // Load reads a YAML configuration file from the given medium and path. // Returns the parsed data as a map, or an error if the file cannot be read or parsed. // Deprecated: Use Config.LoadFile instead. @@ -300,107 +204,6 @@ func Save(m coreio.Medium, path string, data map[string]any) error { return core.E("config.Save", "failed to write config file: "+path, err) } -======= -// getNested retrieves a value from a nested map using dot-notation keys. -func getNested(data map[string]any, key string) (any, bool) { - parts := strings.Split(key, ".") - current := any(data) - - for i, part := range parts { - m, ok := current.(map[string]any) - if !ok { - return nil, false - } - val, exists := m[part] - if !exists { - return nil, false - } - if i == len(parts)-1 { - return val, true - } - current = val - } - - return nil, false -} - -// setNested sets a value in a nested map using dot-notation keys, -// creating intermediate maps as needed. -func setNested(data map[string]any, key string, value any) { - parts := strings.Split(key, ".") - current := data - - for i, part := range parts { - if i == len(parts)-1 { - current[part] = value - return - } - next, ok := current[part] - if !ok { - next = make(map[string]any) - current[part] = next - } - m, ok := next.(map[string]any) - if !ok { - m = make(map[string]any) - current[part] = m - } - current = m - } -} - -// assign sets the value of out to val, handling type conversions. -func assign(val any, out any) error { - switch ptr := out.(type) { - case *string: - switch v := val.(type) { - case string: - *ptr = v - default: - *ptr = fmt.Sprintf("%v", v) - } - case *int: - switch v := val.(type) { - case int: - *ptr = v - case float64: - *ptr = int(v) - case int64: - *ptr = int(v) - default: - return core.E("config.assign", fmt.Sprintf("cannot assign %T to *int", val), nil) - } - case *bool: - switch v := val.(type) { - case bool: - *ptr = v - default: - return core.E("config.assign", fmt.Sprintf("cannot assign %T to *bool", val), nil) - } - case *float64: - switch v := val.(type) { - case float64: - *ptr = v - case int: - *ptr = float64(v) - case int64: - *ptr = float64(v) - default: - return core.E("config.assign", fmt.Sprintf("cannot assign %T to *float64", val), nil) - } - case *any: - *ptr = val - case *map[string]any: - switch v := val.(type) { - case map[string]any: - *ptr = v - default: - return core.E("config.assign", fmt.Sprintf("cannot assign %T to *map[string]any", val), nil) - } - default: - return core.E("config.assign", fmt.Sprintf("unsupported target type: %T", out), nil) - } ->>>>>>> fix/consolidate-workflows return nil } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 762f840..daa9f49 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -225,7 +225,6 @@ func TestSave_Good(t *testing.T) { assert.NoError(t, readErr) assert.Contains(t, content, "key: value") } -<<<<<<< HEAD func TestConfig_LoadFile_Env(t *testing.T) { m := io.NewMockMedium() @@ -276,5 +275,3 @@ func TestConfig_Get_EmptyKey(t *testing.T) { assert.Equal(t, "test", full.App.Name) assert.Equal(t, 1, full.Version) } -======= ->>>>>>> fix/consolidate-workflows diff --git a/pkg/config/loader.go b/pkg/config/loader.go deleted file mode 100644 index 628abfc..0000000 --- a/pkg/config/loader.go +++ /dev/null @@ -1,45 +0,0 @@ -package config - -import ( - "path/filepath" - - core "github.com/host-uk/core/pkg/framework/core" - "github.com/host-uk/core/pkg/io" - "gopkg.in/yaml.v3" -) - -// Load reads a YAML configuration file from the given medium and path. -// Returns the parsed data as a map, or an error if the file cannot be read or parsed. -func Load(m io.Medium, path string) (map[string]any, error) { - content, err := m.Read(path) - if err != nil { - return nil, core.E("config.Load", "failed to read config file: "+path, err) - } - - data := make(map[string]any) - if err := yaml.Unmarshal([]byte(content), &data); err != nil { - return nil, core.E("config.Load", "failed to parse config file: "+path, err) - } - - return data, nil -} - -// Save writes configuration data to a YAML file at the given path. -// It ensures the parent directory exists before writing. -func Save(m io.Medium, path string, data map[string]any) error { - out, err := yaml.Marshal(data) - if err != nil { - return core.E("config.Save", "failed to marshal config", err) - } - - dir := filepath.Dir(path) - if err := m.EnsureDir(dir); err != nil { - return core.E("config.Save", "failed to create config directory: "+dir, err) - } - - if err := m.Write(path, string(out)); err != nil { - return core.E("config.Save", "failed to write config file: "+path, err) - } - - return nil -} diff --git a/pkg/config/service.go b/pkg/config/service.go index b579442..ebdf435 100644 --- a/pkg/config/service.go +++ b/pkg/config/service.go @@ -67,7 +67,6 @@ func (s *Service) Set(key string, v any) error { return s.config.Set(key, v) } -<<<<<<< HEAD // LoadFile merges a configuration file into the central configuration. func (s *Service) LoadFile(m io.Medium, path string) error { if s.config == nil { @@ -76,8 +75,6 @@ func (s *Service) LoadFile(m io.Medium, path string) error { return s.config.LoadFile(m, path) } -======= ->>>>>>> fix/consolidate-workflows // Ensure Service implements core.Config and Startable at compile time. var ( _ core.Config = (*Service)(nil) diff --git a/pkg/container/linuxkit.go b/pkg/container/linuxkit.go index 06647a6..e771b33 100644 --- a/pkg/container/linuxkit.go +++ b/pkg/container/linuxkit.go @@ -436,11 +436,7 @@ func (m *LinuxKitManager) Exec(ctx context.Context, id string, cmd []string) err // Build SSH command sshArgs := []string{ "-p", fmt.Sprintf("%d", sshPort), -<<<<<<< HEAD "-o", "StrictHostKeyChecking=yes", -======= - "-o", "StrictHostKeyChecking=accept-new", ->>>>>>> fix/consolidate-workflows "-o", "UserKnownHostsFile=~/.core/known_hosts", "-o", "LogLevel=ERROR", "root@localhost", diff --git a/pkg/container/linuxkit_test.go b/pkg/container/linuxkit_test.go index 06c1359..b943898 100644 --- a/pkg/container/linuxkit_test.go +++ b/pkg/container/linuxkit_test.go @@ -216,11 +216,7 @@ func TestLinuxKitManager_Stop_Bad_NotRunning(t *testing.T) { statePath := filepath.Join(tmpDir, "containers.json") state, err := LoadState(io.Local, statePath) require.NoError(t, err) -<<<<<<< HEAD manager := NewLinuxKitManagerWithHypervisor(io.Local, state, NewMockHypervisor()) -======= - manager := NewLinuxKitManagerWithHypervisor(state, NewMockHypervisor()) ->>>>>>> fix/consolidate-workflows container := &Container{ ID: "abc12345", @@ -240,11 +236,7 @@ func TestLinuxKitManager_List_Good(t *testing.T) { statePath := filepath.Join(tmpDir, "containers.json") state, err := LoadState(io.Local, statePath) require.NoError(t, err) -<<<<<<< HEAD manager := NewLinuxKitManagerWithHypervisor(io.Local, state, NewMockHypervisor()) -======= - manager := NewLinuxKitManagerWithHypervisor(state, NewMockHypervisor()) ->>>>>>> fix/consolidate-workflows _ = state.Add(&Container{ID: "aaa11111", Status: StatusStopped}) _ = state.Add(&Container{ID: "bbb22222", Status: StatusStopped}) @@ -261,11 +253,7 @@ func TestLinuxKitManager_List_Good_VerifiesRunningStatus(t *testing.T) { statePath := filepath.Join(tmpDir, "containers.json") state, err := LoadState(io.Local, statePath) require.NoError(t, err) -<<<<<<< HEAD manager := NewLinuxKitManagerWithHypervisor(io.Local, state, NewMockHypervisor()) -======= - manager := NewLinuxKitManagerWithHypervisor(state, NewMockHypervisor()) ->>>>>>> fix/consolidate-workflows // Add a "running" container with a fake PID that doesn't exist _ = state.Add(&Container{ diff --git a/pkg/container/templates_test.go b/pkg/container/templates_test.go index df4ae73..c1db5a4 100644 --- a/pkg/container/templates_test.go +++ b/pkg/container/templates_test.go @@ -414,19 +414,8 @@ kernel: err = os.WriteFile(filepath.Join(coreDir, "user-custom.yml"), []byte(templateContent), 0644) require.NoError(t, err) -<<<<<<< HEAD tm := NewTemplateManager(io.Local).WithWorkingDir(tmpDir) templates := tm.ListTemplates() -======= - // Change to the temp directory - oldWd, err := os.Getwd() - require.NoError(t, err) - err = os.Chdir(tmpDir) - require.NoError(t, err) - defer func() { _ = os.Chdir(oldWd) }() - - templates := ListTemplates() ->>>>>>> fix/consolidate-workflows // Should have at least the builtin templates plus the user template assert.GreaterOrEqual(t, len(templates), 3) @@ -460,19 +449,8 @@ services: err = os.WriteFile(filepath.Join(coreDir, "my-user-template.yml"), []byte(templateContent), 0644) require.NoError(t, err) -<<<<<<< HEAD tm := NewTemplateManager(io.Local).WithWorkingDir(tmpDir) content, err := tm.GetTemplate("my-user-template") -======= - // Change to the temp directory - oldWd, err := os.Getwd() - require.NoError(t, err) - err = os.Chdir(tmpDir) - require.NoError(t, err) - defer func() { _ = os.Chdir(oldWd) }() - - content, err := GetTemplate("my-user-template") ->>>>>>> fix/consolidate-workflows require.NoError(t, err) assert.Contains(t, content, "kernel:") @@ -605,21 +583,7 @@ func TestGetUserTemplatesDir_Good_NoDirectory(t *testing.T) { tm := NewTemplateManager(io.Local).WithWorkingDir("/tmp/nonexistent-wd").WithHomeDir("/tmp/nonexistent-home") dir := tm.getUserTemplatesDir() -<<<<<<< HEAD assert.Empty(t, dir) -======= - // Create a temp directory without .core/linuxkit - tmpDir := t.TempDir() - err = os.Chdir(tmpDir) - require.NoError(t, err) - defer func() { _ = os.Chdir(oldWd) }() - - dir := getUserTemplatesDir() - - // Should return empty string since no templates dir exists - // (unless home dir has one) - assert.True(t, dir == "" || strings.Contains(dir, "linuxkit")) ->>>>>>> fix/consolidate-workflows } func TestScanUserTemplates_Good_DefaultDescription(t *testing.T) { diff --git a/pkg/devops/claude.go b/pkg/devops/claude.go index 99a35a1..7bfef0b 100644 --- a/pkg/devops/claude.go +++ b/pkg/devops/claude.go @@ -70,11 +70,7 @@ func (d *DevOps) Claude(ctx context.Context, projectDir string, opts ClaudeOptio // Build SSH command with agent forwarding args := []string{ -<<<<<<< HEAD "-o", "StrictHostKeyChecking=yes", -======= - "-o", "StrictHostKeyChecking=accept-new", ->>>>>>> fix/consolidate-workflows "-o", "UserKnownHostsFile=~/.core/known_hosts", "-o", "LogLevel=ERROR", "-A", // SSH agent forwarding @@ -136,11 +132,7 @@ func (d *DevOps) CopyGHAuth(ctx context.Context) error { // Use scp to copy gh config cmd := exec.CommandContext(ctx, "scp", -<<<<<<< HEAD "-o", "StrictHostKeyChecking=yes", -======= - "-o", "StrictHostKeyChecking=accept-new", ->>>>>>> fix/consolidate-workflows "-o", "UserKnownHostsFile=~/.core/known_hosts", "-o", "LogLevel=ERROR", "-P", fmt.Sprintf("%d", DefaultSSHPort), diff --git a/pkg/devops/serve.go b/pkg/devops/serve.go index 3803583..aac0e8a 100644 --- a/pkg/devops/serve.go +++ b/pkg/devops/serve.go @@ -59,11 +59,7 @@ func (d *DevOps) mountProject(ctx context.Context, path string) error { // Use reverse SSHFS mount // The VM connects back to host to mount the directory cmd := exec.CommandContext(ctx, "ssh", -<<<<<<< HEAD "-o", "StrictHostKeyChecking=yes", -======= - "-o", "StrictHostKeyChecking=accept-new", ->>>>>>> fix/consolidate-workflows "-o", "UserKnownHostsFile=~/.core/known_hosts", "-o", "LogLevel=ERROR", "-R", "10000:localhost:22", // Reverse tunnel for SSHFS diff --git a/pkg/devops/shell.go b/pkg/devops/shell.go index 3afdbcb..fe94d1b 100644 --- a/pkg/devops/shell.go +++ b/pkg/devops/shell.go @@ -33,11 +33,7 @@ func (d *DevOps) Shell(ctx context.Context, opts ShellOptions) error { // sshShell connects via SSH. func (d *DevOps) sshShell(ctx context.Context, command []string) error { args := []string{ -<<<<<<< HEAD "-o", "StrictHostKeyChecking=yes", -======= - "-o", "StrictHostKeyChecking=accept-new", ->>>>>>> fix/consolidate-workflows "-o", "UserKnownHostsFile=~/.core/known_hosts", "-o", "LogLevel=ERROR", "-A", // Agent forwarding diff --git a/pkg/framework/core/core_test.go b/pkg/framework/core/core_test.go index e5550df..07c43cf 100644 --- a/pkg/framework/core/core_test.go +++ b/pkg/framework/core/core_test.go @@ -131,7 +131,6 @@ func TestFeatures_IsEnabled_Good(t *testing.T) { assert.False(t, c.Features.IsEnabled("")) } -<<<<<<< HEAD func TestFeatures_IsEnabled_Edge(t *testing.T) { c, _ := New() c.Features.Flags = []string{" ", "foo"} @@ -140,8 +139,6 @@ func TestFeatures_IsEnabled_Edge(t *testing.T) { assert.False(t, c.Features.IsEnabled("FOO")) // Case sensitive check } -======= ->>>>>>> fix/consolidate-workflows func TestCore_ServiceLifecycle_Good(t *testing.T) { c, err := New() assert.NoError(t, err) diff --git a/pkg/io/local/client.go b/pkg/io/local/client.go index 7bbcae0..78310e4 100644 --- a/pkg/io/local/client.go +++ b/pkg/io/local/client.go @@ -33,7 +33,6 @@ func (m *Medium) path(p string) string { if p == "" { return m.root } -<<<<<<< HEAD // If the path is relative and the medium is rooted at "/", // treat it as relative to the current working directory. @@ -41,23 +40,6 @@ func (m *Medium) path(p string) string { if m.root == "/" && !filepath.IsAbs(p) { cwd, _ := os.Getwd() return filepath.Join(cwd, p) -======= - clean := strings.ReplaceAll(p, "..", ".") - if filepath.IsAbs(clean) { - // If root is "/", allow absolute paths through - if m.root == "/" { - return filepath.Clean(clean) - } - // Otherwise, sandbox absolute paths by stripping volume + leading separators - vol := filepath.VolumeName(clean) - clean = strings.TrimPrefix(clean, vol) - cutset := string(os.PathSeparator) - if os.PathSeparator != '/' { - cutset += "/" - } - clean = strings.TrimLeft(clean, cutset) - return filepath.Join(m.root, clean) ->>>>>>> fix/consolidate-workflows } // Use filepath.Clean with a leading slash to resolve all .. and . internally diff --git a/workspace/local.go b/workspace/local.go deleted file mode 100644 index 4eab633..0000000 --- a/workspace/local.go +++ /dev/null @@ -1,41 +0,0 @@ -package workspace - -import "core/filesystem" - -// localMedium implements the Medium interface for the local disk. -type localMedium struct{} - -// NewLocalMedium creates a new instance of the local storage medium. -func NewLocalMedium() filesystem.Medium { - return &localMedium{} -} - -// FileGet reads a file from the local disk. -func (m *localMedium) FileGet(path string) (string, error) { - return filesystem.Read(filesystem.Local, path) -} - -// FileSet writes a file to the local disk. -func (m *localMedium) FileSet(path, content string) error { - return filesystem.Write(filesystem.Local, path, content) -} - -// Read reads a file from the local disk. -func (m *localMedium) Read(path string) (string, error) { - return filesystem.Read(filesystem.Local, path) -} - -// Write writes a file to the local disk. -func (m *localMedium) Write(path, content string) error { - return filesystem.Write(filesystem.Local, path, content) -} - -// EnsureDir creates a directory on the local disk. -func (m *localMedium) EnsureDir(path string) error { - return filesystem.EnsureDir(filesystem.Local, path) -} - -// IsFile checks if a path exists and is a file on the local disk. -func (m *localMedium) IsFile(path string) bool { - return filesystem.IsFile(filesystem.Local, path) -} diff --git a/workspace/service.go b/workspace/service.go deleted file mode 100644 index f211f16..0000000 --- a/workspace/service.go +++ /dev/null @@ -1,124 +0,0 @@ -package workspace - -import ( - "encoding/json" - "fmt" - "path/filepath" - - "core/config" - "core/crypt/lib/lthn" - "core/crypt/lib/openpgp" - "core/filesystem" -) - -// NewService creates a new WorkspaceService. -func NewService(cfg *config.Config, medium filesystem.Medium) *Service { - return &Service{ - config: cfg, - workspaceList: make(map[string]string), - medium: medium, - } -} - -// ServiceStartup Startup initializes the service, loading the workspace list. -func (s *Service) ServiceStartup() error { - listPath := filepath.Join(s.config.WorkspacesDir, listFile) - - if s.medium.IsFile(listPath) { - content, err := s.medium.FileGet(listPath) - if err != nil { - return fmt.Errorf("failed to read workspace list: %w", err) - } - if err := json.Unmarshal([]byte(content), &s.workspaceList); err != nil { - fmt.Printf("Warning: could not parse workspace list: %v\n", err) - s.workspaceList = make(map[string]string) - } - } - - return s.SwitchWorkspace(defaultWorkspace) -} - -// CreateWorkspace creates a new, obfuscated workspace on the local medium. -func (s *Service) CreateWorkspace(identifier, password string) (string, error) { - realName := lthn.Hash(identifier) - workspaceID := lthn.Hash(fmt.Sprintf("workspace/%s", realName)) - workspacePath := filepath.Join(s.config.WorkspacesDir, workspaceID) - - if _, exists := s.workspaceList[workspaceID]; exists { - return "", fmt.Errorf("workspace for this identifier already exists") - } - - dirsToCreate := []string{"config", "log", "data", "files", "keys"} - for _, dir := range dirsToCreate { - if err := s.medium.EnsureDir(filepath.Join(workspacePath, dir)); err != nil { - return "", fmt.Errorf("failed to create workspace directory '%s': %w", dir, err) - } - } - - keyPair, err := openpgp.CreateKeyPair(workspaceID, password) - if err != nil { - return "", fmt.Errorf("failed to create workspace key pair: %w", err) - } - - keyFiles := map[string]string{ - filepath.Join(workspacePath, "keys", "key.pub"): keyPair.PublicKey, - filepath.Join(workspacePath, "keys", "key.priv"): keyPair.PrivateKey, - } - for path, content := range keyFiles { - if err := s.medium.FileSet(path, content); err != nil { - return "", fmt.Errorf("failed to write key file %s: %w", path, err) - } - } - - s.workspaceList[workspaceID] = keyPair.PublicKey - listData, err := json.MarshalIndent(s.workspaceList, "", " ") - if err != nil { - return "", fmt.Errorf("failed to marshal workspace list: %w", err) - } - - listPath := filepath.Join(s.config.WorkspacesDir, listFile) - if err := s.medium.FileSet(listPath, string(listData)); err != nil { - return "", fmt.Errorf("failed to write workspace list file: %w", err) - } - - return workspaceID, nil -} - -// SwitchWorkspace changes the active workspace. -func (s *Service) SwitchWorkspace(name string) error { - if name != defaultWorkspace { - if _, exists := s.workspaceList[name]; !exists { - return fmt.Errorf("workspace '%s' does not exist", name) - } - } - - path := filepath.Join(s.config.WorkspacesDir, name) - if err := s.medium.EnsureDir(path); err != nil { - return fmt.Errorf("failed to ensure workspace directory exists: %w", err) - } - - s.activeWorkspace = &Workspace{ - Name: name, - Path: path, - } - - return nil -} - -// WorkspaceFileGet retrieves a file from the active workspace. -func (s *Service) WorkspaceFileGet(filename string) (string, error) { - if s.activeWorkspace == nil { - return "", fmt.Errorf("no active workspace") - } - path := filepath.Join(s.activeWorkspace.Path, filename) - return s.medium.FileGet(path) -} - -// WorkspaceFileSet writes a file to the active workspace. -func (s *Service) WorkspaceFileSet(filename, content string) error { - if s.activeWorkspace == nil { - return fmt.Errorf("no active workspace") - } - path := filepath.Join(s.activeWorkspace.Path, filename) - return s.medium.FileSet(path, content) -} diff --git a/workspace/workspace.go b/workspace/workspace.go deleted file mode 100644 index 0d8c0a1..0000000 --- a/workspace/workspace.go +++ /dev/null @@ -1,25 +0,0 @@ -package workspace - -import ( - "core/config" - "core/filesystem" -) - -const ( - defaultWorkspace = "default" - listFile = "list.json" -) - -// Workspace represents a user's workspace. -type Workspace struct { - Name string - Path string -} - -// Service manages user workspaces. -type Service struct { - config *config.Config - activeWorkspace *Workspace - workspaceList map[string]string // Maps Workspace ID to Public Key - medium filesystem.Medium -} diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go deleted file mode 100644 index adacca8..0000000 --- a/workspace/workspace_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package workspace - -import ( - "encoding/json" - "path/filepath" - "testing" - - "core/config" - "github.com/stretchr/testify/assert" -) - -// MockMedium implements the Medium interface for testing purposes. -type MockMedium struct { - Files map[string]string - Dirs map[string]bool -} - -func NewMockMedium() *MockMedium { - return &MockMedium{ - Files: make(map[string]string), - Dirs: make(map[string]bool), - } -} - -func (m *MockMedium) FileGet(path string) (string, error) { - content, ok := m.Files[path] - if !ok { - return "", assert.AnError // Simulate file not found error - } - return content, nil -} - -func (m *MockMedium) FileSet(path, content string) error { - m.Files[path] = content - return nil -} - -func (m *MockMedium) EnsureDir(path string) error { - m.Dirs[path] = true - return nil -} - -func (m *MockMedium) IsFile(path string) bool { - _, ok := m.Files[path] - return ok -} - -func (m *MockMedium) Read(path string) (string, error) { - return m.FileGet(path) -} - -func (m *MockMedium) Write(path, content string) error { - return m.FileSet(path, content) -} - -func TestNewService(t *testing.T) { - mockConfig := &config.Config{} // You might want to mock this further if its behavior is critical - mockMedium := NewMockMedium() - - service := NewService(mockConfig, mockMedium) - - assert.NotNil(t, service) - assert.Equal(t, mockConfig, service.config) - assert.Equal(t, mockMedium, service.medium) - assert.NotNil(t, service.workspaceList) - assert.Nil(t, service.activeWorkspace) // Initially no active workspace -} - -func TestServiceStartup(t *testing.T) { - mockConfig := &config.Config{ - WorkspacesDir: "/tmp/workspaces", - } - - // Test case 1: list.json exists and is valid - t.Run("existing valid list.json", func(t *testing.T) { - mockMedium := NewMockMedium() - - // Prepare a mock workspace list - expectedWorkspaceList := map[string]string{ - "workspace1": "pubkey1", - "workspace2": "pubkey2", - } - listContent, _ := json.MarshalIndent(expectedWorkspaceList, "", " ") - - listPath := filepath.Join(mockConfig.WorkspacesDir, listFile) - mockMedium.FileSet(listPath, string(listContent)) - - service := NewService(mockConfig, mockMedium) - err := service.ServiceStartup() - - assert.NoError(t, err) - assert.Equal(t, expectedWorkspaceList, service.workspaceList) - assert.NotNil(t, service.activeWorkspace) - assert.Equal(t, defaultWorkspace, service.activeWorkspace.Name) - assert.Equal(t, filepath.Join(mockConfig.WorkspacesDir, defaultWorkspace), service.activeWorkspace.Path) - }) - - // Test case 2: list.json does not exist - t.Run("no list.json", func(t *testing.T) { - mockMedium := NewMockMedium() // Fresh medium with no files - - service := NewService(mockConfig, mockMedium) - err := service.ServiceStartup() - - assert.NoError(t, err) - assert.NotNil(t, service.workspaceList) - assert.Empty(t, service.workspaceList) // Should be empty if no list.json - assert.NotNil(t, service.activeWorkspace) - assert.Equal(t, defaultWorkspace, service.activeWorkspace.Name) - assert.Equal(t, filepath.Join(mockConfig.WorkspacesDir, defaultWorkspace), service.activeWorkspace.Path) - }) - - // Test case 3: list.json exists but is invalid - t.Run("invalid list.json", func(t *testing.T) { - mockMedium := NewMockMedium() - - listPath := filepath.Join(mockConfig.WorkspacesDir, listFile) - mockMedium.FileSet(listPath, "{invalid json") // Invalid JSON - - service := NewService(mockConfig, mockMedium) - err := service.ServiceStartup() - - assert.NoError(t, err) // Error is logged, but startup continues - assert.NotNil(t, service.workspaceList) - assert.Empty(t, service.workspaceList) // Should be empty if invalid list.json - assert.NotNil(t, service.activeWorkspace) - assert.Equal(t, defaultWorkspace, service.activeWorkspace.Name) - assert.Equal(t, filepath.Join(mockConfig.WorkspacesDir, defaultWorkspace), service.activeWorkspace.Path) - }) -} - -func TestCreateWorkspace(t *testing.T) { - mockConfig := &config.Config{ - WorkspacesDir: "/tmp/workspaces", - } - mockMedium := NewMockMedium() - service := NewService(mockConfig, mockMedium) - - workspaceID, err := service.CreateWorkspace("test", "password") - assert.NoError(t, err) - assert.NotEmpty(t, workspaceID) -} - -func TestSwitchWorkspace(t *testing.T) { - mockConfig := &config.Config{ - WorkspacesDir: "/tmp/workspaces", - } - mockMedium := NewMockMedium() - service := NewService(mockConfig, mockMedium) - - workspaceID, err := service.CreateWorkspace("test", "password") - assert.NoError(t, err) - - err = service.SwitchWorkspace(workspaceID) - assert.NoError(t, err) - assert.Equal(t, workspaceID, service.activeWorkspace.Name) -}