From 0b6a6b27d576a25a87ddde1a7f4dd085f2a25406 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Fri, 25 Jul 2025 11:46:02 -0300 Subject: [PATCH 1/4] ci: publish to winget, nur; fixes brew token (#297) --- .goreleaser.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.goreleaser.yml b/.goreleaser.yml index 827574aa..17dab8f9 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -110,6 +110,7 @@ homebrew_casks: - repository: owner: charmbracelet name: homebrew-tap + token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" npms: - name: "@charmland/crush" @@ -134,6 +135,32 @@ nfpms: - src: ./manpages/crush.1.gz dst: /usr/share/man/man1/crush.1.gz +nix: + - repository: + owner: "charmbracelet" + name: nur + token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" + extra_install: |- + installManPage ./manpages/crush.1.gz. + installShellCompletion ./completions/* + +winget: + - publisher: charmbracelet + copyright: Charmbracelet, Inc + repository: + owner: "charmbracelet" + name: winget-pkgs + token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" + branch: "crush-{{.Version}}" + pull_request: + enabled: true + draft: false + check_boxes: true + base: + owner: microsoft + name: winget-pkgs + branch: master + changelog: sort: asc disable: "{{ .IsNightly }}" From 76c95de579f1412bfdf74261f95524e14179b350 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Fri, 25 Jul 2025 13:40:50 -0400 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20don=E2=80=99t=20panic=20when=20fetch?= =?UTF-8?q?ing=20document=20URI=20path=20fails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: remove hardcoded panic when fetching document URI path * chore: add structured logging to LSP URI conversion errors --- internal/llm/tools/diagnostics.go | 18 ++++++++++-- internal/lsp/client.go | 13 +++++++-- internal/lsp/protocol/pattern_interfaces.go | 20 +++++++++++-- internal/lsp/protocol/uri.go | 25 +++++++++++----- internal/lsp/util/edit.go | 32 +++++++++++++++++---- internal/lsp/watcher/watcher.go | 14 +++++++-- 6 files changed, 101 insertions(+), 21 deletions(-) diff --git a/internal/llm/tools/diagnostics.go b/internal/llm/tools/diagnostics.go index 6d2da798..fc9bd211 100644 --- a/internal/llm/tools/diagnostics.go +++ b/internal/llm/tools/diagnostics.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "log/slog" "maps" "sort" "strings" @@ -118,7 +119,13 @@ func waitForLspDiagnostics(ctx context.Context, filePath string, lsps map[string return } - if diagParams.URI.Path() == filePath || hasDiagnosticsChanged(client.GetDiagnostics(), originalDiags) { + path, err := diagParams.URI.Path() + if err != nil { + slog.Error("Failed to convert diagnostic URI to path", "uri", diagParams.URI, "error", err) + return + } + + if path == filePath || hasDiagnosticsChanged(client.GetDiagnostics(), originalDiags) { select { case diagChan <- struct{}{}: default: @@ -216,10 +223,15 @@ func getDiagnostics(filePath string, lsps map[string]*lsp.Client) string { diagnostics := client.GetDiagnostics() if len(diagnostics) > 0 { for location, diags := range diagnostics { - isCurrentFile := location.Path() == filePath + path, err := location.Path() + if err != nil { + slog.Error("Failed to convert diagnostic location URI to path", "uri", location, "error", err) + continue + } + isCurrentFile := path == filePath for _, diag := range diags { - formattedDiag := formatDiagnostic(location.Path(), diag, lspName) + formattedDiag := formatDiagnostic(path, diag, lspName) if isCurrentFile { fileDiagnostics = append(fileDiagnostics, formattedDiag) diff --git a/internal/lsp/client.go b/internal/lsp/client.go index f7298a0d..219a5df5 100644 --- a/internal/lsp/client.go +++ b/internal/lsp/client.go @@ -449,7 +449,12 @@ func (c *Client) pingTypeScriptServer(ctx context.Context) error { // If we have any open files, try to get document symbols for one for uri := range c.openFiles { - filePath := protocol.DocumentURI(uri).Path() + filePath, err := protocol.DocumentURI(uri).Path() + if err != nil { + slog.Error("Failed to convert URI to path for TypeScript symbol collection", "uri", uri, "error", err) + continue + } + if strings.HasSuffix(filePath, ".ts") || strings.HasSuffix(filePath, ".js") || strings.HasSuffix(filePath, ".tsx") || strings.HasSuffix(filePath, ".jsx") { var symbols []protocol.DocumentSymbol @@ -712,7 +717,11 @@ func (c *Client) CloseAllFiles(ctx context.Context) { // First collect all URIs that need to be closed for uri := range c.openFiles { // Convert URI back to file path using proper URI handling - filePath := protocol.DocumentURI(uri).Path() + filePath, err := protocol.DocumentURI(uri).Path() + if err != nil { + slog.Error("Failed to convert URI to path for file closing", "uri", uri, "error", err) + continue + } filesToClose = append(filesToClose, filePath) } c.openFilesMu.Unlock() diff --git a/internal/lsp/protocol/pattern_interfaces.go b/internal/lsp/protocol/pattern_interfaces.go index 9ee48cb4..5cb5dbb8 100644 --- a/internal/lsp/protocol/pattern_interfaces.go +++ b/internal/lsp/protocol/pattern_interfaces.go @@ -2,6 +2,7 @@ package protocol import ( "fmt" + "log/slog" ) // PatternInfo is an interface for types that represent glob patterns @@ -36,21 +37,36 @@ func (g *GlobPattern) AsPattern() (PatternInfo, error) { return nil, fmt.Errorf("nil pattern") } + var err error + switch v := g.Value.(type) { case string: return StringPattern{Pattern: v}, nil + case RelativePattern: // Handle BaseURI which could be string or DocumentUri basePath := "" switch baseURI := v.BaseURI.Value.(type) { case string: - basePath = DocumentURI(baseURI).Path() + basePath, err = DocumentURI(baseURI).Path() + if err != nil { + slog.Error("Failed to convert URI to path", "uri", baseURI, "error", err) + return nil, fmt.Errorf("invalid URI: %s", baseURI) + } + case DocumentURI: - basePath = baseURI.Path() + basePath, err = baseURI.Path() + if err != nil { + slog.Error("Failed to convert DocumentURI to path", "uri", baseURI, "error", err) + return nil, fmt.Errorf("invalid DocumentURI: %s", baseURI) + } + default: return nil, fmt.Errorf("unknown BaseURI type: %T", v.BaseURI.Value) } + return RelativePatternInfo{RP: v, BasePath: basePath}, nil + default: return nil, fmt.Errorf("unknown pattern type: %T", g.Value) } diff --git a/internal/lsp/protocol/uri.go b/internal/lsp/protocol/uri.go index a63ed406..ccc45f23 100644 --- a/internal/lsp/protocol/uri.go +++ b/internal/lsp/protocol/uri.go @@ -70,7 +70,7 @@ func (uri *DocumentURI) UnmarshalText(data []byte) (err error) { // DocumentUri("").Path() returns the empty string. // // Path panics if called on a URI that is not a valid filename. -func (uri DocumentURI) Path() string { +func (uri DocumentURI) Path() (string, error) { filename, err := filename(uri) if err != nil { // e.g. ParseRequestURI failed. @@ -79,22 +79,33 @@ func (uri DocumentURI) Path() string { // direct string manipulation; all DocumentUris // received from the client pass through // ParseRequestURI, which ensures validity. - panic(err) + return "", fmt.Errorf("invalid URI %q: %w", uri, err) } - return filepath.FromSlash(filename) + return filepath.FromSlash(filename), nil } // Dir returns the URI for the directory containing the receiver. -func (uri DocumentURI) Dir() DocumentURI { +func (uri DocumentURI) Dir() (DocumentURI, error) { + // XXX: Legacy comment: // This function could be more efficiently implemented by avoiding any call // to Path(), but at least consolidates URI manipulation. - return URIFromPath(uri.DirPath()) + + path, err := uri.DirPath() + if err != nil { + return "", fmt.Errorf("invalid URI %q: %w", uri, err) + } + + return URIFromPath(path), nil } // DirPath returns the file path to the directory containing this URI, which // must be a file URI. -func (uri DocumentURI) DirPath() string { - return filepath.Dir(uri.Path()) +func (uri DocumentURI) DirPath() (string, error) { + path, err := uri.Path() + if err != nil { + return "", err + } + return filepath.Dir(path), nil } func filename(uri DocumentURI) (string, error) { diff --git a/internal/lsp/util/edit.go b/internal/lsp/util/edit.go index b32eac0c..12d8e428 100644 --- a/internal/lsp/util/edit.go +++ b/internal/lsp/util/edit.go @@ -11,7 +11,10 @@ import ( ) func applyTextEdits(uri protocol.DocumentURI, edits []protocol.TextEdit) error { - path := uri.Path() + path, err := uri.Path() + if err != nil { + return fmt.Errorf("invalid URI: %w", err) + } // Read the file content content, err := os.ReadFile(path) @@ -148,7 +151,11 @@ func applyTextEdit(lines []string, edit protocol.TextEdit) ([]string, error) { // applyDocumentChange applies a DocumentChange (create/rename/delete operations) func applyDocumentChange(change protocol.DocumentChange) error { if change.CreateFile != nil { - path := change.CreateFile.URI.Path() + path, err := change.CreateFile.URI.Path() + if err != nil { + return fmt.Errorf("invalid URI: %w", err) + } + if change.CreateFile.Options != nil { if change.CreateFile.Options.Overwrite { // Proceed with overwrite @@ -164,7 +171,11 @@ func applyDocumentChange(change protocol.DocumentChange) error { } if change.DeleteFile != nil { - path := change.DeleteFile.URI.Path() + path, err := change.DeleteFile.URI.Path() + if err != nil { + return fmt.Errorf("invalid URI: %w", err) + } + if change.DeleteFile.Options != nil && change.DeleteFile.Options.Recursive { if err := os.RemoveAll(path); err != nil { return fmt.Errorf("failed to delete directory recursively: %w", err) @@ -177,8 +188,19 @@ func applyDocumentChange(change protocol.DocumentChange) error { } if change.RenameFile != nil { - oldPath := change.RenameFile.OldURI.Path() - newPath := change.RenameFile.NewURI.Path() + var newPath, oldPath string + var err error + + oldPath, err = change.RenameFile.OldURI.Path() + if err != nil { + return err + } + + newPath, err = change.RenameFile.NewURI.Path() + if err != nil { + return err + } + if change.RenameFile.Options != nil { if !change.RenameFile.Options.Overwrite { if _, err := os.Stat(newPath); err == nil { diff --git a/internal/lsp/watcher/watcher.go b/internal/lsp/watcher/watcher.go index 08087093..58b6d41d 100644 --- a/internal/lsp/watcher/watcher.go +++ b/internal/lsp/watcher/watcher.go @@ -617,7 +617,11 @@ func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPatt return false } // For relative patterns - basePath = protocol.DocumentURI(basePath).Path() + if basePath, err = protocol.DocumentURI(basePath).Path(); err != nil { + // XXX: Do we want to return here, or send the error up the stack? + slog.Error("Error converting base path to URI", "basePath", basePath, "error", err) + } + basePath = filepath.ToSlash(basePath) // Make path relative to basePath for matching @@ -660,7 +664,13 @@ func (w *WorkspaceWatcher) debounceHandleFileEvent(ctx context.Context, uri stri // handleFileEvent sends file change notifications func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) { // If the file is open and it's a change event, use didChange notification - filePath := protocol.DocumentURI(uri).Path() + filePath, err := protocol.DocumentURI(uri).Path() + if err != nil { + // XXX: Do we want to return here, or send the error up the stack? + slog.Error("Error converting URI to path", "uri", uri, "error", err) + return + } + if changeType == protocol.FileChangeType(protocol.Deleted) { w.client.ClearDiagnosticsForURI(protocol.DocumentURI(uri)) } else if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) { From 98f51b09368485cd3e28ab3b96b5638438fb3889 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Fri, 25 Jul 2025 15:57:50 -0300 Subject: [PATCH 3/4] fix: allow to override catwalk url --- internal/config/load.go | 2 +- internal/config/provider.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/config/load.go b/internal/config/load.go index 44bcf8e3..77f53356 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -17,7 +17,7 @@ import ( "github.com/charmbracelet/crush/internal/log" ) -const catwalkURL = "https://catwalk.charm.sh" +const defaultCatwalkURL = "https://catwalk.charm.sh" // LoadReader config via io.Reader. func LoadReader(fd io.Reader) (*Config, error) { diff --git a/internal/config/provider.go b/internal/config/provider.go index ba02f9d8..e1efc503 100644 --- a/internal/config/provider.go +++ b/internal/config/provider.go @@ -1,6 +1,7 @@ package config import ( + "cmp" "encoding/json" "fmt" "log/slog" @@ -74,6 +75,7 @@ func loadProvidersFromCache(path string) ([]catwalk.Provider, error) { } func Providers() ([]catwalk.Provider, error) { + catwalkURL := cmp.Or(os.Getenv("CATWALK_URL"), defaultCatwalkURL) client := catwalk.NewWithURL(catwalkURL) path := providerCacheFileData() return loadProvidersOnce(client, path) From 2d809a724ca4fe2417d1c57017c51b2a8130b817 Mon Sep 17 00:00:00 2001 From: ras0q Date: Sat, 26 Jul 2025 06:37:17 +0900 Subject: [PATCH 4/4] fix(tui): typo --- internal/tui/page/chat/chat.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index a17cad40..c03a369a 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -835,7 +835,7 @@ func (p *chatPage) Help() help.KeyMap { ), key.NewBinding( key.WithKeys("g", "home"), - key.WithHelp("g", "hone"), + key.WithHelp("g", "home"), ), key.NewBinding( key.WithKeys("G", "end"),