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)
-}