diff --git a/VERSION b/VERSION index b482243..795460f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.0.2 \ No newline at end of file +v1.1.0 diff --git a/ai.go b/ai.go new file mode 100644 index 0000000..a78159f --- /dev/null +++ b/ai.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + "os" + "time" + + "github.com/teilomillet/gollm" +) + +func NewAI() gollm.LLM { + provider, model, seed := *figs.String(kAiProvider), *figs.String(kAiModel), *figs.Int(kAiSeed) + maxTokens := *figs.Int(kAiMaxTokens) + var opts []gollm.ConfigOption + opts = append(opts, gollm.SetProvider(provider)) + opts = append(opts, gollm.SetModel(model)) + if seed != -1 { + opts = append(opts, gollm.SetSeed(seed)) + } + if maxTokens > 0 { + opts = append(opts, gollm.SetMaxTokens(maxTokens)) + } + opts = append(opts, gollm.SetMemory(*figs.Int(kMemory))) + opts = append(opts, gollm.SetEnableCaching(*figs.Bool(kAiCachingEnabled))) + timeout := *figs.UnitDuration(kAiTimeout) + if timeout < time.Second { + timeout = dTimeout * dTimeoutUnit + } + opts = append(opts, gollm.SetTimeout(*figs.UnitDuration(kAiTimeout))) + switch provider { + case "ollama": + capture("unset OLLAMA_API_KEY env", os.Unsetenv("OLLAMA_API_KEY")) + opts = append(opts, gollm.SetTemperature(0.99)) + opts = append(opts, gollm.SetLogLevel(gollm.LogLevelError)) + default: + apiKey := *figs.String(kAiApiKey) + opts = append(opts, gollm.SetAPIKey(apiKey)) + } + llm, err := gollm.NewLLM(opts...) + if err != nil { + fmt.Printf("āŒ Failed to initialize AI: %v\n", err) + return nil + } + return llm +} diff --git a/chat.go b/chat.go new file mode 100644 index 0000000..ed5d968 --- /dev/null +++ b/chat.go @@ -0,0 +1,332 @@ +package main + +import ( + "bytes" + "context" + "errors" + "fmt" + "log" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/charmbracelet/bubbles/textarea" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/muesli/reflow/wordwrap" + "github.com/teilomillet/gollm" +) + +// --- STYLING --- +var ( + // Styles for chat messages + senderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("5")) // User (Purple) + botStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")) // AI (Cyan) + errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9")).Bold(true) // Error messages + + // A slight border for the chat viewport + viewportStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("8")). // Gray + Padding(1) +) + +func StartChat(buf *bytes.Buffer) { + // Create and run the Bubble Tea program. + // tea.WithAltScreen() provides a full-window TUI experience. + // CORRECTED: Pass aiPtr.llm directly, not its address. + p := tea.NewProgram(initialModel(NewAI(), buf.String()), tea.WithAltScreen(), tea.WithMouseCellMotion()) + + finalModel, err := p.Run() + if err != nil { + log.Fatalf("āŒ Oh no, there's been an error: %v", err) + } + + if m, ok := finalModel.(model); ok && len(m.messages) > 1 { + // More than 1 message means there was a conversation (initial message + at least one more). + + // Create a timestamped filename. + timestamp := time.Now().Format("2006-01-02_15-04-05") + filename := fmt.Sprintf("chatlog_%s.md", timestamp) + + var output bytes.Buffer + output.WriteString("# Summarize Chat Log " + timestamp + "\n\n") + for i := 0; i < len(m.messages); i++ { + message := m.messages[i] + output.WriteString(message) + output.WriteString("\n") + } + + // Write the chat history to the file. + if writeErr := os.WriteFile(filepath.Join(*figs.String(kOutputDir), filename), output.Bytes(), 0644); writeErr != nil { + fmt.Printf("\nāŒ Could not save chat log: %v\n", writeErr) + } else { + fmt.Printf("\nšŸ“ Chat log saved to %s\n", filename) + } + } +} + +// --- BUBBLETEA MESSAGES --- +// We use custom messages to communicate between our async LLM calls and the UI. + +// aiResponseMsg is sent when the AI has successfully generated a response. +type aiResponseMsg string + +// errorMsg is sent when an error occurs during the AI call. +type errorMsg struct{ err error } + +// --- BUBBLETEA MODEL --- +// The model is the single source of truth for the state of your application. +type model struct { + // CORRECTED: The llm field is now the interface type, not a pointer to it. + llm gollm.LLM + viewport viewport.Model + textarea textarea.Model + messages []string + summary string + isGenerating bool + err error + ctx context.Context + chatHistory []string +} + +// initialModel creates the starting state of our application. +// CORRECTED: The llm parameter is now the interface type. +func initialModel(llm gollm.LLM, summary string) model { + if llm == nil { + errMsg := "LLM is nil. Please try again later." + return model{ + llm: nil, + messages: []string{errorStyle.Render(errMsg)}, + chatHistory: []string{}, + isGenerating: false, + err: errors.New("empty summary"), + ctx: context.Background(), + } + } + // Configure the text area for user input. + ta := textarea.New() + ta.Placeholder = "Send a message... (press Enter to send, Esc to quit)" + ta.Focus() + ta.Prompt = "ā”ƒ " + ta.SetHeight(1) + // Remove the default behavior of Enter creating a new line. + ta.KeyMap.InsertNewline.SetEnabled(false) + + // The viewport is the scrolling area for the chat history. + vp := viewport.New(0, 0) // Width and height are set dynamically + + if len(summary) == 0 { + errMsg := "No project summary available. Please provide a valid summary to start the chat." + return model{ + llm: llm, + textarea: ta, + viewport: vp, + summary: summary, + messages: []string{errorStyle.Render(errMsg)}, + chatHistory: []string{}, + isGenerating: false, + err: errors.New("empty summary"), + ctx: context.Background(), + } + } + + msg := fmt.Sprintf("%s %d bytes!", "Welcome to Summarize AI Chat! We've analyzed your project workspace and are ready to chat with you about ", len(summary)) + + return model{ + llm: llm, + textarea: ta, + viewport: vp, + summary: summary, + messages: []string{msg}, + chatHistory: []string{}, + isGenerating: false, + err: nil, + ctx: context.Background(), + } +} + +// generateResponseCmd is a Bubble Tea command that calls the LLM in a goroutine. +// This prevents the UI from blocking while waiting for the AI. +func (m model) generateResponseCmd() tea.Cmd { + return func() tea.Msg { + userInput := m.textarea.Value() + m.chatHistory = append(m.chatHistory, userInput) + + var wc strings.Builder + breaker := "---ARM-GO-SUMMARIZE-BREAK-POINT---" + if len(m.messages) > 0 { + wc.WriteString("You are now continuing this conversation. This is the chat log: ") + for i := 0; i < len(m.messages); i++ { + v := m.messages[i] + x := fmt.Sprintf("line %d: %s\n", i+1, v) + wc.WriteString(x) + } + wc.WriteString("\n") + wc.WriteString("The summarized project is:\n") + parts := strings.Split(m.summary, breaker) + if len(parts) == 2 { + oldPrefix, oldSummary := parts[0], parts[1] + newSummary := oldPrefix + wc.String() + oldSummary + m.summary = newSummary + wc.Reset() + } + wc.WriteString(m.summary) + wc.WriteString("\n") + } else { + wc.WriteString("Your name is Summarize in this engagement. This is a comprehensive one page contents of " + + "entire directory (recursively) of a specific subset of files by extension choice and a strings.Contains() avoid list" + + "that is used to generate the following summary.\n\n" + + "You are communicating with the user and shall refer to them as Commander. You are speaking to them in a " + + "golang bubbletea TUI chat terminal that is ") + wc.WriteString(strconv.Itoa(m.viewport.Width)) + wc.WriteString(" (int) width and ") + wc.WriteString(strconv.Itoa(m.viewport.Height)) + wc.WriteString(" (int) height with ") + wc.WriteString(strconv.Itoa(m.viewport.VisibleLineCount())) + wc.WriteString(" (int) visible lines in the viewport. Your responses should singularly fit in the terminal" + + "window. Be aware that your response will be formatted using wordwrap.String(, m.viewport.Width) in the Bubbletea TUI, so ensure your message fits within the viewport width. " + + "\n\n" + + "The user expects you to be professional and keep focused on the language that you detect from the summary.\n" + + "Be prepared to answer questions about security, best practices, and security concerns that you have over" + + "the code. However, do not get distracted. Always follow the lead of the DevOps engineer. Do not be afraid to" + + "offend. Your brutal honesty is welcome here and iron sharpens iron. Here is the summary now:\n" + + breaker + "\n") + oldSummary := m.summary + newSummary := oldSummary + wc.String() + m.summary = newSummary + wc.Reset() + wc.WriteString(m.summary) + wc.WriteString("\n") + } + + var systemPrompt strings.Builder + systemPrompt.WriteString("This is a summary of a project that you are to respond to user prompts with the contents of this project.\n\n") + systemPrompt.WriteString(m.summary) + systemPrompt.WriteString("\n") + + var ( + response = "" + err = errors.New("failed to ask agent for a response") + prompt = gollm.NewPrompt(userInput, + gollm.WithContext(strings.Join(m.chatHistory, "\n")), + gollm.WithSystemPrompt(systemPrompt.String(), gollm.CacheTypeEphemeral), + gollm.WithMaxLength(7777), + gollm.WithDirectives("Be concise and offer complete solutions", + "Act as Commander Data from the USS Starship Enterprise acting as an AI Agent assisting the user", + "Refer to the user as Commander", + "Speak as if you were on a Military Base as a member of the USS Starship Enterprise", + "Speak as if you are on duty with fellow crew mates", + "When replying to followup requests, build on your previous answer", + "When a mistake is identified by the user, use the full previous response to modify and return", + "Do not be afraid to offend and always give an honest answer in as few words as possible", + ), + gollm.WithOutput( + fmt.Sprintf("%s %d wide %d tall.", "Do not apply any formatting to the output"+ + " text except for line breaks and spaces. Commands and codes should be indented by 4 spaces "+ + "on the left and right side of the line and the text will render inside of a Golang BubbleTea"+ + "TUI window that is ", m.viewport.Width-5, m.viewport.Height-5), + ), + ) + ) + response, err = m.llm.Generate(m.ctx, prompt) + if err != nil { + return errorMsg{err} // On error, return an error message. + } + response = response + "\n\n" + + return aiResponseMsg(response) // On success, return the AI's response. + } +} + +// --- BUBBLETEA LIFECYCLE --- + +// Init is called once when the program starts. It can return an initial command. +func (m model) Init() tea.Cmd { + return textarea.Blink // Start with a blinking cursor in the textarea. +} + +// Update is the core of the application. It's called whenever a message (event) occurs. +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var ( + taCmd tea.Cmd + vpCmd tea.Cmd + ) + + // Handle updates for the textarea and viewport components. + m.textarea, taCmd = m.textarea.Update(msg) + m.viewport, vpCmd = m.viewport.Update(msg) + + switch msg := msg.(type) { + // Handle key presses + case tea.KeyMsg: + switch msg.Type { + case tea.KeyCtrlC, tea.KeyEsc: + return m, tea.Quit + case tea.KeyEnter: + // Don't send if the AI is already working or input is empty. + if m.isGenerating || m.textarea.Value() == "" { + return m, nil + } + + // Add the user's message to the history and set the generating flag. + m.messages = append(m.messages, senderStyle.Render("You: ")+m.textarea.Value()) + m.isGenerating = true + m.err = nil // Clear any previous error. + + // Create the command to call the LLM and reset the input. + cmd := m.generateResponseCmd() + m.textarea.Reset() + m.viewport.SetContent(wordwrap.String(strings.Join(m.messages, "\n"), m.viewport.Width)) + m.viewport.GotoBottom() // Scroll to the latest message. + + return m, cmd + } + + // Handle window resizing + case tea.WindowSizeMsg: + // Adjust the layout to the new window size. + viewportStyle.Width(msg.Width - 2) // Subtract border width + viewportStyle.Height(msg.Height - 4) // Subtract textarea, help text, and border + m.viewport.Width = msg.Width - 2 + m.viewport.Height = msg.Height - 4 + m.textarea.SetWidth(msg.Width) + m.viewport.SetContent(wordwrap.String(strings.Join(m.messages, "\n"), m.viewport.Width)) // Re-render content + + // Handle the AI's response + case aiResponseMsg: + m.isGenerating = false + m.messages = append(m.messages, botStyle.Render("Summarize AI: ")+string(msg)) + m.viewport.SetContent(wordwrap.String(strings.Join(m.messages, "\n"), m.viewport.Width)) + m.viewport.GotoBottom() + + // Handle any errors from the AI call + case errorMsg: + m.isGenerating = false + m.err = msg.err + } + + return m, tea.Batch(taCmd, vpCmd) // Return any commands from the components. +} + +// View renders the UI. It's called after every Update. +func (m model) View() string { + var bottomLine string + if m.isGenerating { + bottomLine = "šŸ¤” Thinking..." + } else if m.err != nil { + bottomLine = errorStyle.Render(fmt.Sprintf("Error: %v", m.err)) + } else { + bottomLine = m.textarea.View() + } + + // Join the viewport and the bottom line (textarea or status) vertically. + return lipgloss.JoinVertical( + lipgloss.Left, + viewportStyle.Render(m.viewport.View()), + bottomLine, + ) +} diff --git a/configure.go b/configure.go new file mode 100644 index 0000000..7d7d87a --- /dev/null +++ b/configure.go @@ -0,0 +1,63 @@ +package main + +import ( + "fmt" + "path/filepath" + + "github.com/andreimerlescu/figtree/v2" + "github.com/andreimerlescu/goenv/env" +) + +// init creates a new figtree with options to use CONFIG_FILE as a way of reading a YAML file while ignoring the env +func configure() { + // figs is a tree of figs that ignore the ENV + figs = figtree.With(figtree.Options{ + Harvest: 9, + IgnoreEnvironment: true, + ConfigFile: env.String(eConfigFile, "./config.yaml"), + }) + + // properties define new fig fruits on the figtree + figs = figs.NewString(kSourceDir, ".", "Absolute path of directory you want to summarize.") + figs = figs.NewString(kOutputDir, filepath.Join(".", "summaries"), fmt.Sprintf("Path of the directory to write the %s file to", newSummaryFilename())) + figs = figs.NewString(kFilename, newSummaryFilename(), "Output file of summary.md") + figs = figs.NewList(kIncludeExt, defaultInclude, "List of extensions to INCLUDE in summary.") + figs = figs.NewList(kExcludeExt, defaultExclude, "List of extensions to EXCLUDE in summary.") + figs = figs.NewList(kSkipContains, defaultAvoid, "List of path substrings if present to skip over full path.") + figs = figs.NewInt(kMaxFiles, 369, "Maximum number of files to process concurrently") + figs = figs.NewInt64(kMaxOutputSize, 1_776_369, "Maximum file size of output file") + figs = figs.NewBool(kDotFiles, false, "Any path that is considered a dotfile can be included by setting this to true") + figs = figs.NewBool(kPrint, env.Bool(eAlwaysPrint, false), "Print generated file contents to STDOUT") + figs = figs.NewBool(kWrite, env.Bool(eAlwaysWrite, false), "Write generated contents to file") + figs = figs.NewBool(kJson, env.Bool(eAlwaysJson, false), "Enable JSON formatting") + figs = figs.NewBool(kCompress, env.Bool(eAlwaysCompress, false), "Use gzip compression in output") + figs = figs.NewBool(kVersion, false, "Display current version of summarize") + figs = figs.NewBool(kDebug, false, "Enable debug mode") + figs = figs.NewBool(kShowExpanded, false, "Show expand menu") + figs = figs.NewBool(kChat, false, "AI chat session with transcript based on new summary information in summary after") + + // ai mode + figs = figs.NewBool(kAiEnabled, env.Bool(eDisableAi, false) == false, "Enable AI Features") + figs = figs.NewString(kAiProvider, env.String(eAiProvider, dAiProvider), "AI Provider to use. (eg. ollama, openai, claude)") + figs = figs.NewString(kAiModel, env.String(eAiModel, dAiModel), "AI Model to use for query") + figs = figs.NewInt(kAiMaxTokens, env.Int(eAiMaxTokens, dAiMaxTokens), "AI Max Tokens to use for query") + figs = figs.NewInt(kAiSeed, env.Int(eAiSeed, dAiSeed), "AI Seed to use for query") + figs = figs.NewString(kAiApiKey, env.String(eAiApiKey, ""), "AI API Key to use for query (leave empty for ollama)") + figs = figs.NewInt(kMemory, env.Int(eAiMemory, dMemory), "AI Memory to use for query") + figs = figs.NewBool(kAiCachingEnabled, env.Bool(eAiAlwaysEnableCache, dCachingEnabled), "Enable LLM caching") + figs = figs.NewUnitDuration(kAiTimeout, env.UnitDuration(eAiGlobalTimeout, dTimeoutUnit, dTimeout), dTimeoutUnit, "AI Timeout on each request allowed") + + // validators run internal figtree Assure funcs as arguments to validate against + figs = figs.WithValidator(kSourceDir, figtree.AssureStringNotEmpty) + figs = figs.WithValidator(kOutputDir, figtree.AssureStringNotEmpty) + figs = figs.WithValidator(kFilename, figtree.AssureStringNotEmpty) + figs = figs.WithValidator(kMaxFiles, figtree.AssureIntInRange(1, 369)) + figs = figs.WithValidator(kMemory, figtree.AssureIntInRange(1, 17_369_369)) + figs = figs.WithValidator(kMaxOutputSize, figtree.AssureInt64InRange(369, 369_369_369_369)) + figs = figs.WithValidator(kAiSeed, figtree.AssureIntInRange(-1, 369_369_369_369)) + figs = figs.WithValidator(kAiMaxTokens, figtree.AssureIntInRange(-1, 369_369_369_369)) + + // callbacks as figtree.CallbackAfterVerify run after the Validators above finish + figs = figs.WithCallback(kSourceDir, figtree.CallbackAfterVerify, callbackVerifyReadableDirectory) + figs = figs.WithCallback(kFilename, figtree.CallbackAfterVerify, callbackVerifyFile) +} diff --git a/const.go b/const.go new file mode 100644 index 0000000..5d20981 --- /dev/null +++ b/const.go @@ -0,0 +1,111 @@ +package main + +import "time" + +const ( + projectName string = "github.com/andreimerlescu/summarize" + tFormat string = "2006.01.02.15.04.05.UTC" + + // eConfigFile ENV string of path to .yml|.yaml|.json|.ini file + eConfigFile string = "SUMMARIZE_CONFIG_FILE" + + // eAddIgnoreInPathList ENV string (comma separated list) of substrings to ignore if path contains + eAddIgnoreInPathList string = "SUMMARIZE_IGNORE_CONTAINS" + + // eAddIncludeExtList ENV string (comma separated list) of acceptable file extensions of any scanned path + eAddIncludeExtList string = "SUMMARIZE_INCLUDE_EXT" + + // eAddExcludeExtList ENV string (comma separated list) of rejected file extensions of any scanned path + eAddExcludeExtList string = "SUMMARIZE_EXCLUDE_EXT" + + // eAlwaysWrite ENV string-as-bool (as "TRUE" or "true" for true) always sets -write true in CLI argument parsing + eAlwaysWrite string = "SUMMARIZE_ALWAYS_WRITE" + + // eAlwaysPrint ENV string-as-bool (as "TRUE" or "true" for true) always sets -print true in CLI argument parsing + eAlwaysPrint string = "SUMMARIZE_ALWAYS_PRINT" + + // eAlwaysJson ENV string-as-bool (as "TRUE" or "true" for true) always sets -json true in CLI argument parsing + eAlwaysJson string = "SUMMARIZE_ALWAYS_JSON" + + // eAlwaysCompress ENV string-as-bool (as "TRUE" or "true" for true) always sets -gz true in CLI argument parsing + eAlwaysCompress string = "SUMMARIZE_ALWAYS_COMPRESS" + + eDisableAi string = "SUMMARIZE_DISABLE_AI" + eAiProvider string = "SUMMARIZE_AI_PROVIDER" + eAiModel string = "SUMMARIZE_AI_MODEL" + eAiApiKey string = "SUMMARIZE_AI_API_KEY" + eAiMaxTokens string = "SUMMARIZE_AI_MAX_TOKENS" + eAiSeed string = "SUMMARIZE_AI_SEED" + eAiMemory string = "SUMMARIZE_AI_MEMORY" + eAiAlwaysEnableCache string = "SUMMARIZE_AI_ENABLE_CACHE" + eAiGlobalTimeout string = "SUMMARIZE_AI_GLOBAL_TIMEOUT" + + dAiSeed int = -1 + dAiMaxTokens int = 3000 + dAiProvider string = "ollama" + dAiModel string = "qwen3:8b" + // dAiModel string = "mistral-small3.2:24b" + dCachingEnabled bool = true + dMemory int = 36963 + dTimeout time.Duration = 77 + dTimeoutUnit time.Duration = time.Second + + kAiEnabled string = "ai" + kAiProvider string = "provider" + kAiModel string = "model" + kAiApiKey string = "api-key" + kAiMaxTokens string = "max-tokens" + kAiSeed string = "seed" + kMemory string = "memory" + kAiCachingEnabled string = "caching" + kAiTimeout string = "timeout" + + kShowExpanded string = "expand" + + kChat string = "chat" + + // kSourceDir figtree fig string -d for the directory path to generate a summary of + kSourceDir string = "d" + + // kOutputDir figtree fig string -o for the output directory where the summary is saved + kOutputDir string = "o" + + // kIncludeExt figtree fig list (string-as-list aka comma separated list) -i for the extensions to summarize + kIncludeExt string = "i" + + // kExcludeExt figtree fig list (string-as-list aka comma separated list) -x for the extensions NOT to summarize + kExcludeExt string = "x" + + // kSkipContains figtree fig list (string-as-list aka comma separated list) -s for the substrings in the paths to ignore + kSkipContains string = "s" + + // kFilename figtree fig string -f is the name of the file to save inside kOutputDir + kFilename string = "f" + + // kPrint figtree fig bool -print will render to STDOUT the contents of the summary + kPrint string = "print" + + // kMaxOutputSize figtree fig int64 -max will stop summarizing the kSourceDir once kFilename reaches this size in bytes + kMaxOutputSize string = "max" + + // kWrite figtree fig bool -write will write the summary to the kFilename in the kSourceDir + kWrite string = "write" + + // kVersion figtree fig bool -v will display the current version of the binary + kVersion string = "v" + + // kDotFiles figtree fig bool -ndf will skip over any directory that has a prefix of "." + kDotFiles string = "ndf" + + // kMaxFiles figtree fig int64 -mf will specify the maximum number of files that will concurrently be summarized + kMaxFiles string = "mf" + + // kDebug figtree fig bool -debug will render addition log statements to STDOUT + kDebug string = "debug" + + // kJson figtree fig bool -json will render the output as JSON to the console's STDOUT only + kJson string = "json" + + // kCompress figtree fig bool -gz will gzip compress the contents of kFilename that is written to kOutputDir + kCompress string = "gz" +) diff --git a/env.go b/env.go new file mode 100644 index 0000000..4191642 --- /dev/null +++ b/env.go @@ -0,0 +1,13 @@ +package main + +import ( + "github.com/andreimerlescu/goenv/env" +) + +// addFromEnv takes a pointer to a slice of strings and a new ENV os.LookupEnv name to return the figtree ToList on the Flesh that sends the list into simplify before being returned +func addFromEnv(e string, l *[]string) { + for _, entry := range env.List(e, []string{}) { + *l = append(*l, entry) + } + *l = simplify(*l) +} diff --git a/go.mod b/go.mod index 4eb1111..e4b19aa 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,57 @@ go 1.24.5 require ( github.com/andreimerlescu/checkfs v1.0.4 github.com/andreimerlescu/figtree/v2 v2.0.14 + github.com/andreimerlescu/goenv v0.0.0-20250810022511-93d6119cf4da github.com/andreimerlescu/sema v1.0.0 + github.com/charmbracelet/bubbles v0.21.0 + github.com/charmbracelet/bubbletea v1.3.6 + github.com/charmbracelet/lipgloss v1.1.0 + github.com/muesli/reflow v0.3.0 + github.com/teilomillet/gollm v0.1.9 ) require ( - github.com/andreimerlescu/bump v1.0.3 // indirect + github.com/andreimerlescu/env v0.0.1 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/caarlos0/env/v11 v11.3.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.9.3 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/go-ini/ini v1.67.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/invopop/jsonschema v0.13.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/pkoukk/tiktoken-go v0.1.7 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.10.0 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/term v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b51abf5..a3ed763 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,118 @@ -github.com/andreimerlescu/bump v1.0.3 h1:RAmNPjS8lGhgiBhiTMEaRl1ydex7Z3YYuyiQohC+ShY= -github.com/andreimerlescu/bump v1.0.3/go.mod h1:ud9Sqvt+zM0sBDhK3Dghq2hGTWrlVIvMqLAzpWQjIy0= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/andreimerlescu/checkfs v1.0.4 h1:pRXZGW1sfe+yXyWNUxmPC2IiX5yT3vF1V5O8PXulnFc= github.com/andreimerlescu/checkfs v1.0.4/go.mod h1:ADaqjiRJf3gmyENLS3v9bJIaEH00IOeM48cXxVwy1JY= -github.com/andreimerlescu/figtree/v2 v2.0.10 h1:UWKBVpwa4lI+mp3VxUy7MzkzaigROZd4zOGJrarNpv0= -github.com/andreimerlescu/figtree/v2 v2.0.10/go.mod h1:PymPGUzzP/UuxZ4mqC5JIrDZJIVcjZ3GMc/MC2GB6Ek= +github.com/andreimerlescu/env v0.0.1 h1:R3DTdLyRSuKLTHvlmTaHLloVpTnrHrX/JGbjM/28cl8= +github.com/andreimerlescu/env v0.0.1/go.mod h1:Jzu9qHPCv7c0o/rBmagt6z6Kf49K3ba0WIpi0D2YYUg= github.com/andreimerlescu/figtree/v2 v2.0.14 h1:pwDbHpfiAdSnaNnxyV2GpG1rG9cmGiHhjXOvBEoVj2w= github.com/andreimerlescu/figtree/v2 v2.0.14/go.mod h1:PymPGUzzP/UuxZ4mqC5JIrDZJIVcjZ3GMc/MC2GB6Ek= +github.com/andreimerlescu/goenv v0.0.0-20250810022511-93d6119cf4da h1:uTPeoV4F7+gFcxLCgyqJFHvOhfaKVahp2BALC5zw0eg= +github.com/andreimerlescu/goenv v0.0.0-20250810022511-93d6119cf4da/go.mod h1:ehn80sBXiQAH4v3FUUVze1Oh23OeHWIk/gYwxTl6WKw= github.com/andreimerlescu/sema v1.0.0 h1:8ai/kqAci7QKUenAJWX13aYtWpjvD0CQW39CFzNIRQs= github.com/andreimerlescu/sema v1.0.0/go.mod h1:VCRQkKVknOKKPtAqvrNHL7hxxfoX5O7it2lWBzVxUs0= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= +github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= +github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= +github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw= +github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +github.com/teilomillet/gollm v0.1.9 h1:1VwknVFVF7RvSv5ajqEYLhQAUi3X3PgmgPG1ipvmBe0= +github.com/teilomillet/gollm v0.1.9/go.mod h1:RBxoPOa1DfkqCy3ll68p6AplCvuRmiDkz0DwhE9J67s= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/gz.go b/gz.go new file mode 100644 index 0000000..bf1a033 --- /dev/null +++ b/gz.go @@ -0,0 +1,40 @@ +package main + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" +) + +// compress compresses a string using gzip and returns the compressed bytes +func compress(s []byte) ([]byte, error) { + var buf bytes.Buffer + gzWriter := gzip.NewWriter(&buf) + _, err := gzWriter.Write(s) + if err != nil { + return nil, fmt.Errorf("failed to write to gzip writer: %w", err) + } + err = gzWriter.Close() + if err != nil { + return nil, fmt.Errorf("failed to close gzip writer: %w", err) + } + return buf.Bytes(), nil +} + +// decompress decompresses gzip compressed bytes back to a string +func decompress(compressed []byte) (string, error) { + buf := bytes.NewReader(compressed) + gzReader, err := gzip.NewReader(buf) + if err != nil { + return "", fmt.Errorf("failed to create gzip reader: %w", err) + } + defer func() { + _ = gzReader.Close() + }() + decompressed, err := io.ReadAll(gzReader) + if err != nil { + return "", fmt.Errorf("failed to read from gzip reader: %w", err) + } + return string(decompressed), nil +} diff --git a/main.go b/main.go index c1f87ec..a1fb34c 100644 --- a/main.go +++ b/main.go @@ -2,213 +2,57 @@ package main import ( "bytes" - "compress/gzip" - "embed" "encoding/json" "fmt" - "io" "io/fs" "os" "path/filepath" "runtime" "slices" - "strconv" "strings" "sync" "sync/atomic" - "time" check "github.com/andreimerlescu/checkfs" "github.com/andreimerlescu/checkfs/directory" - "github.com/andreimerlescu/checkfs/file" - "github.com/andreimerlescu/figtree/v2" "github.com/andreimerlescu/sema" ) -//go:embed VERSION -var versionBytes embed.FS +func main() { + configure() + capture("figs loading environment", figs.Load()) -var currentVersion string + isDebug := *figs.Bool(kDebug) -func Version() string { - if len(currentVersion) == 0 { - versionBytes, err := versionBytes.ReadFile("VERSION") - if err != nil { - return "" - } - currentVersion = strings.TrimSpace(string(versionBytes)) + inc := *figs.List(kIncludeExt) + if len(inc) == 1 && inc[0] == "useExpanded" { + figs.StoreList(kIncludeExt, extendedDefaultInclude) } - return currentVersion -} - -const ( - projectName string = "github.com/andreimerlescu/summarize" - tFormat string = "2006.01.02.15.04.05.UTC" - - eConfigFile string = "SUMMARIZE_CONFIG_FILE" - eAddIgnoreInPathList string = "SUMMARIZE_IGNORE_CONTAINS" - eAddIncludeExtList string = "SUMMARIZE_INCLUDE_EXT" - eAddExcludeExtList string = "SUMMARIZE_EXCLUDE_EXT" - eAlwaysWrite string = "SUMMARIZE_ALWAYS_WRITE" - eAlwaysPrint string = "SUMMARIZE_ALWAYS_PRINT" - eAlwaysJson string = "SUMMARIZE_ALWAYS_JSON" - eAlwaysCompress string = "SUMMARIZE_ALWAYS_COMPRESS" - - kSourceDir string = "d" - kOutputDir string = "o" - kIncludeExt string = "i" - kExcludeExt string = "x" - kSkipContains string = "s" - kFilename string = "f" - kPrint string = "print" - kMaxOutputSize string = "max" - kWrite string = "write" - kVersion string = "v" - kDotFiles string = "ndf" - kMaxFiles string = "mf" - kDebug string = "debug" - kJson string = "json" - kCompress string = "gz" -) - -var ( - // figs is a figtree of fruit for configurable command line arguments that bear fruit - figs figtree.Plant - - alwaysWrite = true - - // defaultExclude are the -exc list of extensions that will be skipped automatically - defaultExclude = []string{ - // Compressed archives - "7z", "gz", "xz", "zst", "zstd", "bz", "bz2", "bzip2", "zip", "tar", "rar", "lz4", "lzma", "cab", "arj", - - // Encryption, certificates, and sensitive keys - "crt", "cert", "cer", "key", "pub", "asc", "pem", "p12", "pfx", "jks", "keystore", - "id_rsa", "id_dsa", "id_ed25519", "id_ecdsa", "gpg", "pgp", - - // Binary & executable artifacts - "exe", "dll", "so", "dylib", "bin", "out", "o", "obj", "a", "lib", "dSYM", - "class", "pyc", "pyo", "__pycache__", - "jar", "war", "ear", "apk", "ipa", "dex", "odex", - "wasm", "node", "beam", "elc", - - // System and disk images - "iso", "img", "dmg", "vhd", "vdi", "vmdk", "qcow2", - - // Database files - "db", "sqlite", "sqlite3", "db3", "mdb", "accdb", "sdf", "ldb", - - // Log files - "log", "trace", "dump", "crash", - - // Media files - Images - "jpg", "jpeg", "png", "gif", "bmp", "tiff", "tif", "webp", "ico", "svg", "heic", "heif", "raw", "cr2", "nef", "dng", - - // Media files - Audio - "mp3", "wav", "flac", "aac", "ogg", "wma", "m4a", "opus", "aiff", - - // Media files - Video - "mp4", "avi", "mov", "mkv", "webm", "flv", "wmv", "m4v", "3gp", "ogv", - - // Font files - "ttf", "otf", "woff", "woff2", "eot", "fon", "pfb", "pfm", - - // Document formats (typically not source code) - "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "odt", "ods", "odp", "rtf", - - // IDE/Editor/Tooling artifacts - "suo", "sln", "user", "ncb", "pdb", "ipch", "ilk", "tlog", "idb", "aps", "res", - "iml", "idea", "vscode", "project", "classpath", "factorypath", "prefs", - "vcxproj", "vcproj", "filters", "xcworkspace", "xcuserstate", "xcscheme", "pbxproj", - "DS_Store", "Thumbs.db", "desktop.ini", - - // Package manager and build artifacts - "lock", "sum", "resolved", // package-lock.json, go.sum, yarn.lock, etc. - - // Temporary and backup files - "tmp", "temp", "swp", "swo", "bak", "backup", "orig", "rej", "patch", - "~", "old", "new", "part", "incomplete", - - // Source maps and minified files (usually generated) - "map", "min.js", "min.css", "bundle.js", "bundle.css", "chunk.js", - - // Configuration that's typically binary or generated - "dat", "data", "cache", "pid", "sock", - // Version control artifacts (though usually in ignored directories) - "pack", "idx", "rev", - - // Other binary formats - "pickle", "pkl", "npy", "npz", "mat", "rdata", "rds", + exc := *figs.List(kExcludeExt) + if len(exc) == 1 && exc[0] == "useExpanded" { + figs.StoreList(kExcludeExt, extendedDefaultExclude) } - // defaultInclude are the -inc list of extensions that will be included in the summary - defaultInclude = []string{ - "go", "ts", "tf", "sh", "py", "js", "Makefile", "mod", "Dockerfile", "dockerignore", "gitignore", "esconfigs", "md", + ski := *figs.List(kSkipContains) + if len(ski) == 1 && ski[0] == "useExpanded" { + figs.StoreList(kSkipContains, extendedDefaultAvoid) } - // defaultAvoid are the -avoid list of substrings in file path names to avoid in the summary - defaultAvoid = []string{ - ".min.js", ".min.css", ".git/", ".svn/", ".vscode/", ".vs/", ".idea/", "logs/", "secrets/", - ".venv/", "/site-packages", ".terraform/", "summaries/", "node_modules/", "/tmp", "tmp/", "logs/", + if isDebug { + fmt.Println("INCLUDE: ", strings.Join(inc, ", ")) + fmt.Println("EXCLUDE: ", strings.Join(exc, ", ")) + fmt.Println("SKIP: ", strings.Join(ski, ", ")) } -) -// newSummaryFilename returns summary.time.Now().UTC().Format(tFormat).md -var newSummaryFilename = func() string { - return fmt.Sprintf("summary.%s.md", time.Now().UTC().Format(tFormat)) -} - -// init creates a new figtree with options to use CONFIG_FILE as a way of reading a YAML file while ignoring the env -func configure() { - figs = figtree.With(figtree.Options{ - Harvest: 9, - IgnoreEnvironment: true, - ConfigFile: envVal(eConfigFile, "./config.yaml"), - }) - // properties - figs = figs.NewString(kSourceDir, ".", "Absolute path of directory you want to summarize.") - figs = figs.NewString(kOutputDir, filepath.Join(".", "summaries"), fmt.Sprintf("Path of the directory to write the %s file to", newSummaryFilename())) - figs = figs.NewString(kFilename, newSummaryFilename(), "Output file of summary.md") - figs = figs.NewList(kIncludeExt, defaultInclude, "List of extensions to INCLUDE in summary.") - figs = figs.NewList(kExcludeExt, defaultExclude, "List of extensions to EXCLUDE in summary.") - figs = figs.NewList(kSkipContains, defaultAvoid, "List of path substrings if present to skip over full path.") - figs = figs.NewInt(kMaxFiles, 369, "Maximum number of files to process concurrently") - figs = figs.NewInt64(kMaxOutputSize, 1_776_369, "Maximum file size of output file") - figs = figs.NewBool(kDotFiles, false, "Any path that is considered a dotfile can be included by setting this to true") - figs = figs.NewBool(kPrint, envIs(eAlwaysPrint), "Print generated file contents to STDOUT") - figs = figs.NewBool(kWrite, envIs(eAlwaysWrite), "Write generated contents to file") - figs = figs.NewBool(kJson, envIs(eAlwaysJson), "Enable JSON formatting") - figs = figs.NewBool(kCompress, envIs(eAlwaysCompress), "Use gzip compression in output") - figs = figs.NewBool(kVersion, false, "Display current version of summarize") - figs = figs.NewBool(kDebug, false, "Enable debug mode") - // validators - figs = figs.WithValidator(kSourceDir, figtree.AssureStringNotEmpty) - figs = figs.WithValidator(kOutputDir, figtree.AssureStringNotEmpty) - figs = figs.WithValidator(kFilename, figtree.AssureStringNotEmpty) - figs = figs.WithValidator(kMaxFiles, figtree.AssureIntInRange(1, 17_369)) - figs = figs.WithValidator(kMaxOutputSize, figtree.AssureInt64InRange(369, 369_369_369_369)) - // callbacks - figs = figs.WithCallback(kSourceDir, figtree.CallbackAfterVerify, callbackVerifyReadableDirectory) - figs = figs.WithCallback(kFilename, figtree.CallbackAfterVerify, callbackVerifyFile) -} - -type result struct { - Path string `yaml:"path" json:"path"` - Contents []byte `yaml:"contents" json:"contents"` - Size int64 `yaml:"size" json:"size"` -} - -type final struct { - Path string `yaml:"path" json:"path"` - Contents string `yaml:"contents" json:"contents"` - Size int64 `yaml:"size" json:"size"` -} + if *figs.Bool(kShowExpanded) { + fmt.Println("Expanded:") + fmt.Printf("-%s=%s\n", kIncludeExt, strings.Join(*figs.List(kIncludeExt), ",")) + fmt.Printf("-%s=%s\n", kExcludeExt, strings.Join(*figs.List(kExcludeExt), ",")) + fmt.Printf("-%s=%s\n", kSkipContains, strings.Join(*figs.List(kSkipContains), ",")) + os.Exit(0) + } -func main() { - configure() - capture("figs loading environment", figs.Load()) - isDebug := *figs.Bool(kDebug) if *figs.Bool(kVersion) { fmt.Println(Version()) os.Exit(0) @@ -368,7 +212,7 @@ func main() { } maxFileSemaphore := sema.New(*figs.Int(kMaxFiles)) - resultsChan := make(chan result, *figs.Int(kMaxFiles)) + resultsChan := make(chan Result, *figs.Int(kMaxFiles)) writerWG := sync.WaitGroup{} writerWG.Add(1) go func() { @@ -391,9 +235,9 @@ func main() { buf.WriteString("### Workspace\n\n") abs, err := filepath.Abs(srcDir) if err == nil { - buf.WriteString("" + abs + "\n\n") + buf.WriteString("`" + abs + "`\n\n") } else { - buf.WriteString("" + srcDir + "\n\n") + buf.WriteString("`" + srcDir + "`\n\n") } renderMu := &sync.Mutex{} @@ -413,6 +257,23 @@ func main() { renderMu.Unlock() } + if *figs.Bool(kChat) { + StartChat(&buf) + path := latestChatLog() + contents, err := os.ReadFile(path) + if err == nil { + old := buf.String() + buf.Reset() + buf.WriteString("## Chat Log \n\n") + body := string(contents) + body = strings.ReplaceAll(body, "You: ", "\n### ") + buf.WriteString(body) + buf.WriteString("\n\n") + buf.WriteString("## Summary \n\n") + buf.WriteString(old) + } + } + shouldPrint := *figs.Bool(kPrint) canWrite := *figs.Bool(kWrite) showJson := *figs.Bool(kJson) @@ -438,7 +299,7 @@ func main() { if shouldPrint { if showJson { - r := final{ + r := Final{ Path: outputFileName, Size: int64(buf.Len()), Contents: buf.String(), @@ -471,8 +332,8 @@ func main() { } paths := slices.Clone(thisData.Paths) - throttler.Acquire() // throttler is used to protect the runtime from excessive use - wg.Add(1) // wg is used to prevent the runtime from exiting early + throttler.Acquire() // throttler is used to protect the runtime from excessive use + wg.Add(1) // wg is used to prevent the runtime from exiting early go func(innerData *mapData, toUpdate *[]mapData, ext string, paths []string) { // run this extension in a goroutine defer throttler.Release() // when we're done, release the throttler defer wg.Done() // then tell the sync.WaitGroup that we are done @@ -539,7 +400,7 @@ func main() { content = []byte{} // clear memory after its written sb.WriteString("\n```\n\n") // close out the file footer seen.Add(filePath) - resultsChan <- result{ + resultsChan <- Result{ Path: filePath, Contents: sb.Bytes(), Size: int64(sb.Len()), @@ -566,7 +427,7 @@ func main() { // Print completion message if *figs.Bool(kJson) { - r := m{ + r := M{ Message: fmt.Sprintf("Summary generated: %s\n", filepath.Join(*figs.String(kOutputDir), *figs.String(kFilename)), ), @@ -583,176 +444,3 @@ func main() { ) } } - -var callbackVerifyFile = func(value interface{}) error { - return check.File(toString(value), file.Options{Exists: false}) -} - -var callbackVerifyReadableDirectory = func(value interface{}) error { - return check.Directory(toString(value), directory.Options{Exists: true, MorePermissiveThan: 0444}) -} - -var toString = func(value interface{}) string { - switch v := value.(type) { - case string: - return v - case *string: - return *v - default: - flesh := figtree.NewFlesh(value) - f := fmt.Sprintf("%v", flesh.ToString()) - return f - } -} - -var capture = func(msg string, d ...error) { - if len(d) == 0 || (len(d) == 1 && d[0] == nil) { - return - } - terminate(os.Stderr, "[EXCUSE ME, BUT] %s\n\ncaptured error: %v\n", msg, d) -} - -type m struct { - Message string `json:"message"` -} - -var terminate = func(d io.Writer, i string, e ...interface{}) { - for _, f := range os.Args { - if strings.HasPrefix(f, "-json") { - mm := m{Message: fmt.Sprintf(i, e...)} - jb, err := json.MarshalIndent(mm, "", " ") - if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "Error serializing json: %v\n", err) - _, _ = fmt.Fprintf(d, i, e...) - } else { - fmt.Println(string(jb)) - } - os.Exit(1) - } - } - _, _ = fmt.Fprintf(d, i, e...) - os.Exit(1) -} - -func simplify(t []string) []string { - seen := make(map[string]bool) - for _, v := range t { - seen[v] = true - } - results := make([]string, len(t)) - for i, v := range t { - if seen[v] { - results[i] = v - } - } - return results -} -func addFromEnv(e string, l *[]string) { - v, ok := os.LookupEnv(e) - if ok { - flesh := figtree.NewFlesh(v) - maybeAdd := flesh.ToList() - for _, entry := range maybeAdd { - *l = append(*l, entry) - } - } - *l = simplify(*l) -} - -type seenStrings struct { - mu sync.RWMutex - m map[string]bool -} - -func (s *seenStrings) Add(entry string) { - s.mu.Lock() - defer s.mu.Unlock() - s.m[entry] = true -} -func (s *seenStrings) Remove(entry string) { - s.mu.Lock() - defer s.mu.Unlock() - delete(s.m, entry) -} - -func (s *seenStrings) Len() int { - s.mu.RLock() - defer s.mu.RUnlock() - return len(s.m) -} - -func (s *seenStrings) String() string { - s.mu.RLock() - defer s.mu.RUnlock() - return fmt.Sprint(s.m) -} - -func (s *seenStrings) True(entry string) { - s.mu.Lock() - defer s.mu.Unlock() - s.m[entry] = true -} - -func (s *seenStrings) False(entry string) { - s.mu.Lock() - defer s.mu.Unlock() - delete(s.m, entry) -} - -func (s *seenStrings) Exists(entry string) bool { - s.mu.RLock() - defer s.mu.RUnlock() - return s.m[entry] -} - -func envVal(name, fallback string) string { - v, ok := os.LookupEnv(name) - if !ok { - return fallback - } - return v -} - -func envIs(name string) bool { - v, ok := os.LookupEnv(name) - if !ok { - return false - } - vb, err := strconv.ParseBool(v) - if err != nil { - return false - } - return vb -} - -// compress compresses a string using gzip and returns the compressed bytes -func compress(s []byte) ([]byte, error) { - var buf bytes.Buffer - gzWriter := gzip.NewWriter(&buf) - _, err := gzWriter.Write(s) - if err != nil { - return nil, fmt.Errorf("failed to write to gzip writer: %w", err) - } - err = gzWriter.Close() - if err != nil { - return nil, fmt.Errorf("failed to close gzip writer: %w", err) - } - return buf.Bytes(), nil -} - -// decompress decompresses gzip compressed bytes back to a string -func decompress(compressed []byte) (string, error) { - buf := bytes.NewReader(compressed) - gzReader, err := gzip.NewReader(buf) - if err != nil { - return "", fmt.Errorf("failed to create gzip reader: %w", err) - } - defer func() { - _ = gzReader.Close() - }() - decompressed, err := io.ReadAll(gzReader) - if err != nil { - return "", fmt.Errorf("failed to read from gzip reader: %w", err) - } - return string(decompressed), nil -} diff --git a/reflect.go b/reflect.go new file mode 100644 index 0000000..ebae251 --- /dev/null +++ b/reflect.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" +) + +func latestChatLog() string { + entries, err := os.ReadDir(kOutputDir) + if err != nil { + if *figs.Bool(kDebug) { + _, _ = fmt.Fprintf(os.Stderr, "Error reading directory %s: %v\n", kOutputDir, err) + } + return "" + } + + var latestFile string + var latestModTime time.Time + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + filename := entry.Name() + if strings.HasPrefix(filename, "chatlog.") && strings.HasSuffix(filename, ".md") { + fullPath := filepath.Join(kOutputDir, filename) + + fileInfo, err := entry.Info() + if err != nil { + if *figs.Bool(kDebug) { + _, _ = fmt.Fprintf(os.Stderr, "Error getting file info for %s: %v\n", fullPath, err) + } + continue + } + + modTime := fileInfo.ModTime() + if latestFile == "" || modTime.After(latestModTime) { + latestFile = fullPath + latestModTime = modTime + } + } + } + + if latestFile == "" && *figs.Bool(kDebug) { + _, _ = fmt.Fprintf(os.Stderr, "No summary files found in directory %s\n", kOutputDir) + } + + return latestFile +} diff --git a/simplify.go b/simplify.go new file mode 100644 index 0000000..081a035 --- /dev/null +++ b/simplify.go @@ -0,0 +1,14 @@ +package main + +// simplify takes a list of strings and reduces duplicates from the slice +func simplify(t []string) []string { + seen := make(map[string]bool) + results := make([]string, 0) + for _, v := range t { + if !seen[v] { + seen[v] = true + results = append(results, v) + } + } + return results +} diff --git a/type.go b/type.go new file mode 100644 index 0000000..f9a8f93 --- /dev/null +++ b/type.go @@ -0,0 +1,33 @@ +package main + +import ( + "sync" +) + +type ( + + // Result contains the scanned path in the kSourceDir that matched the conditions and shall be included in the Final summary of kFilename + Result struct { + Path string `yaml:"path" json:"path"` + Contents []byte `yaml:"contents" json:"contents"` + Size int64 `yaml:"size" json:"size"` + } + + // Final contains the rendered Result of the matched path that gets written to kFilename + Final struct { + Path string `yaml:"path" json:"path"` + Contents string `yaml:"contents" json:"contents"` + Size int64 `yaml:"size" json:"size"` + } + + // M defines a Message that should be rendered to JSON + M struct { + Message string `json:"message"` + } + + // seenStrings captures a concurrent safe map of strings and booleans that indicate whether the string has been seen + seenStrings struct { + mu sync.RWMutex + m map[string]bool + } +) diff --git a/type_funcs.go b/type_funcs.go new file mode 100644 index 0000000..3800f00 --- /dev/null +++ b/type_funcs.go @@ -0,0 +1,52 @@ +package main + +import "fmt" + +// Add inserts an entry into the map +func (s *seenStrings) Add(entry string) { + s.mu.Lock() + defer s.mu.Unlock() + s.m[entry] = true +} + +// Remove uses delete on the entry in the map +func (s *seenStrings) Remove(entry string) { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.m, entry) +} + +// Len returns the length of the map +func (s *seenStrings) Len() int { + s.mu.RLock() + defer s.mu.RUnlock() + return len(s.m) +} + +// String implements the Stringer interface +func (s *seenStrings) String() string { + s.mu.RLock() + defer s.mu.RUnlock() + return fmt.Sprint(s.m) +} + +// True sets the entry to true +func (s *seenStrings) True(entry string) { + s.mu.Lock() + defer s.mu.Unlock() + s.m[entry] = true +} + +// False sets the entry to false +func (s *seenStrings) False(entry string) { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.m, entry) +} + +// Exists returns a bool if the map contains the entry +func (s *seenStrings) Exists(entry string) bool { + s.mu.RLock() + defer s.mu.RUnlock() + return s.m[entry] +} diff --git a/var.go b/var.go new file mode 100644 index 0000000..a6b83ba --- /dev/null +++ b/var.go @@ -0,0 +1,97 @@ +package main + +import ( + "github.com/andreimerlescu/figtree/v2" +) + +var ( + // figs is a figtree of fruit for configurable command line arguments that bear fruit + figs figtree.Plant + + defaultExclude = []string{ + "useExpanded", + } + // defaultExclude are the -exc list of extensions that will be skipped automatically + extendedDefaultExclude = []string{ + // Compressed archives + "7z", "gz", "xz", "zst", "zstd", "bz", "bz2", "bzip2", "zip", "tar", "rar", "lz4", "lzma", "cab", "arj", + + // Encryption, certificates, and sensitive keys + "crt", "cert", "cer", "key", "pub", "asc", "pem", "p12", "pfx", "jks", "keystore", + "id_rsa", "id_dsa", "id_ed25519", "id_ecdsa", "gpg", "pgp", + + // Binary & executable artifacts + "exe", "dll", "so", "dylib", "bin", "out", "o", "obj", "a", "lib", "dSYM", + "class", "pyc", "pyo", "__pycache__", + "jar", "war", "ear", "apk", "ipa", "dex", "odex", + "wasm", "node", "beam", "elc", + + // System and disk images + "iso", "img", "dmg", "vhd", "vdi", "vmdk", "qcow2", + + // Database files + "db", "sqlite", "sqlite3", "db3", "mdb", "accdb", "sdf", "ldb", + + // Log files + "log", "trace", "dump", "crash", + + // Media files - Images + "jpg", "jpeg", "png", "gif", "bmp", "tiff", "tif", "webp", "ico", "svg", "heic", "heif", "raw", "cr2", "nef", "dng", + + // Media files - Audio + "mp3", "wav", "flac", "aac", "ogg", "wma", "m4a", "opus", "aiff", + + // Media files - Video + "mp4", "avi", "mov", "mkv", "webm", "flv", "wmv", "m4v", "3gp", "ogv", + + // Font files + "ttf", "otf", "woff", "woff2", "eot", "fon", "pfb", "pfm", + + // Document formats (typically not source code) + "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "odt", "ods", "odp", "rtf", + + // IDE/Editor/Tooling artifacts + "suo", "sln", "user", "ncb", "pdb", "ipch", "ilk", "tlog", "idb", "aps", "res", + "iml", "idea", "vscode", "project", "classpath", "factorypath", "prefs", + "vcxproj", "vcproj", "filters", "xcworkspace", "xcuserstate", "xcscheme", "pbxproj", + "DS_Store", "Thumbs.db", "desktop.ini", + + // Package manager and build artifacts + "lock", "sum", "resolved", // package-lock.json, go.sum, yarn.lock, etc. + + // Temporary and backup files + "tmp", "temp", "swp", "swo", "bak", "backup", "orig", "rej", "patch", + "~", "old", "new", "part", "incomplete", + + // Source maps and minified files (usually generated) + "map", "min.js", "min.css", "bundle.js", "bundle.css", "chunk.js", + + // Configuration that's typically binary or generated + "dat", "data", "cache", "pid", "sock", + + // Version control artifacts (though usually in ignored directories) + "pack", "idx", "rev", + + // Other binary formats + "pickle", "pkl", "npy", "npz", "mat", "rdata", "rds", + } + + // defaultInclude are the -inc list of extensions that will be included in the summary + defaultInclude = []string{ + "useExpanded", + } + + extendedDefaultInclude = []string{ + "go", "ts", "tf", "sh", "py", "js", "Makefile", "mod", "Dockerfile", "dockerignore", "gitignore", "esconfigs", "md", + } + + defaultAvoid = []string{ + "useExpanded", + } + + // extendedDefaultAvoid are the -avoid list of substrings in file path names to avoid in the summary + extendedDefaultAvoid = []string{ + ".min.js", ".min.css", ".git/", ".svn/", ".vscode/", ".vs/", ".idea/", "logs/", "secrets/", + ".venv/", "/site-packages", ".terraform/", "summaries/", "node_modules/", "/tmp", "tmp/", "logs/", + } +) diff --git a/var_funcs.go b/var_funcs.go new file mode 100644 index 0000000..69314e5 --- /dev/null +++ b/var_funcs.go @@ -0,0 +1,71 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "os" + "strings" + "time" + + check "github.com/andreimerlescu/checkfs" + "github.com/andreimerlescu/checkfs/directory" + "github.com/andreimerlescu/checkfs/file" + "github.com/andreimerlescu/figtree/v2" +) + +// newSummaryFilename returns summary.time.Now().UTC().Format(tFormat).md +var newSummaryFilename = func() string { + return fmt.Sprintf("summary.%s.md", time.Now().UTC().Format(tFormat)) +} + +// callbackVerifyFile is a figtree WithCallback on the kFilename fig that uses checkfs.File to validate the file does NOT already exist +var callbackVerifyFile = func(value interface{}) error { + return check.File(toString(value), file.Options{Exists: false}) +} + +// callbackVerifyReadableDirectory is a figtree WithCallback on the kSourceDir that uses checkfs.Directory to be More Permissive than 0444 +var callbackVerifyReadableDirectory = func(value interface{}) error { + return check.Directory(toString(value), directory.Options{Exists: true, MorePermissiveThan: 0444}) +} + +// toString uses figtree NewFlesh to return the ToString() value of the provided value argument +var toString = func(value interface{}) string { + switch v := value.(type) { + case string: + return v + case *string: + return *v + default: + flesh := figtree.NewFlesh(value) + f := fmt.Sprintf("%v", flesh.ToString()) + return f + } +} + +// capture assures that the d errors are not nil then runs terminate to write to os.Stderr +var capture = func(msg string, d ...error) { + if len(d) == 0 || (len(d) == 1 && d[0] == nil) { + return + } + terminate(os.Stderr, "[EXCUSE ME, BUT] %s\n\ncaptured error: %v\n", msg, d) +} + +// terminate can write to os.Stderr or os.Stdout with a fmt.Fprintf format as i and a variadic interface of e that gets rendered to d either in plain text or as JSON +var terminate = func(d io.Writer, i string, e ...interface{}) { + for _, f := range os.Args { + if strings.HasPrefix(f, "-json") { + mm := M{Message: fmt.Sprintf(i, e...)} + jb, err := json.MarshalIndent(mm, "", " ") + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Error serializing json: %v\n", err) + _, _ = fmt.Fprintf(d, i, e...) + } else { + fmt.Println(string(jb)) + } + os.Exit(1) + } + } + _, _ = fmt.Fprintf(d, i, e...) + os.Exit(1) +} diff --git a/version.go b/version.go new file mode 100644 index 0000000..8b3bbd5 --- /dev/null +++ b/version.go @@ -0,0 +1,22 @@ +package main + +import ( + "embed" + "strings" +) + +//go:embed VERSION +var versionBytes embed.FS + +var currentVersion string + +func Version() string { + if len(currentVersion) == 0 { + versionBytes, err := versionBytes.ReadFile("VERSION") + if err != nil { + return "" + } + currentVersion = strings.TrimSpace(string(versionBytes)) + } + return currentVersion +}