Compare commits

...

8 Commits

Author SHA1 Message Date
github-actions[bot]
832fd2f718 Update version to v1.4.243 and commit 2025-07-09 10:53:52 +00:00
Kayvan Sylvan
dd0935fb70 Merge pull request #1597 from ksylvan/0708-more-refactoring-fixes-for-patterns-loading
CLI Refactoring: Modular Command Processing and Pattern Loading Improvements
2025-07-09 03:52:25 -07:00
Kayvan Sylvan
e64bdd849c fix: improve error handling and temporary file management in patterns loader
## CHANGES

- Replace println with fmt.Fprintln to stderr for errors
- Use os.MkdirTemp for secure temporary directory creation
- Remove unused time import from patterns loader
- Add proper error wrapping for file operations
- Handle RemoveAll errors with warning messages
- Improve error messages with context information
- Add explicit error checking for cleanup operations
2025-07-09 03:40:03 -07:00
Kayvan Sylvan
be82b4b013 chore: improve error handling for scraping configuration in tools.go 2025-07-09 03:23:29 -07:00
Kayvan Sylvan
6e2f00090c chore: enhance error handling and early returns in CLI
### CHANGES
- Add early return if registry is nil to prevent panics.
- Introduce early return for non-chat tool operations.
- Update error message to use original input on HTML readability failure.
- Enhance error wrapping for playlist video fetching.
- Modify temp patterns folder name with timestamp for uniqueness.
- Improve error handling for patterns directory access.
2025-07-09 03:15:57 -07:00
Kayvan Sylvan
2c2b374664 chore: remove fabric binary 2025-07-09 02:58:09 -07:00
Kayvan Sylvan
b884c529bd chore: update command handlers to return 'handled' boolean
### CHANGES

- Add `handled` boolean return to command handlers
- Modify `handleSetupAndServerCommands` to use `handled`
- Update `handleConfigurationCommands` with `handled` logic
- Implement `handled` return in `handleExtensionCommands`
- Revise `handleListingCommands` to support `handled` return
- Adjust `handleManagementCommands` to return `handled`
2025-07-09 02:57:29 -07:00
Kayvan Sylvan
137aff2268 feat: refactor CLI to modularize command handling
### CHANGES

* Extract chat processing logic into separate function
* Create modular command handlers for setup, configuration, listing, management, and extensions
* Improve patterns loader with migration support and better error handling
* Simplify main CLI logic by delegating to specialized handlers
* Enhance code organization and maintainability
* Add tool processing for YouTube and web scraping functionality
2025-07-09 02:29:38 -07:00
12 changed files with 548 additions and 262 deletions

View File

@@ -1,3 +1,3 @@
package main
var version = "v1.4.242"
var version = "v1.4.243"

66
internal/cli/chat.go Normal file
View File

@@ -0,0 +1,66 @@
package cli
import (
"fmt"
"os"
"strings"
"github.com/danielmiessler/fabric/internal/core"
"github.com/danielmiessler/fabric/internal/domain"
"github.com/danielmiessler/fabric/internal/plugins/db/fsdb"
)
// handleChatProcessing handles the main chat processing logic
func handleChatProcessing(currentFlags *Flags, registry *core.PluginRegistry, messageTools string) (err error) {
if messageTools != "" {
currentFlags.AppendMessage(messageTools)
}
var chatter *core.Chatter
if chatter, err = registry.GetChatter(currentFlags.Model, currentFlags.ModelContextLength,
currentFlags.Strategy, currentFlags.Stream, currentFlags.DryRun); err != nil {
return
}
var session *fsdb.Session
var chatReq *domain.ChatRequest
if chatReq, err = currentFlags.BuildChatRequest(strings.Join(os.Args[1:], " ")); err != nil {
return
}
if chatReq.Language == "" {
chatReq.Language = registry.Language.DefaultLanguage.Value
}
var chatOptions *domain.ChatOptions
if chatOptions, err = currentFlags.BuildChatOptions(); err != nil {
return
}
if session, err = chatter.Send(chatReq, chatOptions); err != nil {
return
}
result := session.GetLastMessage().Content
if !currentFlags.Stream {
// print the result if it was not streamed already
fmt.Println(result)
}
// if the copy flag is set, copy the message to the clipboard
if currentFlags.Copy {
if err = CopyToClipboard(result); err != nil {
return
}
}
// if the output flag is set, create an output file
if currentFlags.Output != "" {
if currentFlags.OutputSession {
sessionAsString := session.String()
err = CreateOutputFile(sessionAsString, currentFlags.Output)
} else {
err = CreateOutputFile(result, currentFlags.Output)
}
}
return
}

View File

@@ -4,18 +4,11 @@ import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/danielmiessler/fabric/internal/tools/youtube"
"github.com/danielmiessler/fabric/internal/core"
"github.com/danielmiessler/fabric/internal/domain"
"github.com/danielmiessler/fabric/internal/plugins/ai"
"github.com/danielmiessler/fabric/internal/plugins/db/fsdb"
restapi "github.com/danielmiessler/fabric/internal/server"
"github.com/danielmiessler/fabric/internal/tools/converter"
"github.com/danielmiessler/fabric/internal/tools/youtube"
)
// Cli Controls the cli. It takes in the flags and runs the appropriate functions
@@ -30,277 +23,67 @@ func Cli(version string) (err error) {
return
}
var homedir string
if homedir, err = os.UserHomeDir(); err != nil {
return
}
fabricDb := fsdb.NewDb(filepath.Join(homedir, ".config/fabric"))
if err = fabricDb.Configure(); err != nil {
// Initialize database and registry
var registry, err2 = initializeFabric()
if err2 != nil {
if !currentFlags.Setup {
println(err.Error())
fmt.Fprintln(os.Stderr, err2.Error())
currentFlags.Setup = true
}
}
var registry *core.PluginRegistry
if registry, err = core.NewPluginRegistry(fabricDb); err != nil {
return
}
// if the setup flag is set, run the setup function
if currentFlags.Setup {
err = registry.Setup()
return
}
if currentFlags.Serve {
registry.ConfigureVendors()
err = restapi.Serve(registry, currentFlags.ServeAddress, currentFlags.ServeAPIKey)
return
}
if currentFlags.ServeOllama {
registry.ConfigureVendors()
err = restapi.ServeOllama(registry, currentFlags.ServeAddress, version)
return
}
if currentFlags.UpdatePatterns {
err = registry.PatternsLoader.PopulateDB()
return
}
if currentFlags.ChangeDefaultModel {
if err = registry.Defaults.Setup(); err != nil {
return
// Return early if registry is nil to prevent panics in subsequent handlers
if registry == nil {
return err2
}
err = registry.SaveEnvFile()
}
// Handle setup and server commands
var handled bool
if handled, err = handleSetupAndServerCommands(currentFlags, registry, version); err != nil || handled {
return
}
if currentFlags.LatestPatterns != "0" {
var parsedToInt int
if parsedToInt, err = strconv.Atoi(currentFlags.LatestPatterns); err != nil {
return
}
if err = fabricDb.Patterns.PrintLatestPatterns(parsedToInt); err != nil {
return
}
// Handle configuration commands
if handled, err = handleConfigurationCommands(currentFlags, registry); err != nil || handled {
return
}
if currentFlags.ListPatterns {
err = fabricDb.Patterns.ListNames(currentFlags.ShellCompleteOutput)
// Handle listing commands
if handled, err = handleListingCommands(currentFlags, registry.Db, registry); err != nil || handled {
return
}
if currentFlags.ListAllModels {
var models *ai.VendorsModels
if models, err = registry.VendorManager.GetModels(); err != nil {
return
}
models.Print(currentFlags.ShellCompleteOutput)
// Handle management commands
if handled, err = handleManagementCommands(currentFlags, registry.Db); err != nil || handled {
return
}
if currentFlags.ListAllContexts {
err = fabricDb.Contexts.ListNames(currentFlags.ShellCompleteOutput)
return
}
if currentFlags.ListAllSessions {
err = fabricDb.Sessions.ListNames(currentFlags.ShellCompleteOutput)
return
}
if currentFlags.WipeContext != "" {
err = fabricDb.Contexts.Delete(currentFlags.WipeContext)
return
}
if currentFlags.WipeSession != "" {
err = fabricDb.Sessions.Delete(currentFlags.WipeSession)
return
}
if currentFlags.PrintSession != "" {
err = fabricDb.Sessions.PrintSession(currentFlags.PrintSession)
return
}
if currentFlags.PrintContext != "" {
err = fabricDb.Contexts.PrintContext(currentFlags.PrintContext)
// Handle extension commands
if handled, err = handleExtensionCommands(currentFlags, registry); err != nil || handled {
return
}
// Process HTML readability if needed
if currentFlags.HtmlReadability {
if msg, cleanErr := converter.HtmlReadability(currentFlags.Message); cleanErr != nil {
fmt.Println("use original input, because can't apply html readability", err)
fmt.Println("use original input, because can't apply html readability", cleanErr)
} else {
currentFlags.Message = msg
}
}
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.ListStrategies {
err = registry.Strategies.ListStrategies(currentFlags.ShellCompleteOutput)
return
}
if currentFlags.ListVendors {
err = registry.ListVendors(os.Stdout)
return
}
// if the interactive flag is set, run the interactive function
// if currentFlags.Interactive {
// interactive.Interactive()
// }
// if none of the above currentFlags are set, run the initiate chat function
// Handle tool-based message processing
var messageTools string
if currentFlags.YouTube != "" {
if !registry.YouTube.IsConfigured() {
err = fmt.Errorf("YouTube is not configured, please run the setup procedure")
return
}
var videoId string
var playlistId string
if videoId, playlistId, err = registry.YouTube.GetVideoOrPlaylistId(currentFlags.YouTube); err != nil {
return
} else if (videoId == "" || currentFlags.YouTubePlaylist) && playlistId != "" {
if currentFlags.Output != "" {
err = registry.YouTube.FetchAndSavePlaylist(playlistId, currentFlags.Output)
} else {
var videos []*youtube.VideoMeta
if videos, err = registry.YouTube.FetchPlaylistVideos(playlistId); err != nil {
err = fmt.Errorf("error fetching playlist videos: %v", err)
return
}
for _, video := range videos {
var message string
if message, err = processYoutubeVideo(currentFlags, registry, video.Id); err != nil {
return
}
if !currentFlags.IsChatRequest() {
if err = WriteOutput(message, fmt.Sprintf("%v.md", video.TitleNormalized)); err != nil {
return
}
} else {
messageTools = AppendMessage(messageTools, message)
}
}
}
return
}
if messageTools, err = processYoutubeVideo(currentFlags, registry, videoId); err != nil {
return
}
if !currentFlags.IsChatRequest() {
err = currentFlags.WriteOutput(messageTools)
return
}
}
if (currentFlags.ScrapeURL != "" || currentFlags.ScrapeQuestion != "") && registry.Jina.IsConfigured() {
// Check if the scrape_url flag is set and call ScrapeURL
if currentFlags.ScrapeURL != "" {
var website string
if website, err = registry.Jina.ScrapeURL(currentFlags.ScrapeURL); err != nil {
return
}
messageTools = AppendMessage(messageTools, website)
}
// Check if the scrape_question flag is set and call ScrapeQuestion
if currentFlags.ScrapeQuestion != "" {
var website string
if website, err = registry.Jina.ScrapeQuestion(currentFlags.ScrapeQuestion); err != nil {
return
}
messageTools = AppendMessage(messageTools, website)
}
if !currentFlags.IsChatRequest() {
err = currentFlags.WriteOutput(messageTools)
return
}
}
if messageTools != "" {
currentFlags.AppendMessage(messageTools)
}
var chatter *core.Chatter
if chatter, err = registry.GetChatter(currentFlags.Model, currentFlags.ModelContextLength,
currentFlags.Strategy, currentFlags.Stream, currentFlags.DryRun); err != nil {
if messageTools, err = handleToolProcessing(currentFlags, registry); err != nil {
return
}
var session *fsdb.Session
var chatReq *domain.ChatRequest
if chatReq, err = currentFlags.BuildChatRequest(strings.Join(os.Args[1:], " ")); err != nil {
return
// Return early for non-chat tool operations
if messageTools != "" && !currentFlags.IsChatRequest() {
return nil
}
if chatReq.Language == "" {
chatReq.Language = registry.Language.DefaultLanguage.Value
}
var chatOptions *domain.ChatOptions
if chatOptions, err = currentFlags.BuildChatOptions(); err != nil {
return
}
if session, err = chatter.Send(chatReq, chatOptions); err != nil {
return
}
result := session.GetLastMessage().Content
if !currentFlags.Stream {
// print the result if it was not streamed already
fmt.Println(result)
}
// if the copy flag is set, copy the message to the clipboard
if currentFlags.Copy {
if err = CopyToClipboard(result); err != nil {
return
}
}
// if the output flag is set, create an output file
if currentFlags.Output != "" {
if currentFlags.OutputSession {
sessionAsString := session.String()
err = CreateOutputFile(sessionAsString, currentFlags.Output)
} else {
err = CreateOutputFile(result, currentFlags.Output)
}
}
// Handle chat processing
err = handleChatProcessing(currentFlags, registry, messageTools)
return
}

View File

@@ -0,0 +1,28 @@
package cli
import (
"github.com/danielmiessler/fabric/internal/core"
)
// handleConfigurationCommands handles configuration-related commands
// Returns (handled, error) where handled indicates if a command was processed and should exit
func handleConfigurationCommands(currentFlags *Flags, registry *core.PluginRegistry) (handled bool, err error) {
if currentFlags.UpdatePatterns {
if err = registry.PatternsLoader.PopulateDB(); err != nil {
return true, err
}
// Save configuration in case any paths were migrated during pattern loading
err = registry.SaveEnvFile()
return true, err
}
if currentFlags.ChangeDefaultModel {
if err = registry.Defaults.Setup(); err != nil {
return true, err
}
err = registry.SaveEnvFile()
return true, err
}
return false, nil
}

View File

@@ -0,0 +1,26 @@
package cli
import (
"github.com/danielmiessler/fabric/internal/core"
)
// handleExtensionCommands handles extension-related commands
// Returns (handled, error) where handled indicates if a command was processed and should exit
func handleExtensionCommands(currentFlags *Flags, registry *core.PluginRegistry) (handled bool, err error) {
if currentFlags.ListExtensions {
err = registry.TemplateExtensions.ListExtensions()
return true, err
}
if currentFlags.AddExtension != "" {
err = registry.TemplateExtensions.RegisterExtension(currentFlags.AddExtension)
return true, err
}
if currentFlags.RemoveExtension != "" {
err = registry.TemplateExtensions.RemoveExtension(currentFlags.RemoveExtension)
return true, err
}
return false, nil
}

View File

@@ -0,0 +1,28 @@
package cli
import (
"os"
"path/filepath"
"github.com/danielmiessler/fabric/internal/core"
"github.com/danielmiessler/fabric/internal/plugins/db/fsdb"
)
// initializeFabric initializes the fabric database and plugin registry
func initializeFabric() (registry *core.PluginRegistry, err error) {
var homedir string
if homedir, err = os.UserHomeDir(); err != nil {
return
}
fabricDb := fsdb.NewDb(filepath.Join(homedir, ".config/fabric"))
if err = fabricDb.Configure(); err != nil {
return
}
if registry, err = core.NewPluginRegistry(fabricDb); err != nil {
return
}
return
}

62
internal/cli/listing.go Normal file
View File

@@ -0,0 +1,62 @@
package cli
import (
"os"
"strconv"
"github.com/danielmiessler/fabric/internal/core"
"github.com/danielmiessler/fabric/internal/plugins/ai"
"github.com/danielmiessler/fabric/internal/plugins/db/fsdb"
)
// handleListingCommands handles listing-related commands
// Returns (handled, error) where handled indicates if a command was processed and should exit
func handleListingCommands(currentFlags *Flags, fabricDb *fsdb.Db, registry *core.PluginRegistry) (handled bool, err error) {
if currentFlags.LatestPatterns != "0" {
var parsedToInt int
if parsedToInt, err = strconv.Atoi(currentFlags.LatestPatterns); err != nil {
return true, err
}
if err = fabricDb.Patterns.PrintLatestPatterns(parsedToInt); err != nil {
return true, err
}
return true, nil
}
if currentFlags.ListPatterns {
err = fabricDb.Patterns.ListNames(currentFlags.ShellCompleteOutput)
return true, err
}
if currentFlags.ListAllModels {
var models *ai.VendorsModels
if models, err = registry.VendorManager.GetModels(); err != nil {
return true, err
}
models.Print(currentFlags.ShellCompleteOutput)
return true, nil
}
if currentFlags.ListAllContexts {
err = fabricDb.Contexts.ListNames(currentFlags.ShellCompleteOutput)
return true, err
}
if currentFlags.ListAllSessions {
err = fabricDb.Sessions.ListNames(currentFlags.ShellCompleteOutput)
return true, err
}
if currentFlags.ListStrategies {
err = registry.Strategies.ListStrategies(currentFlags.ShellCompleteOutput)
return true, err
}
if currentFlags.ListVendors {
err = registry.ListVendors(os.Stdout)
return true, err
}
return false, nil
}

View File

@@ -0,0 +1,31 @@
package cli
import (
"github.com/danielmiessler/fabric/internal/plugins/db/fsdb"
)
// handleManagementCommands handles management-related commands (delete, print, etc.)
// Returns (handled, error) where handled indicates if a command was processed and should exit
func handleManagementCommands(currentFlags *Flags, fabricDb *fsdb.Db) (handled bool, err error) {
if currentFlags.WipeContext != "" {
err = fabricDb.Contexts.Delete(currentFlags.WipeContext)
return true, err
}
if currentFlags.WipeSession != "" {
err = fabricDb.Sessions.Delete(currentFlags.WipeSession)
return true, err
}
if currentFlags.PrintSession != "" {
err = fabricDb.Sessions.PrintSession(currentFlags.PrintSession)
return true, err
}
if currentFlags.PrintContext != "" {
err = fabricDb.Contexts.PrintContext(currentFlags.PrintContext)
return true, err
}
return false, nil
}

View File

@@ -0,0 +1,30 @@
package cli
import (
"github.com/danielmiessler/fabric/internal/core"
restapi "github.com/danielmiessler/fabric/internal/server"
)
// handleSetupAndServerCommands handles setup and server-related commands
// Returns (handled, error) where handled indicates if a command was processed and should exit
func handleSetupAndServerCommands(currentFlags *Flags, registry *core.PluginRegistry, version string) (handled bool, err error) {
// if the setup flag is set, run the setup function
if currentFlags.Setup {
err = registry.Setup()
return true, err
}
if currentFlags.Serve {
registry.ConfigureVendors()
err = restapi.Serve(registry, currentFlags.ServeAddress, currentFlags.ServeAPIKey)
return true, err
}
if currentFlags.ServeOllama {
registry.ConfigureVendors()
err = restapi.ServeOllama(registry, currentFlags.ServeAddress, version)
return true, err
}
return false, nil
}

90
internal/cli/tools.go Normal file
View File

@@ -0,0 +1,90 @@
package cli
import (
"fmt"
"github.com/danielmiessler/fabric/internal/core"
"github.com/danielmiessler/fabric/internal/tools/youtube"
)
// handleToolProcessing handles YouTube and web scraping tool processing
func handleToolProcessing(currentFlags *Flags, registry *core.PluginRegistry) (messageTools string, err error) {
if currentFlags.YouTube != "" {
if !registry.YouTube.IsConfigured() {
err = fmt.Errorf("YouTube is not configured, please run the setup procedure")
return
}
var videoId string
var playlistId string
if videoId, playlistId, err = registry.YouTube.GetVideoOrPlaylistId(currentFlags.YouTube); err != nil {
return
} else if (videoId == "" || currentFlags.YouTubePlaylist) && playlistId != "" {
if currentFlags.Output != "" {
err = registry.YouTube.FetchAndSavePlaylist(playlistId, currentFlags.Output)
} else {
var videos []*youtube.VideoMeta
if videos, err = registry.YouTube.FetchPlaylistVideos(playlistId); err != nil {
err = fmt.Errorf("error fetching playlist videos: %w", err)
return
}
for _, video := range videos {
var message string
if message, err = processYoutubeVideo(currentFlags, registry, video.Id); err != nil {
return
}
if !currentFlags.IsChatRequest() {
if err = WriteOutput(message, fmt.Sprintf("%v.md", video.TitleNormalized)); err != nil {
return
}
} else {
messageTools = AppendMessage(messageTools, message)
}
}
}
return
}
if messageTools, err = processYoutubeVideo(currentFlags, registry, videoId); err != nil {
return
}
if !currentFlags.IsChatRequest() {
err = currentFlags.WriteOutput(messageTools)
return
}
}
if currentFlags.ScrapeURL != "" || currentFlags.ScrapeQuestion != "" {
if !registry.Jina.IsConfigured() {
err = fmt.Errorf("scraping functionality is not configured. Please set up Jina to enable scraping")
return
}
// Check if the scrape_url flag is set and call ScrapeURL
if currentFlags.ScrapeURL != "" {
var website string
if website, err = registry.Jina.ScrapeURL(currentFlags.ScrapeURL); err != nil {
return
}
messageTools = AppendMessage(messageTools, website)
}
// Check if the scrape_question flag is set and call ScrapeQuestion
if currentFlags.ScrapeQuestion != "" {
var website string
if website, err = registry.Jina.ScrapeQuestion(currentFlags.ScrapeQuestion); err != nil {
return
}
messageTools = AppendMessage(messageTools, website)
}
if !currentFlags.IsChatRequest() {
err = currentFlags.WriteOutput(messageTools)
return
}
}
return
}

View File

@@ -55,7 +55,12 @@ type PatternsLoader struct {
func (o *PatternsLoader) configure() (err error) {
o.pathPatternsPrefix = fmt.Sprintf("%v/", o.DefaultFolder.Value)
o.tempPatternsFolder = filepath.Join(os.TempDir(), o.DefaultFolder.Value)
// Use a consistent temp folder name regardless of the source path structure
tempDir, err := os.MkdirTemp("", "fabric-patterns-")
if err != nil {
return fmt.Errorf("failed to create temporary patterns folder: %w", err)
}
o.tempPatternsFolder = tempDir
return
}
@@ -85,18 +90,38 @@ func (o *PatternsLoader) Setup() (err error) {
func (o *PatternsLoader) PopulateDB() (err error) {
fmt.Printf("Downloading patterns and Populating %s...\n", o.Patterns.Dir)
fmt.Println()
originalPath := o.DefaultFolder.Value
if err = o.gitCloneAndCopy(); err != nil {
return
return fmt.Errorf("failed to download patterns from git repository: %w", err)
}
// If the path was migrated during gitCloneAndCopy, we need to save the updated configuration
if o.DefaultFolder.Value != originalPath {
fmt.Printf("💾 Saving updated configuration (path changed from '%s' to '%s')...\n", originalPath, o.DefaultFolder.Value)
// The configuration will be saved by the calling code after this returns successfully
}
if err = o.movePatterns(); err != nil {
return
return fmt.Errorf("failed to move patterns to config directory: %w", err)
}
fmt.Printf("✅ Successfully downloaded and installed patterns to %s\n", o.Patterns.Dir)
return
}
// PersistPatterns copies custom patterns to the updated patterns directory
func (o *PatternsLoader) PersistPatterns() (err error) {
// Check if patterns directory exists, if not, nothing to persist
if _, err = os.Stat(o.Patterns.Dir); err != nil {
if os.IsNotExist(err) {
// No existing patterns directory, nothing to persist
return nil
}
// Return unexpected errors (e.g., permission issues)
return fmt.Errorf("failed to access patterns directory '%s': %w", o.Patterns.Dir, err)
}
var currentPatterns []os.DirEntry
if currentPatterns, err = os.ReadDir(o.Patterns.Dir); err != nil {
return
@@ -108,15 +133,28 @@ func (o *PatternsLoader) PersistPatterns() (err error) {
return
}
for _, currentPattern := range currentPatterns {
for _, newPattern := range newPatterns {
if currentPattern.Name() == newPattern.Name() {
break
}
err = copy.Copy(filepath.Join(o.Patterns.Dir, newPattern.Name()), filepath.Join(newPatternsFolder, newPattern.Name()))
// Create a map of new patterns for faster lookup
newPatternNames := make(map[string]bool)
for _, newPattern := range newPatterns {
if newPattern.IsDir() {
newPatternNames[newPattern.Name()] = true
}
}
return
// Copy custom patterns that don't exist in the new download
for _, currentPattern := range currentPatterns {
if currentPattern.IsDir() && !newPatternNames[currentPattern.Name()] {
// This is a custom pattern, preserve it
src := filepath.Join(o.Patterns.Dir, currentPattern.Name())
dst := filepath.Join(newPatternsFolder, currentPattern.Name())
if copyErr := copy.Copy(src, dst); copyErr != nil {
fmt.Printf("Warning: failed to preserve custom pattern '%s': %v\n", currentPattern.Name(), copyErr)
} else {
fmt.Printf("Preserved custom pattern: %s\n", currentPattern.Name())
}
}
}
return nil
}
// movePatterns copies the new patterns into the config directory
@@ -134,8 +172,29 @@ func (o *PatternsLoader) movePatterns() (err error) {
return
}
// Verify that patterns were actually copied before creating the loaded marker
var entries []os.DirEntry
if entries, err = os.ReadDir(o.Patterns.Dir); err != nil {
return
}
// Count actual pattern directories (exclude the loaded file itself)
patternCount := 0
for _, entry := range entries {
if entry.IsDir() {
patternCount++
}
}
if patternCount == 0 {
err = fmt.Errorf("no patterns were successfully copied to %s", o.Patterns.Dir)
return
}
//create an empty file to indicate that the patterns have been updated if not exists
_, _ = os.Create(o.loadedFilePath)
if _, err = os.Create(o.loadedFilePath); err != nil {
return fmt.Errorf("failed to create loaded marker file '%s': %w", o.loadedFilePath, err)
}
err = os.RemoveAll(patternsDir)
return
@@ -147,15 +206,98 @@ func (o *PatternsLoader) gitCloneAndCopy() (err error) {
return fmt.Errorf("failed to create temp directory: %w", err)
}
// Use the helper to fetch files
fmt.Printf("Cloning repository %s (path: %s)...\n", o.DefaultGitRepoUrl.Value, o.DefaultFolder.Value)
// Try to fetch files with the current path
err = githelper.FetchFilesFromRepo(githelper.FetchOptions{
RepoURL: o.DefaultGitRepoUrl.Value,
PathPrefix: o.DefaultFolder.Value,
DestDir: o.tempPatternsFolder,
})
if err != nil {
return fmt.Errorf("failed to download patterns: %w", err)
return fmt.Errorf("failed to download patterns from %s: %w", o.DefaultGitRepoUrl.Value, err)
}
// Check if patterns were downloaded
if patternCount, checkErr := o.countPatternsInDirectory(o.tempPatternsFolder); checkErr != nil {
return fmt.Errorf("failed to read temp patterns directory: %w", checkErr)
} else if patternCount == 0 {
// No patterns found with current path, try automatic migration
if migrationErr := o.tryPathMigration(); migrationErr != nil {
return fmt.Errorf("no patterns found in repository at path %s and migration failed: %w", o.DefaultFolder.Value, migrationErr)
}
// Migration successful, try downloading again
return o.gitCloneAndCopy()
} else {
fmt.Printf("Downloaded %d patterns to temporary directory\n", patternCount)
}
return nil
}
// tryPathMigration attempts to migrate from old pattern paths to new restructured paths
func (o *PatternsLoader) tryPathMigration() (err error) {
// Check if current path is the old "patterns" path
if o.DefaultFolder.Value == "patterns" {
fmt.Println("🔄 Detected old pattern path 'patterns', trying migration to 'data/patterns'...")
// Try the new restructured path
newPath := "data/patterns"
testTempFolder := filepath.Join(os.TempDir(), "fabric-patterns-test")
// Clean up any existing test temp folder
if err := os.RemoveAll(testTempFolder); err != nil {
fmt.Printf("Warning: failed to remove test temporary folder '%s': %v\n", testTempFolder, err)
}
// Test if the new path works
testErr := githelper.FetchFilesFromRepo(githelper.FetchOptions{
RepoURL: o.DefaultGitRepoUrl.Value,
PathPrefix: newPath,
DestDir: testTempFolder,
})
if testErr == nil {
// Check if patterns exist in the new path
if patternCount, countErr := o.countPatternsInDirectory(testTempFolder); countErr == nil && patternCount > 0 {
fmt.Printf("✅ Found %d patterns at new path '%s', updating configuration...\n", patternCount, newPath)
// Update the configuration
o.DefaultFolder.Value = newPath
// Clean up the main temp folder and replace it with the test one
os.RemoveAll(o.tempPatternsFolder)
if renameErr := os.Rename(testTempFolder, o.tempPatternsFolder); renameErr != nil {
// If rename fails, try copy
if copyErr := copy.Copy(testTempFolder, o.tempPatternsFolder); copyErr != nil {
return fmt.Errorf("failed to move test patterns to temp folder: %w", copyErr)
}
os.RemoveAll(testTempFolder)
}
return nil
}
}
// Clean up test folder
os.RemoveAll(testTempFolder)
}
return fmt.Errorf("unable to find patterns at current path '%s' or migrate to new structure", o.DefaultFolder.Value)
}
// countPatternsInDirectory counts the number of pattern directories in a given directory
func (o *PatternsLoader) countPatternsInDirectory(dir string) (int, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return 0, err
}
patternCount := 0
for _, entry := range entries {
if entry.IsDir() {
patternCount++
}
}
return patternCount, nil
}

View File

@@ -1 +1 @@
"1.4.242"
"1.4.243"