diff --git a/cli/cli.go b/cli/cli.go index 432ad5bb4..cc0e853d1 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -45,7 +45,7 @@ func Cli(version string) (err error) { var registry *core.PluginRegistry if registry, err = core.NewPluginRegistry(fabricDb); err != nil { - return + return } // if the setup flag is set, run the setup function @@ -140,22 +140,20 @@ func Cli(version string) (err error) { } } -if currentFlags.ListExtensions { - err = registry.TemplateExtensions.ListExtensions() - return -} - -if currentFlags.AddExtension != "" { - err = registry.TemplateExtensions.RegisterExtension(currentFlags.AddExtension) - return -} - -if currentFlags.RemoveExtension != "" { - err = registry.TemplateExtensions.RemoveExtension(currentFlags.RemoveExtension) - return -} + if currentFlags.ListExtensions { + err = registry.TemplateExtensions.ListExtensions() + return + } + if currentFlags.AddExtension != "" { + err = registry.TemplateExtensions.RegisterExtension(currentFlags.AddExtension) + return + } + if currentFlags.RemoveExtension != "" { + err = registry.TemplateExtensions.RemoveExtension(currentFlags.RemoveExtension) + return + } // if the interactive flag is set, run the interactive function // if currentFlags.Interactive { diff --git a/cli/flags.go b/cli/flags.go index ea5fc5d4b..b65371a62 100644 --- a/cli/flags.go +++ b/cli/flags.go @@ -66,9 +66,8 @@ type Flags struct { Config string `long:"config" description:"Path to YAML config file"` Version bool `long:"version" description:"Print current version"` ListExtensions bool `long:"listextensions" description:"List all registered extensions"` - AddExtension string `long:"addextension" description:"Register a new extension from config file path"` - RemoveExtension string `long:"rmextension" description:"Remove a registered extension by name"` - + AddExtension string `long:"addextension" description:"Register a new extension from config file path"` + RemoveExtension string `long:"rmextension" description:"Remove a registered extension by name"` } var debug = false diff --git a/core/plugin_registry.go b/core/plugin_registry.go index 3dc76bbdb..e4f87c579 100644 --- a/core/plugin_registry.go +++ b/core/plugin_registry.go @@ -40,7 +40,7 @@ func NewPluginRegistry(db *fsdb.Db) (ret *PluginRegistry, err error) { Language: lang.NewLanguage(), Jina: jina.NewClient(), } - + var homedir string if homedir, err = os.UserHomeDir(); err != nil { return @@ -62,14 +62,14 @@ func NewPluginRegistry(db *fsdb.Db) (ret *PluginRegistry, err error) { type PluginRegistry struct { Db *fsdb.Db - VendorManager *ai.VendorsManager - VendorsAll *ai.VendorsManager - Defaults *tools.Defaults - PatternsLoader *tools.PatternsLoader - YouTube *youtube.YouTube - Language *lang.Language - Jina *jina.Client - TemplateExtensions *template.ExtensionManager + VendorManager *ai.VendorsManager + VendorsAll *ai.VendorsManager + Defaults *tools.Defaults + PatternsLoader *tools.PatternsLoader + YouTube *youtube.YouTube + Language *lang.Language + Jina *jina.Client + TemplateExtensions *template.ExtensionManager } func (o *PluginRegistry) SaveEnvFile() (err error) { diff --git a/core/plugin_registry_test.go b/core/plugin_registry_test.go index f0b000005..412a9fe2e 100644 --- a/core/plugin_registry_test.go +++ b/core/plugin_registry_test.go @@ -11,7 +11,7 @@ func TestSaveEnvFile(t *testing.T) { db := fsdb.NewDb(os.TempDir()) registry, err := NewPluginRegistry(db) if err != nil { - t.Fatalf("NewPluginRegistry() error = %v", err) + t.Fatalf("NewPluginRegistry() error = %v", err) } err = registry.SaveEnvFile() diff --git a/pkgs/fabric/version.nix b/pkgs/fabric/version.nix index daa597fcf..4196d904b 100644 --- a/pkgs/fabric/version.nix +++ b/pkgs/fabric/version.nix @@ -1 +1 @@ -"1.4.127" +"1.4.128" diff --git a/plugins/template/extension_executor.go b/plugins/template/extension_executor.go index 9edc07891..1da098439 100644 --- a/plugins/template/extension_executor.go +++ b/plugins/template/extension_executor.go @@ -14,15 +14,15 @@ import ( // ExtensionExecutor handles the secure execution of extensions // It uses the registry to verify extensions before running them type ExtensionExecutor struct { - registry *ExtensionRegistry + registry *ExtensionRegistry } // NewExtensionExecutor creates a new executor instance // It requires a registry to verify extensions func NewExtensionExecutor(registry *ExtensionRegistry) *ExtensionExecutor { - return &ExtensionExecutor{ - registry: registry, - } + return &ExtensionExecutor{ + registry: registry, + } } // Execute runs an extension with the given operation and value string @@ -34,19 +34,19 @@ func (e *ExtensionExecutor) Execute(name, operation, value string) (string, erro // Get and verify extension from registry ext, err := e.registry.GetExtension(name) if err != nil { - return "", fmt.Errorf("failed to get extension: %w", err) + return "", fmt.Errorf("failed to get extension: %w", err) } // Format the command using our template system cmdStr, err := e.formatCommand(ext, operation, value) if err != nil { - return "", fmt.Errorf("failed to format command: %w", err) + return "", fmt.Errorf("failed to format command: %w", err) } // Split the command string into command and arguments cmdParts := strings.Fields(cmdStr) if len(cmdParts) < 1 { - return "", fmt.Errorf("empty command after formatting") + return "", fmt.Errorf("empty command after formatting") } // Create command with the Executable and formatted arguments @@ -55,13 +55,13 @@ func (e *ExtensionExecutor) Execute(name, operation, value string) (string, erro // Set up environment if specified if len(ext.Env) > 0 { - cmd.Env = append(os.Environ(), ext.Env...) + cmd.Env = append(os.Environ(), ext.Env...) } // Execute based on output method outputMethod := ext.GetOutputMethod() if outputMethod == "file" { - return e.executeWithFile(cmd, ext) + return e.executeWithFile(cmd, ext) } return e.executeStdout(cmd, ext) } @@ -72,7 +72,7 @@ func (e *ExtensionExecutor) formatCommand(ext *ExtensionDefinition, operation st // Get operation config opConfig, exists := ext.Operations[operation] if !exists { - return "", fmt.Errorf("operation %s not found for extension %s", operation, ext.Name) + return "", fmt.Errorf("operation %s not found for extension %s", operation, ext.Name) } vars := make(map[string]string) @@ -83,27 +83,27 @@ func (e *ExtensionExecutor) formatCommand(ext *ExtensionDefinition, operation st // Split on pipe for numbered variables values := strings.Split(value, "|") for i, val := range values { - vars[fmt.Sprintf("%d", i+1)] = val + vars[fmt.Sprintf("%d", i+1)] = val } - + return ApplyTemplate(opConfig.CmdTemplate, vars, "") } // executeStdout runs the command and captures its stdout func (e *ExtensionExecutor) executeStdout(cmd *exec.Cmd, ext *ExtensionDefinition) (string, error) { - var stdout bytes.Buffer - var stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr - //debug output - fmt.Printf("Executing command: %s\n", cmd.String()) + //debug output + fmt.Printf("Executing command: %s\n", cmd.String()) - if err := cmd.Run(); err != nil { - return "", fmt.Errorf("execution failed: %w\nstderr: %s", err, stderr.String()) - } + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("execution failed: %w\nstderr: %s", err, stderr.String()) + } - return stdout.String(), nil + return stdout.String(), nil } // executeWithFile runs the command and handles file-based output @@ -111,7 +111,7 @@ func (e *ExtensionExecutor) executeWithFile(cmd *exec.Cmd, ext *ExtensionDefinit // Parse timeout - this is now a first-class field timeout, err := time.ParseDuration(ext.Timeout) if err != nil { - return "", fmt.Errorf("invalid timeout format: %w", err) + return "", fmt.Errorf("invalid timeout format: %w", err) } // Create context with timeout @@ -122,51 +122,51 @@ func (e *ExtensionExecutor) executeWithFile(cmd *exec.Cmd, ext *ExtensionDefinit fileConfig := ext.GetFileConfig() if fileConfig == nil { - return "", fmt.Errorf("no file configuration found") + return "", fmt.Errorf("no file configuration found") } // Handle path from stdout case if pathFromStdout, ok := fileConfig["path_from_stdout"].(bool); ok && pathFromStdout { - return e.handlePathFromStdout(cmd, ext) + return e.handlePathFromStdout(cmd, ext) } // Handle fixed file case workDir, _ := fileConfig["work_dir"].(string) outputFile, _ := fileConfig["output_file"].(string) - + if outputFile == "" { - return "", fmt.Errorf("no output file specified in configuration") + return "", fmt.Errorf("no output file specified in configuration") } // Set working directory if specified if workDir != "" { - cmd.Dir = workDir + cmd.Dir = workDir } var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { - if ctx.Err() == context.DeadlineExceeded { - return "", fmt.Errorf("execution timed out after %v", timeout) - } - return "", fmt.Errorf("execution failed: %w\nerr: %s", err, stderr.String()) + if ctx.Err() == context.DeadlineExceeded { + return "", fmt.Errorf("execution timed out after %v", timeout) + } + return "", fmt.Errorf("execution failed: %w\nerr: %s", err, stderr.String()) } // Construct full file path outputPath := outputFile if workDir != "" { - outputPath = filepath.Join(workDir, outputFile) + outputPath = filepath.Join(workDir, outputFile) } content, err := os.ReadFile(outputPath) if err != nil { - return "", fmt.Errorf("failed to read output file: %w", err) + return "", fmt.Errorf("failed to read output file: %w", err) } // Handle cleanup if enabled if ext.IsCleanupEnabled() { - defer os.Remove(outputPath) + defer os.Remove(outputPath) } return string(content), nil @@ -179,18 +179,18 @@ func (e *ExtensionExecutor) handlePathFromStdout(cmd *exec.Cmd, ext *ExtensionDe cmd.Stderr = &stderr if err := cmd.Run(); err != nil { - return "", fmt.Errorf("failed to get output path: %w\nerr: %s", err, stderr.String()) + return "", fmt.Errorf("failed to get output path: %w\nerr: %s", err, stderr.String()) } outputPath := strings.TrimSpace(stdout.String()) content, err := os.ReadFile(outputPath) if err != nil { - return "", fmt.Errorf("failed to read output file: %w", err) + return "", fmt.Errorf("failed to read output file: %w", err) } if ext.IsCleanupEnabled() { - defer os.Remove(outputPath) + defer os.Remove(outputPath) } return string(content), nil -} \ No newline at end of file +} diff --git a/plugins/template/extension_executor_test.go b/plugins/template/extension_executor_test.go index 72033979e..bfc235a8f 100644 --- a/plugins/template/extension_executor_test.go +++ b/plugins/template/extension_executor_test.go @@ -222,7 +222,7 @@ config: "cleanup": "true", } - err := createExtension("basic-test", "write", + err := createExtension("basic-test", "write", "{{executable}} write {{1}} "+outputFile, config) if err != nil { t.Fatalf("Failed to create extension: %v", err) @@ -261,7 +261,7 @@ config: // Test cleanup behavior t.Run("CleanupBehavior", func(t *testing.T) { outputFile := filepath.Join(tmpDir, "cleanup-test.txt") - + // Test with cleanup enabled config := map[string]interface{}{ "output_file": `"cleanup-test.txt"`, @@ -357,4 +357,4 @@ config: t.Error("Expected error from missing output_file, got nil") } }) -} \ No newline at end of file +} diff --git a/plugins/template/extension_manager.go b/plugins/template/extension_manager.go index 131c988da..5ae6f8a8d 100644 --- a/plugins/template/extension_manager.go +++ b/plugins/template/extension_manager.go @@ -11,59 +11,59 @@ import ( // ExtensionManager handles the high-level operations of the extension system type ExtensionManager struct { - registry *ExtensionRegistry - executor *ExtensionExecutor - configDir string + registry *ExtensionRegistry + executor *ExtensionExecutor + configDir string } // NewExtensionManager creates a new extension manager instance func NewExtensionManager(configDir string) *ExtensionManager { - registry := NewExtensionRegistry(configDir) - return &ExtensionManager{ - registry: registry, - executor: NewExtensionExecutor(registry), - configDir: configDir, - } + registry := NewExtensionRegistry(configDir) + return &ExtensionManager{ + registry: registry, + executor: NewExtensionExecutor(registry), + configDir: configDir, + } } // ListExtensions handles the listextensions flag action func (em *ExtensionManager) ListExtensions() error { if em.registry == nil || em.registry.registry.Extensions == nil { - return fmt.Errorf("extension registry not initialized") + return fmt.Errorf("extension registry not initialized") } for name, entry := range em.registry.registry.Extensions { - fmt.Printf("Extension: %s\n", name) - - // Try to load extension details - ext, err := em.registry.GetExtension(name) - if err != nil { - fmt.Printf(" Status: DISABLED - Hash verification failed: %v\n", err) - fmt.Printf(" Config Path: %s\n\n", entry.ConfigPath) - continue - } + fmt.Printf("Extension: %s\n", name) + + // Try to load extension details + ext, err := em.registry.GetExtension(name) + if err != nil { + fmt.Printf(" Status: DISABLED - Hash verification failed: %v\n", err) + fmt.Printf(" Config Path: %s\n\n", entry.ConfigPath) + continue + } + + // Print extension details if verification succeeded + fmt.Printf(" Status: ENABLED\n") + fmt.Printf(" Executable: %s\n", ext.Executable) + fmt.Printf(" Type: %s\n", ext.Type) + fmt.Printf(" Timeout: %s\n", ext.Timeout) + fmt.Printf(" Description: %s\n", ext.Description) + fmt.Printf(" Version: %s\n", ext.Version) + + fmt.Printf(" Operations:\n") + for opName, opConfig := range ext.Operations { + fmt.Printf(" %s:\n", opName) + fmt.Printf(" Command Template: %s\n", opConfig.CmdTemplate) + } - // Print extension details if verification succeeded - fmt.Printf(" Status: ENABLED\n") - fmt.Printf(" Executable: %s\n", ext.Executable) - fmt.Printf(" Type: %s\n", ext.Type) - fmt.Printf(" Timeout: %s\n", ext.Timeout) - fmt.Printf(" Description: %s\n", ext.Description) - fmt.Printf(" Version: %s\n", ext.Version) - - fmt.Printf(" Operations:\n") - for opName, opConfig := range ext.Operations { - fmt.Printf(" %s:\n", opName) - fmt.Printf(" Command Template: %s\n", opConfig.CmdTemplate) - } - - if fileConfig := ext.GetFileConfig(); fileConfig != nil { - fmt.Printf(" File Configuration:\n") - for k, v := range fileConfig { - fmt.Printf(" %s: %v\n", k, v) - } + if fileConfig := ext.GetFileConfig(); fileConfig != nil { + fmt.Printf(" File Configuration:\n") + for k, v := range fileConfig { + fmt.Printf(" %s: %v\n", k, v) } - fmt.Printf("\n") + } + fmt.Printf("\n") } return nil @@ -73,27 +73,27 @@ func (em *ExtensionManager) ListExtensions() error { func (em *ExtensionManager) RegisterExtension(configPath string) error { absPath, err := filepath.Abs(configPath) if err != nil { - return fmt.Errorf("invalid config path: %w", err) + return fmt.Errorf("invalid config path: %w", err) } // Get extension name before registration for status message data, err := os.ReadFile(absPath) if err != nil { - return fmt.Errorf("failed to read config file: %w", err) + return fmt.Errorf("failed to read config file: %w", err) } var ext ExtensionDefinition if err := yaml.Unmarshal(data, &ext); err != nil { - return fmt.Errorf("failed to parse config file: %w", err) + return fmt.Errorf("failed to parse config file: %w", err) } if err := em.registry.Register(absPath); err != nil { - return fmt.Errorf("failed to register extension: %w", err) + return fmt.Errorf("failed to register extension: %w", err) } if _, err := time.ParseDuration(ext.Timeout); err != nil { - return fmt.Errorf("invalid timeout value '%s': must be a duration like '30s' or '1m': %w", ext.Timeout, err) -} + return fmt.Errorf("invalid timeout value '%s': must be a duration like '30s' or '1m': %w", ext.Timeout, err) + } // Print success message with extension details fmt.Printf("Successfully registered extension:\n") @@ -103,18 +103,18 @@ func (em *ExtensionManager) RegisterExtension(configPath string) error { fmt.Printf(" Timeout: %s\n", ext.Timeout) fmt.Printf(" Description: %s\n", ext.Description) fmt.Printf(" Version: %s\n", ext.Version) - + fmt.Printf(" Operations:\n") for opName, opConfig := range ext.Operations { - fmt.Printf(" %s:\n", opName) - fmt.Printf(" Command Template: %s\n", opConfig.CmdTemplate) + fmt.Printf(" %s:\n", opName) + fmt.Printf(" Command Template: %s\n", opConfig.CmdTemplate) } if fileConfig := ext.GetFileConfig(); fileConfig != nil { - fmt.Printf(" File Configuration:\n") - for k, v := range fileConfig { - fmt.Printf(" %s: %v\n", k, v) - } + fmt.Printf(" File Configuration:\n") + for k, v := range fileConfig { + fmt.Printf(" %s: %v\n", k, v) + } } return nil @@ -122,14 +122,14 @@ func (em *ExtensionManager) RegisterExtension(configPath string) error { // RemoveExtension handles the rmextension flag action func (em *ExtensionManager) RemoveExtension(name string) error { - if err := em.registry.Remove(name); err != nil { - return fmt.Errorf("failed to remove extension: %w", err) - } + if err := em.registry.Remove(name); err != nil { + return fmt.Errorf("failed to remove extension: %w", err) + } - return nil + return nil } // ProcessExtension handles template processing for extension directives func (em *ExtensionManager) ProcessExtension(name, operation, value string) (string, error) { return em.executor.Execute(name, operation, value) -} \ No newline at end of file +} diff --git a/plugins/template/extension_manager_test.go b/plugins/template/extension_manager_test.go index 2deec2f13..0e7d259a2 100644 --- a/plugins/template/extension_manager_test.go +++ b/plugins/template/extension_manager_test.go @@ -181,4 +181,4 @@ timeout: 30s`, } }) } -} \ No newline at end of file +} diff --git a/plugins/template/extension_registry.go b/plugins/template/extension_registry.go index 43690888f..470ae9a37 100644 --- a/plugins/template/extension_registry.go +++ b/plugins/template/extension_registry.go @@ -16,321 +16,314 @@ import ( // ExtensionDefinition represents a single extension configuration type ExtensionDefinition struct { - // Global properties - Name string `yaml:"name"` - Executable string `yaml:"executable"` - Type string `yaml:"type"` - Timeout string `yaml:"timeout"` - Description string `yaml:"description"` - Version string `yaml:"version"` - Env []string `yaml:"env"` - - // Operation-specific commands - Operations map[string]OperationConfig `yaml:"operations"` - - // Additional config - Config map[string]interface{} `yaml:"config"` + // Global properties + Name string `yaml:"name"` + Executable string `yaml:"executable"` + Type string `yaml:"type"` + Timeout string `yaml:"timeout"` + Description string `yaml:"description"` + Version string `yaml:"version"` + Env []string `yaml:"env"` + + // Operation-specific commands + Operations map[string]OperationConfig `yaml:"operations"` + + // Additional config + Config map[string]interface{} `yaml:"config"` } type OperationConfig struct { - CmdTemplate string `yaml:"cmd_template"` + CmdTemplate string `yaml:"cmd_template"` } - - // RegistryEntry represents a registered extension type RegistryEntry struct { - ConfigPath string `yaml:"config_path"` - ConfigHash string `yaml:"config_hash"` - ExecutableHash string `yaml:"executable_hash"` + ConfigPath string `yaml:"config_path"` + ConfigHash string `yaml:"config_hash"` + ExecutableHash string `yaml:"executable_hash"` } type ExtensionRegistry struct { - configDir string - registry struct { - Extensions map[string]*RegistryEntry `yaml:"extensions"` - } + configDir string + registry struct { + Extensions map[string]*RegistryEntry `yaml:"extensions"` + } } - - // Helper methods for Config access func (e *ExtensionDefinition) GetOutputMethod() string { if output, ok := e.Config["output"].(map[string]interface{}); ok { - if method, ok := output["method"].(string); ok { - return method - } + if method, ok := output["method"].(string); ok { + return method + } } return "stdout" // default to stdout if not specified } func (e *ExtensionDefinition) GetFileConfig() map[string]interface{} { if output, ok := e.Config["output"].(map[string]interface{}); ok { - if fileConfig, ok := output["file_config"].(map[string]interface{}); ok { - return fileConfig - } + if fileConfig, ok := output["file_config"].(map[string]interface{}); ok { + return fileConfig + } } return nil } func (e *ExtensionDefinition) IsCleanupEnabled() bool { if fc := e.GetFileConfig(); fc != nil { - if cleanup, ok := fc["cleanup"].(bool); ok { - return cleanup - } + if cleanup, ok := fc["cleanup"].(bool); ok { + return cleanup + } } return false // default to no cleanup } - func NewExtensionRegistry(configDir string) *ExtensionRegistry { - r := &ExtensionRegistry{ - configDir: configDir, - } - r.registry.Extensions = make(map[string]*RegistryEntry) - - r.ensureConfigDir() - - if err := r.loadRegistry(); err != nil { - if Debug { - fmt.Printf("Warning: could not load extension registry: %v\n", err) - } - } - - return r + r := &ExtensionRegistry{ + configDir: configDir, + } + r.registry.Extensions = make(map[string]*RegistryEntry) + + r.ensureConfigDir() + + if err := r.loadRegistry(); err != nil { + if Debug { + fmt.Printf("Warning: could not load extension registry: %v\n", err) + } + } + + return r } func (r *ExtensionRegistry) ensureConfigDir() error { - extDir := filepath.Join(r.configDir, "extensions") - return os.MkdirAll(extDir, 0755) + extDir := filepath.Join(r.configDir, "extensions") + return os.MkdirAll(extDir, 0755) } // Update the Register method in extension_registry.go func (r *ExtensionRegistry) Register(configPath string) error { - // Read and parse the extension definition to verify it - data, err := os.ReadFile(configPath) - if err != nil { - return fmt.Errorf("failed to read config file: %w", err) - } - - var ext ExtensionDefinition - if err := yaml.Unmarshal(data, &ext); err != nil { - return fmt.Errorf("failed to parse config file: %w", err) - } - - // Validate extension name - if ext.Name == "" { - return fmt.Errorf("extension name cannot be empty") - } - - if strings.Contains(ext.Name, " ") { - return fmt.Errorf("extension name '%s' contains spaces - names must not contain spaces", ext.Name) - } - - // Verify executable exists - if _, err := os.Stat(ext.Executable); err != nil { - return fmt.Errorf("executable not found: %w", err) - } - - // Get absolute path to config - absPath, err := filepath.Abs(configPath) - if err != nil { - return fmt.Errorf("failed to get absolute path: %w", err) - } - - // Calculate hashes - configHash := ComputeStringHash(string(data)) - executableHash, err := ComputeHash(ext.Executable) - if err != nil { - return fmt.Errorf("failed to hash executable: %w", err) - } - - // Store entry - r.registry.Extensions[ext.Name] = &RegistryEntry{ - ConfigPath: absPath, - ConfigHash: configHash, - ExecutableHash: executableHash, - } - - return r.saveRegistry() + // Read and parse the extension definition to verify it + data, err := os.ReadFile(configPath) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + var ext ExtensionDefinition + if err := yaml.Unmarshal(data, &ext); err != nil { + return fmt.Errorf("failed to parse config file: %w", err) + } + + // Validate extension name + if ext.Name == "" { + return fmt.Errorf("extension name cannot be empty") + } + + if strings.Contains(ext.Name, " ") { + return fmt.Errorf("extension name '%s' contains spaces - names must not contain spaces", ext.Name) + } + + // Verify executable exists + if _, err := os.Stat(ext.Executable); err != nil { + return fmt.Errorf("executable not found: %w", err) + } + + // Get absolute path to config + absPath, err := filepath.Abs(configPath) + if err != nil { + return fmt.Errorf("failed to get absolute path: %w", err) + } + + // Calculate hashes + configHash := ComputeStringHash(string(data)) + executableHash, err := ComputeHash(ext.Executable) + if err != nil { + return fmt.Errorf("failed to hash executable: %w", err) + } + + // Store entry + r.registry.Extensions[ext.Name] = &RegistryEntry{ + ConfigPath: absPath, + ConfigHash: configHash, + ExecutableHash: executableHash, + } + + return r.saveRegistry() } func (r *ExtensionRegistry) validateExtensionDefinition(ext *ExtensionDefinition) error { - // Validate required fields - if ext.Name == "" { - return fmt.Errorf("extension name is required") - } - if ext.Executable == "" { - return fmt.Errorf("executable path is required") - } - if ext.Type == "" { - return fmt.Errorf("extension type is required") - } - - // Validate timeout format - if ext.Timeout != "" { - if _, err := time.ParseDuration(ext.Timeout); err != nil { - return fmt.Errorf("invalid timeout format: %w", err) - } - } - - // Validate operations - if len(ext.Operations) == 0 { - return fmt.Errorf("at least one operation must be defined") - } - for name, op := range ext.Operations { - if op.CmdTemplate == "" { - return fmt.Errorf("command template is required for operation %s", name) - } - } - - return nil -} + // Validate required fields + if ext.Name == "" { + return fmt.Errorf("extension name is required") + } + if ext.Executable == "" { + return fmt.Errorf("executable path is required") + } + if ext.Type == "" { + return fmt.Errorf("extension type is required") + } + + // Validate timeout format + if ext.Timeout != "" { + if _, err := time.ParseDuration(ext.Timeout); err != nil { + return fmt.Errorf("invalid timeout format: %w", err) + } + } + + // Validate operations + if len(ext.Operations) == 0 { + return fmt.Errorf("at least one operation must be defined") + } + for name, op := range ext.Operations { + if op.CmdTemplate == "" { + return fmt.Errorf("command template is required for operation %s", name) + } + } + return nil +} func (r *ExtensionRegistry) Remove(name string) error { - if _, exists := r.registry.Extensions[name]; !exists { - return fmt.Errorf("extension %s not found", name) - } + if _, exists := r.registry.Extensions[name]; !exists { + return fmt.Errorf("extension %s not found", name) + } - delete(r.registry.Extensions, name) + delete(r.registry.Extensions, name) - return r.saveRegistry() + return r.saveRegistry() } func (r *ExtensionRegistry) Verify(name string) error { - // Get the registry entry - entry, exists := r.registry.Extensions[name] - if !exists { - return fmt.Errorf("extension %s not found", name) - } - - // Load and parse the config file - data, err := os.ReadFile(entry.ConfigPath) - if err != nil { - return fmt.Errorf("failed to read config file: %w", err) - } - - // Verify config hash - currentConfigHash := ComputeStringHash(string(data)) - if currentConfigHash != entry.ConfigHash { - return fmt.Errorf("config file hash mismatch for %s", name) - } - - // Parse to get executable path - var ext ExtensionDefinition - if err := yaml.Unmarshal(data, &ext); err != nil { - return fmt.Errorf("failed to parse config file: %w", err) - } - - // Verify executable hash - currentExecutableHash, err := ComputeHash(ext.Executable) - if err != nil { - return fmt.Errorf("failed to verify executable: %w", err) - } - - if currentExecutableHash != entry.ExecutableHash { - return fmt.Errorf("executable hash mismatch for %s", name) - } - - return nil + // Get the registry entry + entry, exists := r.registry.Extensions[name] + if !exists { + return fmt.Errorf("extension %s not found", name) + } + + // Load and parse the config file + data, err := os.ReadFile(entry.ConfigPath) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + // Verify config hash + currentConfigHash := ComputeStringHash(string(data)) + if currentConfigHash != entry.ConfigHash { + return fmt.Errorf("config file hash mismatch for %s", name) + } + + // Parse to get executable path + var ext ExtensionDefinition + if err := yaml.Unmarshal(data, &ext); err != nil { + return fmt.Errorf("failed to parse config file: %w", err) + } + + // Verify executable hash + currentExecutableHash, err := ComputeHash(ext.Executable) + if err != nil { + return fmt.Errorf("failed to verify executable: %w", err) + } + + if currentExecutableHash != entry.ExecutableHash { + return fmt.Errorf("executable hash mismatch for %s", name) + } + + return nil } func (r *ExtensionRegistry) GetExtension(name string) (*ExtensionDefinition, error) { - entry, exists := r.registry.Extensions[name] - if !exists { - return nil, fmt.Errorf("extension %s not found", name) - } - - // Read current config file - data, err := os.ReadFile(entry.ConfigPath) - if err != nil { - return nil, fmt.Errorf("failed to read config file: %w", err) - } - - // Verify config hash - currentHash := ComputeStringHash(string(data)) - if currentHash != entry.ConfigHash { - return nil, fmt.Errorf("config file hash mismatch for %s", name) - } - - // Parse config - var ext ExtensionDefinition - if err := yaml.Unmarshal(data, &ext); err != nil { - return nil, fmt.Errorf("failed to parse config file: %w", err) - } - - // Verify executable hash - currentExecHash, err := ComputeHash(ext.Executable) - if err != nil { - return nil, fmt.Errorf("failed to verify executable: %w", err) - } - - if currentExecHash != entry.ExecutableHash { - return nil, fmt.Errorf("executable hash mismatch for %s", name) - } - - return &ext, nil -} + entry, exists := r.registry.Extensions[name] + if !exists { + return nil, fmt.Errorf("extension %s not found", name) + } + // Read current config file + data, err := os.ReadFile(entry.ConfigPath) + if err != nil { + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + // Verify config hash + currentHash := ComputeStringHash(string(data)) + if currentHash != entry.ConfigHash { + return nil, fmt.Errorf("config file hash mismatch for %s", name) + } + + // Parse config + var ext ExtensionDefinition + if err := yaml.Unmarshal(data, &ext); err != nil { + return nil, fmt.Errorf("failed to parse config file: %w", err) + } + + // Verify executable hash + currentExecHash, err := ComputeHash(ext.Executable) + if err != nil { + return nil, fmt.Errorf("failed to verify executable: %w", err) + } + + if currentExecHash != entry.ExecutableHash { + return nil, fmt.Errorf("executable hash mismatch for %s", name) + } + + return &ext, nil +} func (r *ExtensionRegistry) ListExtensions() ([]*ExtensionDefinition, error) { - var exts []*ExtensionDefinition - - for name := range r.registry.Extensions { - ext, err := r.GetExtension(name) - if err != nil { - // Instead of failing, we'll return nil for this extension - // The manager will handle displaying the error - exts = append(exts, nil) - continue - } - exts = append(exts, ext) - } - - return exts, nil + var exts []*ExtensionDefinition + + for name := range r.registry.Extensions { + ext, err := r.GetExtension(name) + if err != nil { + // Instead of failing, we'll return nil for this extension + // The manager will handle displaying the error + exts = append(exts, nil) + continue + } + exts = append(exts, ext) + } + + return exts, nil } func (r *ExtensionRegistry) calculateFileHash(path string) (string, error) { - f, err := os.Open(path) - if err != nil { - return "", err - } - defer f.Close() - - h := sha256.New() - if _, err := io.Copy(h, f); err != nil { - return "", err - } - - return hex.EncodeToString(h.Sum(nil)), nil + f, err := os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + + return hex.EncodeToString(h.Sum(nil)), nil } func (r *ExtensionRegistry) saveRegistry() error { - data, err := yaml.Marshal(r.registry) - if err != nil { - return fmt.Errorf("failed to marshal extension registry: %w", err) - } + data, err := yaml.Marshal(r.registry) + if err != nil { + return fmt.Errorf("failed to marshal extension registry: %w", err) + } - registryPath := filepath.Join(r.configDir, "extensions", "extensions.yaml") - return os.WriteFile(registryPath, data, 0644) + registryPath := filepath.Join(r.configDir, "extensions", "extensions.yaml") + return os.WriteFile(registryPath, data, 0644) } func (r *ExtensionRegistry) loadRegistry() error { registryPath := filepath.Join(r.configDir, "extensions", "extensions.yaml") data, err := os.ReadFile(registryPath) if err != nil { - if os.IsNotExist(err) { - return nil // New registry - } - return fmt.Errorf("failed to read extension registry: %w", err) + if os.IsNotExist(err) { + return nil // New registry + } + return fmt.Errorf("failed to read extension registry: %w", err) } // Need to unmarshal the data into our registry if err := yaml.Unmarshal(data, &r.registry); err != nil { - return fmt.Errorf("failed to parse extension registry: %w", err) + return fmt.Errorf("failed to parse extension registry: %w", err) } return nil -} \ No newline at end of file +} diff --git a/plugins/template/extension_registry_test.go b/plugins/template/extension_registry_test.go index 5dcadc4c9..fa90ffe9d 100644 --- a/plugins/template/extension_registry_test.go +++ b/plugins/template/extension_registry_test.go @@ -59,7 +59,7 @@ operations: // Test hash verification t.Run("HashVerification", func(t *testing.T) { registry := NewExtensionRegistry(tmpDir) - + // Modify executable after registration modifiedExecContent := []byte("#!/bin/bash\necho \"modified\"") err := os.WriteFile(execPath, modifiedExecContent, 0755) @@ -72,4 +72,4 @@ operations: t.Error("Expected error when executable modified, got nil") } }) -} \ No newline at end of file +} diff --git a/plugins/template/hash.go b/plugins/template/hash.go index 38f7dde82..8e616bed8 100644 --- a/plugins/template/hash.go +++ b/plugins/template/hash.go @@ -11,23 +11,23 @@ import ( // ComputeHash computes SHA-256 hash of a file at given path. // Returns the hex-encoded hash string or an error if the operation fails. func ComputeHash(path string) (string, error) { - f, err := os.Open(path) - if err != nil { - return "", fmt.Errorf("open file: %w", err) - } - defer f.Close() + f, err := os.Open(path) + if err != nil { + return "", fmt.Errorf("open file: %w", err) + } + defer f.Close() - h := sha256.New() - if _, err := io.Copy(h, f); err != nil { - return "", fmt.Errorf("read file: %w", err) - } + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", fmt.Errorf("read file: %w", err) + } - return hex.EncodeToString(h.Sum(nil)), nil + return hex.EncodeToString(h.Sum(nil)), nil } // ComputeStringHash returns hex-encoded SHA-256 hash of the given string func ComputeStringHash(s string) string { - h := sha256.New() - h.Write([]byte(s)) - return hex.EncodeToString(h.Sum(nil)) -} \ No newline at end of file + h := sha256.New() + h.Write([]byte(s)) + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/plugins/template/hash_test.go b/plugins/template/hash_test.go index a7b124217..adf951270 100644 --- a/plugins/template/hash_test.go +++ b/plugins/template/hash_test.go @@ -8,112 +8,112 @@ import ( ) func TestComputeHash(t *testing.T) { - // Create a temporary test file - content := []byte("test content for hashing") - tmpfile, err := os.CreateTemp("", "hashtest") - if err != nil { - t.Fatalf("failed to create temp file: %v", err) - } - defer os.Remove(tmpfile.Name()) + // Create a temporary test file + content := []byte("test content for hashing") + tmpfile, err := os.CreateTemp("", "hashtest") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer os.Remove(tmpfile.Name()) - if _, err := tmpfile.Write(content); err != nil { - t.Fatalf("failed to write to temp file: %v", err) - } - if err := tmpfile.Close(); err != nil { - t.Fatalf("failed to close temp file: %v", err) - } + if _, err := tmpfile.Write(content); err != nil { + t.Fatalf("failed to write to temp file: %v", err) + } + if err := tmpfile.Close(); err != nil { + t.Fatalf("failed to close temp file: %v", err) + } - tests := []struct { - name string - path string - want string // known hash for test content - wantErr bool - }{ - { - name: "valid file", - path: tmpfile.Name(), - want: "e25dd806d495b413931f4eea50b677a7a5c02d00460924661283f211a37f7e7f", // pre-computed hash of "test content for hashing" - wantErr: false, - }, - { - name: "nonexistent file", - path: filepath.Join(os.TempDir(), "nonexistent"), - want: "", - wantErr: true, - }, - } + tests := []struct { + name string + path string + want string // known hash for test content + wantErr bool + }{ + { + name: "valid file", + path: tmpfile.Name(), + want: "e25dd806d495b413931f4eea50b677a7a5c02d00460924661283f211a37f7e7f", // pre-computed hash of "test content for hashing" + wantErr: false, + }, + { + name: "nonexistent file", + path: filepath.Join(os.TempDir(), "nonexistent"), + want: "", + wantErr: true, + }, + } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ComputeHash(tt.path) - if (err != nil) != tt.wantErr { - t.Errorf("ComputeHash() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want && !tt.wantErr { - t.Errorf("ComputeHash() = %v, want %v", got, tt.want) - } - }) - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ComputeHash(tt.path) + if (err != nil) != tt.wantErr { + t.Errorf("ComputeHash() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want && !tt.wantErr { + t.Errorf("ComputeHash() = %v, want %v", got, tt.want) + } + }) + } } func TestComputeStringHash(t *testing.T) { - tests := []struct { - name string - input string - want string - }{ - { - name: "empty string", - input: "", - want: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - }, - { - name: "simple string", - input: "test", - want: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", - }, - { - name: "longer string with spaces", - input: "this is a test string", - want: "f6774519d1c7a3389ef327e9c04766b999db8cdfb85d1346c471ee86d65885bc", - }, - } + tests := []struct { + name string + input string + want string + }{ + { + name: "empty string", + input: "", + want: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }, + { + name: "simple string", + input: "test", + want: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + }, + { + name: "longer string with spaces", + input: "this is a test string", + want: "f6774519d1c7a3389ef327e9c04766b999db8cdfb85d1346c471ee86d65885bc", + }, + } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := ComputeStringHash(tt.input); got != tt.want { - t.Errorf("ComputeStringHash() = %v, want %v", got, tt.want) - } - }) - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ComputeStringHash(tt.input); got != tt.want { + t.Errorf("ComputeStringHash() = %v, want %v", got, tt.want) + } + }) + } } // TestHashConsistency ensures both hash functions produce same results for same content func TestHashConsistency(t *testing.T) { - content := "test content for consistency check" - - // Create a file with the test content - tmpfile, err := os.CreateTemp("", "hashconsistency") - if err != nil { - t.Fatalf("failed to create temp file: %v", err) - } - defer os.Remove(tmpfile.Name()) + content := "test content for consistency check" - if err := os.WriteFile(tmpfile.Name(), []byte(content), 0644); err != nil { - t.Fatalf("failed to write to temp file: %v", err) - } + // Create a file with the test content + tmpfile, err := os.CreateTemp("", "hashconsistency") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer os.Remove(tmpfile.Name()) - // Get hashes using both methods - fileHash, err := ComputeHash(tmpfile.Name()) - if err != nil { - t.Fatalf("ComputeHash failed: %v", err) - } + if err := os.WriteFile(tmpfile.Name(), []byte(content), 0644); err != nil { + t.Fatalf("failed to write to temp file: %v", err) + } - stringHash := ComputeStringHash(content) + // Get hashes using both methods + fileHash, err := ComputeHash(tmpfile.Name()) + if err != nil { + t.Fatalf("ComputeHash failed: %v", err) + } - // Compare results - if fileHash != stringHash { - t.Errorf("Hash inconsistency: file hash %v != string hash %v", fileHash, stringHash) - } -} \ No newline at end of file + stringHash := ComputeStringHash(content) + + // Compare results + if fileHash != stringHash { + t.Errorf("Hash inconsistency: file hash %v != string hash %v", fileHash, stringHash) + } +} diff --git a/plugins/template/template.go b/plugins/template/template.go index 09e3690e9..9b2b41c19 100644 --- a/plugins/template/template.go +++ b/plugins/template/template.go @@ -20,13 +20,13 @@ var ( var extensionManager *ExtensionManager func init() { - homedir, err := os.UserHomeDir() - if err != nil { - debugf("Warning: could not initialize extension manager: %v\n", err) - } - configDir := filepath.Join(homedir, ".config/fabric") - extensionManager = NewExtensionManager(configDir) - // Extensions will work if registry exists, otherwise they'll just fail gracefully + homedir, err := os.UserHomeDir() + if err != nil { + debugf("Warning: could not initialize extension manager: %v\n", err) + } + configDir := filepath.Join(homedir, ".config/fabric") + extensionManager = NewExtensionManager(configDir) + // Extensions will work if registry exists, otherwise they'll just fail gracefully } var pluginPattern = regexp.MustCompile(`\{\{plugin:([^:]+):([^:]+)(?::([^}]+))?\}\}`) @@ -40,120 +40,118 @@ func debugf(format string, a ...interface{}) { func ApplyTemplate(content string, variables map[string]string, input string) (string, error) { - var missingVars []string - r := regexp.MustCompile(`\{\{([^{}]+)\}\}`) - - debugf("Starting template processing\n") - for strings.Contains(content, "{{") { - matches := r.FindAllStringSubmatch(content, -1) - if len(matches) == 0 { - break - } - - replaced := false - for _, match := range matches { - fullMatch := match[0] - varName := match[1] - - // Check if this is a plugin call - if strings.HasPrefix(varName, "plugin:") { - pluginMatches := pluginPattern.FindStringSubmatch(fullMatch) - if len(pluginMatches) >= 3 { - namespace := pluginMatches[1] - operation := pluginMatches[2] - value := "" - if len(pluginMatches) == 4 { - value = pluginMatches[3] - } - - debugf("\nPlugin call:\n") - debugf(" Namespace: %s\n", namespace) - debugf(" Operation: %s\n", operation) - debugf(" Value: %s\n", value) - - var result string - var err error - - switch namespace { - case "text": - debugf("Executing text plugin\n") - result, err = textPlugin.Apply(operation, value) - case "datetime": - debugf("Executing datetime plugin\n") - result, err = datetimePlugin.Apply(operation, value) - case "file": - debugf("Executing file plugin\n") - result, err = filePlugin.Apply(operation, value) - debugf("File plugin result: %#v\n", result) - case "fetch": - debugf("Executing fetch plugin\n") - result, err = fetchPlugin.Apply(operation, value) - case "sys": - debugf("Executing sys plugin\n") - result, err = sysPlugin.Apply(operation, value) - default: - return "", fmt.Errorf("unknown plugin namespace: %s", namespace) - } - - if err != nil { - debugf("Plugin error: %v\n", err) - return "", fmt.Errorf("plugin %s error: %v", namespace, err) - } - - debugf("Plugin result: %s\n", result) - content = strings.ReplaceAll(content, fullMatch, result) - debugf("Content after replacement: %s\n", content) - continue - } - } - - if pluginMatches := extensionPattern.FindStringSubmatch(fullMatch); len(pluginMatches) >= 3 { - name := pluginMatches[1] - operation := pluginMatches[2] - value := "" - if len(pluginMatches) == 4 { - value = pluginMatches[3] - } - - debugf("\nExtension call:\n") - debugf(" Name: %s\n", name) - debugf(" Operation: %s\n", operation) - debugf(" Value: %s\n", value) - - result, err := extensionManager.ProcessExtension(name, operation, value) - if err != nil { - return "", fmt.Errorf("extension %s error: %v", name, err) - } - - content = strings.ReplaceAll(content, fullMatch, result) - replaced = true - continue - } - - - - // Handle regular variables and input - debugf("Processing variable: %s\n", varName) - if varName == "input" { - debugf("Replacing {{input}}\n") - replaced = true - content = strings.ReplaceAll(content, fullMatch, input) - } else { - if val, ok := variables[varName]; !ok { - debugf("Missing variable: %s\n", varName) - missingVars = append(missingVars, varName) - return "", fmt.Errorf("missing required variable: %s", varName) - } else { - debugf("Replacing variable %s with value: %s\n", varName, val) - content = strings.ReplaceAll(content, fullMatch, val) - replaced = true - } - } - if !replaced { - return "", fmt.Errorf("template processing stuck - potential infinite loop") - } - } - } + var missingVars []string + r := regexp.MustCompile(`\{\{([^{}]+)\}\}`) + + debugf("Starting template processing\n") + for strings.Contains(content, "{{") { + matches := r.FindAllStringSubmatch(content, -1) + if len(matches) == 0 { + break + } + + replaced := false + for _, match := range matches { + fullMatch := match[0] + varName := match[1] + + // Check if this is a plugin call + if strings.HasPrefix(varName, "plugin:") { + pluginMatches := pluginPattern.FindStringSubmatch(fullMatch) + if len(pluginMatches) >= 3 { + namespace := pluginMatches[1] + operation := pluginMatches[2] + value := "" + if len(pluginMatches) == 4 { + value = pluginMatches[3] + } + + debugf("\nPlugin call:\n") + debugf(" Namespace: %s\n", namespace) + debugf(" Operation: %s\n", operation) + debugf(" Value: %s\n", value) + + var result string + var err error + + switch namespace { + case "text": + debugf("Executing text plugin\n") + result, err = textPlugin.Apply(operation, value) + case "datetime": + debugf("Executing datetime plugin\n") + result, err = datetimePlugin.Apply(operation, value) + case "file": + debugf("Executing file plugin\n") + result, err = filePlugin.Apply(operation, value) + debugf("File plugin result: %#v\n", result) + case "fetch": + debugf("Executing fetch plugin\n") + result, err = fetchPlugin.Apply(operation, value) + case "sys": + debugf("Executing sys plugin\n") + result, err = sysPlugin.Apply(operation, value) + default: + return "", fmt.Errorf("unknown plugin namespace: %s", namespace) + } + + if err != nil { + debugf("Plugin error: %v\n", err) + return "", fmt.Errorf("plugin %s error: %v", namespace, err) + } + + debugf("Plugin result: %s\n", result) + content = strings.ReplaceAll(content, fullMatch, result) + debugf("Content after replacement: %s\n", content) + continue + } + } + + if pluginMatches := extensionPattern.FindStringSubmatch(fullMatch); len(pluginMatches) >= 3 { + name := pluginMatches[1] + operation := pluginMatches[2] + value := "" + if len(pluginMatches) == 4 { + value = pluginMatches[3] + } + + debugf("\nExtension call:\n") + debugf(" Name: %s\n", name) + debugf(" Operation: %s\n", operation) + debugf(" Value: %s\n", value) + + result, err := extensionManager.ProcessExtension(name, operation, value) + if err != nil { + return "", fmt.Errorf("extension %s error: %v", name, err) + } + + content = strings.ReplaceAll(content, fullMatch, result) + replaced = true + continue + } + + // Handle regular variables and input + debugf("Processing variable: %s\n", varName) + if varName == "input" { + debugf("Replacing {{input}}\n") + replaced = true + content = strings.ReplaceAll(content, fullMatch, input) + } else { + if val, ok := variables[varName]; !ok { + debugf("Missing variable: %s\n", varName) + missingVars = append(missingVars, varName) + return "", fmt.Errorf("missing required variable: %s", varName) + } else { + debugf("Replacing variable %s with value: %s\n", varName, val) + content = strings.ReplaceAll(content, fullMatch, val) + replaced = true + } + } + if !replaced { + return "", fmt.Errorf("template processing stuck - potential infinite loop") + } + } + } debugf("Starting template processing\n") for strings.Contains(content, "{{") { diff --git a/plugins/template/utils.go b/plugins/template/utils.go index 1db7c8913..a5a9f4ab8 100644 --- a/plugins/template/utils.go +++ b/plugins/template/utils.go @@ -16,26 +16,26 @@ import ( // - cannot convert to absolute path // - path doesn't exist func ExpandPath(path string) (string, error) { - // If path starts with ~ - if strings.HasPrefix(path, "~/") { - usr, err := user.Current() - if err != nil { - return "", fmt.Errorf("failed to get user home directory: %w", err) - } - // Replace ~/ with actual home directory - path = filepath.Join(usr.HomeDir, path[2:]) - } + // If path starts with ~ + if strings.HasPrefix(path, "~/") { + usr, err := user.Current() + if err != nil { + return "", fmt.Errorf("failed to get user home directory: %w", err) + } + // Replace ~/ with actual home directory + path = filepath.Join(usr.HomeDir, path[2:]) + } - // Convert to absolute path - absPath, err := filepath.Abs(path) - if err != nil { - return "", fmt.Errorf("failed to get absolute path: %w", err) - } + // Convert to absolute path + absPath, err := filepath.Abs(path) + if err != nil { + return "", fmt.Errorf("failed to get absolute path: %w", err) + } - // Check if path exists - if _, err := os.Stat(absPath); err != nil { - return "", fmt.Errorf("path does not exist: %w", err) - } + // Check if path exists + if _, err := os.Stat(absPath); err != nil { + return "", fmt.Errorf("path does not exist: %w", err) + } - return absPath, nil + return absPath, nil } diff --git a/version.go b/version.go index cad9a291e..aa41f9451 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package main -var version = "v1.4.127" +var version = "v1.4.128"