Merge pull request from charmbracelet/the-great-cleanup
Configuration tests and new logging
commitcd0b998abee91318f311b4933a1a7c1b68f350c8
authorChristian Rocha <christian@rocha.is>
dateMon 7 Jul 2025, 16:18 (1 year ago)
parent5004105b89b8a0
4435 files changed+1263886−7166
1435 more files changed are not listed.
.github/workflows/build.yml+2
@@ -7,3 +7,5 @@ jobs:
77 with:
88 go-version: ""
99 go-version-file: ./go.mod
10+ secrets:
11+ gh_pat: "${{ secrets.PERSONAL_ACCESS_TOKEN }}"
.gitignore+1−1
@@ -43,7 +43,7 @@ Thumbs.db
4343
4444**/.crush/**
4545
46−crush
46+/crush
4747
4848manpages/
4949completions/
.golangci.yml+1−1
@@ -6,7 +6,7 @@ linters:
66 # - goconst
77 # - godot
88 # - godox
9− - gomoddirectives
9+ # - gomoddirectives
1010 - goprintffuncname
1111 # - gosec
1212 - misspell
cmd/logs.gonew file+99
@@ -0,0 +1,99 @@
1+package cmd
2+
3+import (
4+ "encoding/json"
5+ "fmt"
6+ "os"
7+ "path/filepath"
8+ "slices"
9+ "time"
10+
11+ "github.com/charmbracelet/crush/internal/config"
12+ "github.com/charmbracelet/log/v2"
13+ "github.com/nxadm/tail"
14+ "github.com/spf13/cobra"
15+)
16+
17+func init() {
18+ rootCmd.AddCommand(logsCmd)
19+}
20+
21+var logsCmd = &cobra.Command{
22+ Use: "logs",
23+ Short: "View crush logs",
24+ Long: `View the logs generated by Crush. This command allows you to see the log output for debugging and monitoring purposes.`,
25+ RunE: func(cmd *cobra.Command, args []string) error {
26+ cwd, err := cmd.Flags().GetString("cwd")
27+ if err != nil {
28+ return fmt.Errorf("failed to get current working directory: %v", err)
29+ }
30+ log.SetLevel(log.DebugLevel)
31+ cfg, err := config.Load(cwd, false)
32+ if err != nil {
33+ return fmt.Errorf("failed to load configuration: %v", err)
34+ }
35+ logsFile := filepath.Join(cfg.WorkingDir(), cfg.Options.DataDirectory, "logs", "crush.log")
36+ _, err = os.Stat(logsFile)
37+ if os.IsNotExist(err) {
38+ log.Warn("Looks like you are not in a crush project. No logs found.")
39+ return nil
40+ }
41+ t, err := tail.TailFile(logsFile, tail.Config{Follow: true, ReOpen: true, Logger: tail.DiscardingLogger})
42+ if err != nil {
43+ return fmt.Errorf("failed to tail log file: %v", err)
44+ }
45+
46+ // Print the text of each received line
47+ for line := range t.Lines {
48+ var data map[string]any
49+ if err := json.Unmarshal([]byte(line.Text), &data); err != nil {
50+ continue
51+ }
52+ msg := data["msg"]
53+ level := data["level"]
54+ otherData := []any{}
55+ keys := []string{}
56+ for k := range data {
57+ keys = append(keys, k)
58+ }
59+ slices.Sort(keys)
60+ for _, k := range keys {
61+ switch k {
62+ case "msg", "level", "time":
63+ continue
64+ case "source":
65+ source, ok := data[k].(map[string]any)
66+ if !ok {
67+ continue
68+ }
69+ sourceFile := fmt.Sprintf("%s:%d", source["file"], int(source["line"].(float64)))
70+ otherData = append(otherData, "source", sourceFile)
71+
72+ default:
73+ otherData = append(otherData, k, data[k])
74+ }
75+ }
76+ log.SetTimeFunction(func(_ time.Time) time.Time {
77+ // parse the timestamp from the log line if available
78+ t, err := time.Parse(time.RFC3339, data["time"].(string))
79+ if err != nil {
80+ return time.Now() // fallback to current time if parsing fails
81+ }
82+ return t
83+ })
84+ switch level {
85+ case "INFO":
86+ log.Info(msg, otherData...)
87+ case "DEBUG":
88+ log.Debug(msg, otherData...)
89+ case "ERROR":
90+ log.Error(msg, otherData...)
91+ case "WARN":
92+ log.Warn(msg, otherData...)
93+ default:
94+ log.Info(msg, otherData...)
95+ }
96+ }
97+ return nil
98+ },
99+}
cmd/root.go+31−30
@@ -4,6 +4,7 @@ import (
44 "context"
55 "fmt"
66 "io"
7+ "log/slog"
78 "os"
89 "sync"
910 "time"
@@ -14,7 +15,7 @@ import (
1415 "github.com/charmbracelet/crush/internal/db"
1516 "github.com/charmbracelet/crush/internal/format"
1617 "github.com/charmbracelet/crush/internal/llm/agent"
17− "github.com/charmbracelet/crush/internal/logging"
18+ "github.com/charmbracelet/crush/internal/log"
1819 "github.com/charmbracelet/crush/internal/pubsub"
1920 "github.com/charmbracelet/crush/internal/tui"
2021 "github.com/charmbracelet/crush/internal/version"
@@ -36,7 +37,7 @@ to assist developers in writing, debugging, and understanding code directly from
3637 # Run with debug logging
3738 crush -d
3839
39− # Run with debug logging in a specific directory
40+ # Run with debug slog.in a specific directory
4041 crush -d -c /path/to/project
4142
4243 # Print version
@@ -75,7 +76,7 @@ to assist developers in writing, debugging, and understanding code directly from
7576 cwd = c
7677 }
7778
78− _, err := config.Init(cwd, debug)
79+ cfg, err := config.Init(cwd, debug)
7980 if err != nil {
8081 return err
8182 }
@@ -85,25 +86,25 @@ to assist developers in writing, debugging, and understanding code directly from
8586 defer cancel()
8687
8788 // Connect DB, this will also run migrations
88− conn, err := db.Connect(ctx)
89+ conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
8990 if err != nil {
9091 return err
9192 }
9293
93− app, err := app.New(ctx, conn)
94+ app, err := app.New(ctx, conn, cfg)
9495 if err != nil {
95− logging.Error("Failed to create app: %v", err)
96+ slog.Error(fmt.Sprintf("Failed to create app instance: %v", err))
9697 return err
9798 }
9899 // Defer shutdown here so it runs for both interactive and non-interactive modes
99100 defer app.Shutdown()
100101
101102 // Initialize MCP tools early for both modes
102− initMCPTools(ctx, app)
103+ initMCPTools(ctx, app, cfg)
103104
104105 prompt, err = maybePrependStdin(prompt)
105106 if err != nil {
106− logging.Error("Failed to read stdin: %v", err)
107+ slog.Error(fmt.Sprintf("Failed to read from stdin: %v", err))
107108 return err
108109 }
109110
@@ -132,18 +133,18 @@ to assist developers in writing, debugging, and understanding code directly from
132133 // Set up message handling for the TUI
133134 go func() {
134135 defer tuiWg.Done()
135− defer logging.RecoverPanic("TUI-message-handler", func() {
136+ defer log.RecoverPanic("TUI-message-handler", func() {
136137 attemptTUIRecovery(program)
137138 })
138139
139140 for {
140141 select {
141142 case <-tuiCtx.Done():
142− logging.Info("TUI message handler shutting down")
143+ slog.Info("TUI message handler shutting down")
143144 return
144145 case msg, ok := <-ch:
145146 if !ok {
146− logging.Info("TUI message channel closed")
147+ slog.Info("TUI message channel closed")
147148 return
148149 }
149150 program.Send(msg)
@@ -165,7 +166,7 @@ to assist developers in writing, debugging, and understanding code directly from
165166 // Wait for TUI message handler to finish
166167 tuiWg.Wait()
167168
168− logging.Info("All goroutines cleaned up")
169+ slog.Info("All goroutines cleaned up")
169170 }
170171
171172 // Run the TUI
@@ -173,35 +174,35 @@ to assist developers in writing, debugging, and understanding code directly from
173174 cleanup()
174175
175176 if err != nil {
176− logging.Error("TUI error: %v", err)
177+ slog.Error(fmt.Sprintf("TUI run error: %v", err))
177178 return fmt.Errorf("TUI error: %v", err)
178179 }
179180
180− logging.Info("TUI exited with result: %v", result)
181+ slog.Info(fmt.Sprintf("TUI exited with result: %v", result))
181182 return nil
182183 },
183184}
184185
185186// attemptTUIRecovery tries to recover the TUI after a panic
186187func attemptTUIRecovery(program *tea.Program) {
187− logging.Info("Attempting to recover TUI after panic")
188+ slog.Info("Attempting to recover TUI after panic")
188189
189190 // We could try to restart the TUI or gracefully exit
190191 // For now, we'll just quit the program to avoid further issues
191192 program.Quit()
192193}
193194
194−func initMCPTools(ctx context.Context, app *app.App) {
195+func initMCPTools(ctx context.Context, app *app.App, cfg *config.Config) {
195196 go func() {
196− defer logging.RecoverPanic("MCP-goroutine", nil)
197+ defer log.RecoverPanic("MCP-goroutine", nil)
197198
198199 // Create a context with timeout for the initial MCP tools fetch
199200 ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
200201 defer cancel()
201202
202203 // Set this up once with proper error handling
203− agent.GetMcpTools(ctxWithTimeout, app.Permissions)
204− logging.Info("MCP message handling goroutine exiting")
204+ agent.GetMcpTools(ctxWithTimeout, app.Permissions, cfg)
205+ slog.Info("MCP message handling goroutine exiting")
205206 }()
206207}
207208
@@ -215,7 +216,7 @@ func setupSubscriber[T any](
215216 wg.Add(1)
216217 go func() {
217218 defer wg.Done()
218− defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
219+ defer log.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
219220
220221 subCh := subscriber(ctx)
221222
@@ -223,7 +224,7 @@ func setupSubscriber[T any](
223224 select {
224225 case event, ok := <-subCh:
225226 if !ok {
226− logging.Info("subscription channel closed", "name", name)
227+ slog.Info("subscription channel closed", "name", name)
227228 return
228229 }
229230
@@ -232,13 +233,13 @@ func setupSubscriber[T any](
232233 select {
233234 case outputCh <- msg:
234235 case <-time.After(2 * time.Second):
235− logging.Warn("message dropped due to slow consumer", "name", name)
236+ slog.Warn("message dropped due to slow consumer", "name", name)
236237 case <-ctx.Done():
237− logging.Info("subscription cancelled", "name", name)
238+ slog.Info("subscription cancelled", "name", name)
238239 return
239240 }
240241 case <-ctx.Done():
241− logging.Info("subscription cancelled", "name", name)
242+ slog.Info("subscription cancelled", "name", name)
242243 return
243244 }
244245 }
@@ -251,7 +252,6 @@ func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg,
251252 wg := sync.WaitGroup{}
252253 ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
253254
254− setupSubscriber(ctx, &wg, "logging", logging.Subscribe, ch)
255255 setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch)
256256 setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
257257 setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
@@ -259,22 +259,22 @@ func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg,
259259 setupSubscriber(ctx, &wg, "history", app.History.Subscribe, ch)
260260
261261 cleanupFunc := func() {
262− logging.Info("Cancelling all subscriptions")
262+ slog.Info("Cancelling all subscriptions")
263263 cancel() // Signal all goroutines to stop
264264
265265 waitCh := make(chan struct{})
266266 go func() {
267− defer logging.RecoverPanic("subscription-cleanup", nil)
267+ defer log.RecoverPanic("subscription-cleanup", nil)
268268 wg.Wait()
269269 close(waitCh)
270270 }()
271271
272272 select {
273273 case <-waitCh:
274− logging.Info("All subscription goroutines completed successfully")
274+ slog.Info("All subscription goroutines completed successfully")
275275 close(ch) // Only close after all writers are confirmed done
276276 case <-time.After(5 * time.Second):
277− logging.Warn("Timed out waiting for some subscription goroutines to complete")
277+ slog.Warn("Timed out waiting for some subscription goroutines to complete")
278278 close(ch)
279279 }
280280 }
@@ -292,9 +292,10 @@ func Execute() {
292292}
293293
294294func init() {
295+ rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
296+
295297 rootCmd.Flags().BoolP("help", "h", false, "Help")
296298 rootCmd.Flags().BoolP("debug", "d", false, "Debug")
297− rootCmd.Flags().StringP("cwd", "c", "", "Current working directory")
298299 rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode")
299300
300301 // Add format flag with validation logic
cmd/schema/README.mddeleted−182
@@ -1,182 +0,0 @@
1−# Crush Configuration Schema Generator
2−
3−This tool automatically generates a JSON Schema for the Crush configuration file by using Go reflection to analyze the configuration structs. The schema provides validation, autocompletion, and documentation for configuration files.
4−
5−## Features
6−
7−- **Automated Generation**: Uses reflection to automatically generate schemas from Go structs
8−- **Always Up-to-Date**: Schema stays in sync with code changes automatically
9−- **Comprehensive**: Includes all configuration options, types, and validation rules
10−- **Enhanced**: Adds provider enums, model lists, and custom descriptions
11−- **Extensible**: Easy to add new fields and modify existing ones
12−
13−## Usage
14−
15−```bash
16−# Generate the schema
17−go run cmd/schema/main.go > crush-schema.json
18−
19−# Or use the task runner
20−task schema
21−```
22−
23−## How It Works
24−
25−The generator:
26−
27−1. **Reflects on Config Structs**: Analyzes the `config.Config` struct and all related types
28−2. **Generates Base Schema**: Creates JSON Schema definitions for all struct fields
29−3. **Enhances with Runtime Data**: Adds provider lists, model enums, and tool lists from the actual codebase
30−4. **Adds Custom Descriptions**: Provides meaningful descriptions for configuration options
31−5. **Sets Default Values**: Includes appropriate defaults for optional fields
32−
33−## Schema Features
34−
35−The generated schema includes:
36−
37−- **Type Safety**: Proper type definitions for all configuration fields
38−- **Validation**: Required fields, enum constraints, and format validation
39−- **Documentation**: Descriptions for all configuration options
40−- **Defaults**: Default values for optional settings
41−- **Provider Enums**: Current list of supported providers
42−- **Model Enums**: Available models from all configured providers
43−- **Tool Lists**: Valid tool names for agent configurations
44−- **Cross-References**: Proper relationships between different config sections
45−
46−## Adding New Configuration Fields
47−
48−To add new configuration options:
49−
50−1. **Add to Config Structs**: Add the field to the appropriate struct in `internal/config/`
51−2. **Add JSON Tags**: Include proper JSON tags with field names
52−3. **Regenerate Schema**: Run the schema generator to update the JSON schema
53−4. **Update Validation**: Add any custom validation logic if needed
54−
55−Example:
56−```go
57−type Options struct {
58− // ... existing fields ...
59−
60− // New field with JSON tag and description
61− NewFeature bool `json:"new_feature,omitempty"`
62−}
63−```
64−
65−The schema generator will automatically:
66−- Detect the new field
67−- Generate appropriate JSON schema
68−- Add type information
69−- Include in validation
70−
71−## Using the Schema
72−
73−### Editor Integration
74−
75−Most modern editors support JSON Schema:
76−
77−**VS Code**: Add to your workspace settings:
78−```json
79−{
80− "json.schemas": [
81− {
82− "fileMatch": ["crush.json", ".crush.json"],
83− "url": "./crush-schema.json"
84− }
85− ]
86−}
87−```
88−
89−**JetBrains IDEs**: Configure in Settings → Languages & Frameworks → Schemas and DTDs → JSON Schema Mappings
90−
91−### Validation Tools
92−
93−```bash
94−# Using jsonschema (Python)
95−pip install jsonschema
96−jsonschema -i crush.json crush-schema.json
97−
98−# Using ajv-cli (Node.js)
99−npm install -g ajv-cli
100−ajv validate -s crush-schema.json -d crush.json
101−```
102−
103−### Configuration Example
104−
105−```json
106−{
107− "models": {
108− "large": {
109− "model_id": "claude-3-5-sonnet-20241022",
110− "provider": "anthropic",
111− "reasoning_effort": "medium",
112− "max_tokens": 8192
113− },
114− "small": {
115− "model_id": "claude-3-5-haiku-20241022",
116− "provider": "anthropic"
117− }
118− },
119− "providers": {
120− "anthropic": {
121− "id": "anthropic",
122− "provider_type": "anthropic",
123− "api_key": "your-api-key",
124− "disabled": false
125− }
126− },
127− "agents": {
128− "coder": {
129− "id": "coder",
130− "name": "Coder",
131− "model": "large",
132− "disabled": false
133− },
134− "custom-agent": {
135− "id": "custom-agent",
136− "name": "Custom Agent",
137− "description": "A custom agent for specific tasks",
138− "model": "small",
139− "allowed_tools": ["glob", "grep", "view"],
140− "allowed_mcp": {
141− "filesystem": ["read", "write"]
142− }
143− }
144− },
145− "mcp": {
146− "filesystem": {
147− "command": "mcp-filesystem",
148− "args": ["--root", "/workspace"],
149− "type": "stdio"
150− }
151− },
152− "lsp": {
153− "typescript": {
154− "command": "typescript-language-server",
155− "args": ["--stdio"],
156− "enabled": true
157− }
158− },
159− "options": {
160− "context_paths": [
161− "README.md",
162− "docs/",
163− ".cursorrules"
164− ],
165− "data_directory": ".crush",
166− "debug": false,
167− "tui": {
168− "compact_mode": false
169− }
170− }
171−}
172−```
173−
174−## Maintenance
175−
176−The schema generator is designed to be maintenance-free. As long as:
177−
178−- Configuration structs have proper JSON tags
179−- New enums are added to the enhancement functions
180−- The generator is run after significant config changes
181−
182−The schema will stay current with the codebase automatically.
cmd/schema/main.godeleted−155
@@ -1,155 +0,0 @@
1−package main
2−
3−import (
4− "encoding/json"
5− "fmt"
6− "os"
7−
8− "github.com/charmbracelet/crush/internal/config"
9− "github.com/invopop/jsonschema"
10−)
11−
12−func main() {
13− // Create a new reflector
14− r := &jsonschema.Reflector{
15− // Use anonymous schemas to avoid ID conflicts
16− Anonymous: true,
17− // Expand the root struct instead of referencing it
18− ExpandedStruct: true,
19− AllowAdditionalProperties: true,
20− }
21−
22− // Generate schema for the main Config struct
23− schema := r.Reflect(&config.Config{})
24−
25− // Enhance the schema with additional information
26− enhanceSchema(schema)
27−
28− // Set the schema metadata
29− schema.Version = "https://json-schema.org/draft/2020-12/schema"
30− schema.Title = "Crush Configuration"
31− schema.Description = "Configuration schema for the Crush application"
32−
33− // Pretty print the schema
34− encoder := json.NewEncoder(os.Stdout)
35− encoder.SetIndent("", " ")
36− if err := encoder.Encode(schema); err != nil {
37− fmt.Fprintf(os.Stderr, "Error encoding schema: %v\n", err)
38− os.Exit(1)
39− }
40−}
41−
42−// enhanceSchema adds additional enhancements to the generated schema
43−func enhanceSchema(schema *jsonschema.Schema) {
44− // Add provider enums
45− addProviderEnums(schema)
46−
47− // Add model enums
48− addModelEnums(schema)
49−
50− // Add tool enums
51− addToolEnums(schema)
52−
53− // Add default context paths
54− addDefaultContextPaths(schema)
55−}
56−
57−// addProviderEnums adds provider enums to the schema
58−func addProviderEnums(schema *jsonschema.Schema) {
59− providers := config.Providers()
60− var providerIDs []any
61− for _, p := range providers {
62− providerIDs = append(providerIDs, string(p.ID))
63− }
64−
65− // Add to PreferredModel provider field
66− if schema.Definitions != nil {
67− if preferredModelDef, exists := schema.Definitions["PreferredModel"]; exists {
68− if providerProp, exists := preferredModelDef.Properties.Get("provider"); exists {
69− providerProp.Enum = providerIDs
70− }
71− }
72−
73− // Add to ProviderConfig ID field
74− if providerConfigDef, exists := schema.Definitions["ProviderConfig"]; exists {
75− if idProp, exists := providerConfigDef.Properties.Get("id"); exists {
76− idProp.Enum = providerIDs
77− }
78− }
79− }
80−}
81−
82−// addModelEnums adds model enums to the schema
83−func addModelEnums(schema *jsonschema.Schema) {
84− providers := config.Providers()
85− var modelIDs []any
86− for _, p := range providers {
87− for _, m := range p.Models {
88− modelIDs = append(modelIDs, m.ID)
89− }
90− }
91−
92− // Add to PreferredModel model_id field
93− if schema.Definitions != nil {
94− if preferredModelDef, exists := schema.Definitions["PreferredModel"]; exists {
95− if modelIDProp, exists := preferredModelDef.Properties.Get("model_id"); exists {
96− modelIDProp.Enum = modelIDs
97− }
98− }
99− }
100−}
101−
102−// addToolEnums adds tool enums to the schema
103−func addToolEnums(schema *jsonschema.Schema) {
104− tools := []any{
105− "bash", "edit", "fetch", "glob", "grep", "ls", "sourcegraph", "view", "write", "agent",
106− }
107−
108− if schema.Definitions != nil {
109− if agentDef, exists := schema.Definitions["Agent"]; exists {
110− if allowedToolsProp, exists := agentDef.Properties.Get("allowed_tools"); exists {
111− if allowedToolsProp.Items != nil {
112− allowedToolsProp.Items.Enum = tools
113− }
114− }
115− }
116− }
117−}
118−
119−// addDefaultContextPaths adds default context paths to the schema
120−func addDefaultContextPaths(schema *jsonschema.Schema) {
121− defaultContextPaths := []any{
122− ".github/copilot-instructions.md",
123− ".cursorrules",
124− ".cursor/rules/",
125− "CLAUDE.md",
126− "CLAUDE.local.md",
127− "GEMINI.md",
128− "gemini.md",
129− "crush.md",
130− "crush.local.md",
131− "Crush.md",
132− "Crush.local.md",
133− "CRUSH.md",
134− "CRUSH.local.md",
135− }
136−
137− if schema.Definitions != nil {
138− if optionsDef, exists := schema.Definitions["Options"]; exists {
139− if contextPathsProp, exists := optionsDef.Properties.Get("context_paths"); exists {
140− contextPathsProp.Default = defaultContextPaths
141− }
142− }
143− }
144−
145− // Also add to root properties if they exist
146− if schema.Properties != nil {
147− if optionsProp, exists := schema.Properties.Get("options"); exists {
148− if optionsProp.Properties != nil {
149− if contextPathsProp, exists := optionsProp.Properties.Get("context_paths"); exists {
150− contextPathsProp.Default = defaultContextPaths
151− }
152− }
153− }
154− }
155−}
crush-schema.jsondeleted−700
@@ -1,700 +0,0 @@
1−{
2− "$schema": "https://json-schema.org/draft/2020-12/schema",
3− "$defs": {
4− "Agent": {
5− "properties": {
6− "id": {
7− "type": "string",
8− "enum": [
9− "coder",
10− "task",
11− "coder",
12− "task"
13− ],
14− "title": "Agent ID",
15− "description": "Unique identifier for the agent"
16− },
17− "name": {
18− "type": "string",
19− "title": "Name",
20− "description": "Display name of the agent"
21− },
22− "description": {
23− "type": "string",
24− "title": "Description",
25− "description": "Description of what the agent does"
26− },
27− "disabled": {
28− "type": "boolean",
29− "title": "Disabled",
30− "description": "Whether this agent is disabled",
31− "default": false
32− },
33− "model": {
34− "type": "string",
35− "enum": [
36− "large",
37− "small",
38− "large",
39− "small"
40− ],
41− "title": "Model Type",
42− "description": "Type of model to use (large or small)"
43− },
44− "allowed_tools": {
45− "items": {
46− "type": "string",
47− "enum": [
48− "bash",
49− "edit",
50− "fetch",
51− "glob",
52− "grep",
53− "ls",
54− "sourcegraph",
55− "view",
56− "write",
57− "agent"
58− ]
59− },
60− "type": "array",
61− "title": "Allowed Tools",
62− "description": "List of tools this agent is allowed to use (if nil all tools are allowed)"
63− },
64− "allowed_mcp": {
65− "additionalProperties": {
66− "items": {
67− "type": "string"
68− },
69− "type": "array"
70− },
71− "type": "object",
72− "title": "Allowed MCP",
73− "description": "Map of MCP servers this agent can use and their allowed tools"
74− },
75− "allowed_lsp": {
76− "items": {
77− "type": "string"
78− },
79− "type": "array",
80− "title": "Allowed LSP",
81− "description": "List of LSP servers this agent can use (if nil all LSPs are allowed)"
82− },
83− "context_paths": {
84− "items": {
85− "type": "string"
86− },
87− "type": "array",
88− "title": "Context Paths",
89− "description": "Custom context paths for this agent (additive to global context paths)"
90− }
91− },
92− "type": "object",
93− "required": [
94− "model"
95− ]
96− },
97− "LSPConfig": {
98− "properties": {
99− "enabled": {
100− "type": "boolean",
101− "title": "Enabled",
102− "description": "Whether this LSP server is enabled",
103− "default": true
104− },
105− "command": {
106− "type": "string",
107− "title": "Command",
108− "description": "Command to execute for the LSP server"
109− },
110− "args": {
111− "items": {
112− "type": "string"
113− },
114− "type": "array",
115− "title": "Arguments",
116− "description": "Command line arguments for the LSP server"
117− },
118− "options": {
119− "title": "Options",
120− "description": "LSP server specific options"
121− }
122− },
123− "type": "object",
124− "required": [
125− "command"
126− ]
127− },
128− "MCP": {
129− "properties": {
130− "command": {
131− "type": "string",
132− "title": "Command",
133− "description": "Command to execute for stdio MCP servers"
134− },
135− "env": {
136− "items": {
137− "type": "string"
138− },
139− "type": "array",
140− "title": "Environment",
141− "description": "Environment variables for the MCP server"
142− },
143− "args": {
144− "items": {
145− "type": "string"
146− },
147− "type": "array",
148− "title": "Arguments",
149− "description": "Command line arguments for the MCP server"
150− },
151− "type": {
152− "type": "string",
153− "enum": [
154− "stdio",
155− "sse",
156− "stdio",
157− "sse",
158− "http"
159− ],
160− "title": "Type",
161− "description": "Type of MCP connection",
162− "default": "stdio"
163− },
164− "url": {
165− "type": "string",
166− "title": "URL",
167− "description": "URL for SSE MCP servers"
168− },
169− "headers": {
170− "additionalProperties": {
171− "type": "string"
172− },
173− "type": "object",
174− "title": "Headers",
175− "description": "HTTP headers for SSE MCP servers"
176− }
177− },
178− "type": "object",
179− "required": [
180− "type"
181− ]
182− },
183− "Model": {
184− "properties": {
185− "id": {
186− "type": "string",
187− "title": "Model ID",
188− "description": "Unique identifier for the model"
189− },
190− "name": {
191− "type": "string",
192− "title": "Model Name",
193− "description": "Display name of the model"
194− },
195− "cost_per_1m_in": {
196− "type": "number",
197− "minimum": 0,
198− "title": "Input Cost",
199− "description": "Cost per 1 million input tokens"
200− },
201− "cost_per_1m_out": {
202− "type": "number",
203− "minimum": 0,
204− "title": "Output Cost",
205− "description": "Cost per 1 million output tokens"
206− },
207− "cost_per_1m_in_cached": {
208− "type": "number",
209− "minimum": 0,
210− "title": "Cached Input Cost",
211− "description": "Cost per 1 million cached input tokens"
212− },
213− "cost_per_1m_out_cached": {
214− "type": "number",
215− "minimum": 0,
216− "title": "Cached Output Cost",
217− "description": "Cost per 1 million cached output tokens"
218− },
219− "context_window": {
220− "type": "integer",
221− "minimum": 1,
222− "title": "Context Window",
223− "description": "Maximum context window size in tokens"
224− },
225− "default_max_tokens": {
226− "type": "integer",
227− "minimum": 1,
228− "title": "Default Max Tokens",
229− "description": "Default maximum tokens for responses"
230− },
231− "can_reason": {
232− "type": "boolean",
233− "title": "Can Reason",
234− "description": "Whether the model supports reasoning capabilities"
235− },
236− "reasoning_effort": {
237− "type": "string",
238− "title": "Reasoning Effort",
239− "description": "Default reasoning effort level for reasoning models"
240− },
241− "has_reasoning_effort": {
242− "type": "boolean",
243− "title": "Has Reasoning Effort",
244− "description": "Whether the model supports reasoning effort configuration"
245− },
246− "supports_attachments": {
247− "type": "boolean",
248− "title": "Supports Images",
249− "description": "Whether the model supports image attachments"
250− }
251− },
252− "type": "object",
253− "required": [
254− "id",
255− "name",
256− "context_window",
257− "default_max_tokens"
258− ]
259− },
260− "Options": {
261− "properties": {
262− "context_paths": {
263− "items": {
264− "type": "string"
265− },
266− "type": "array",
267− "title": "Context Paths",
268− "description": "List of paths to search for context files",
269− "default": [
270− ".github/copilot-instructions.md",
271− ".cursorrules",
272− ".cursor/rules/",
273− "CLAUDE.md",
274− "CLAUDE.local.md",
275− "GEMINI.md",
276− "gemini.md",
277− "crush.md",
278− "crush.local.md",
279− "Crush.md",
280− "Crush.local.md",
281− "CRUSH.md",
282− "CRUSH.local.md"
283− ]
284− },
285− "tui": {
286− "$ref": "#/$defs/TUIOptions",
287− "title": "TUI Options",
288− "description": "Terminal UI configuration options"
289− },
290− "debug": {
291− "type": "boolean",
292− "title": "Debug",
293− "description": "Enable debug logging",
294− "default": false
295− },
296− "debug_lsp": {
297− "type": "boolean",
298− "title": "Debug LSP",
299− "description": "Enable LSP debug logging",
300− "default": false
301− },
302− "disable_auto_summarize": {
303− "type": "boolean",
304− "title": "Disable Auto Summarize",
305− "description": "Disable automatic conversation summarization",
306− "default": false
307− },
308− "data_directory": {
309− "type": "string",
310− "title": "Data Directory",
311− "description": "Directory for storing application data",
312− "default": ".crush"
313− }
314− },
315− "type": "object"
316− },
317− "PreferredModel": {
318− "properties": {
319− "model_id": {
320− "type": "string",
321− "enum": [
322− "claude-opus-4-20250514",
323− "claude-sonnet-4-20250514",
324− "claude-3-7-sonnet-20250219",
325− "claude-3-5-haiku-20241022",
326− "claude-3-5-sonnet-20240620",
327− "claude-3-5-sonnet-20241022",
328− "codex-mini-latest",
329− "o4-mini",
330− "o3",
331− "o3-pro",
332− "gpt-4.1",
333− "gpt-4.1-mini",
334− "gpt-4.1-nano",
335− "gpt-4.5-preview",
336− "o3-mini",
337− "gpt-4o",
338− "gpt-4o-mini",
339− "gemini-2.5-pro",
340− "gemini-2.5-flash",
341− "codex-mini-latest",
342− "o4-mini",
343− "o3",
344− "o3-pro",
345− "gpt-4.1",
346− "gpt-4.1-mini",
347− "gpt-4.1-nano",
348− "gpt-4.5-preview",
349− "o3-mini",
350− "gpt-4o",
351− "gpt-4o-mini",
352− "anthropic.claude-opus-4-20250514-v1:0",
353− "anthropic.claude-sonnet-4-20250514-v1:0",
354− "anthropic.claude-3-7-sonnet-20250219-v1:0",
355− "anthropic.claude-3-5-haiku-20241022-v1:0",
356− "gemini-2.5-pro",
357− "gemini-2.5-flash",
358− "grok-3-mini",
359− "grok-3",
360− "mistralai/mistral-small-3.2-24b-instruct:free",
361− "mistralai/mistral-small-3.2-24b-instruct",
362− "minimax/minimax-m1:extended",
363− "minimax/minimax-m1",
364− "google/gemini-2.5-flash-lite-preview-06-17",
365− "google/gemini-2.5-flash",
366− "google/gemini-2.5-pro",
367− "openai/o3-pro",
368− "x-ai/grok-3-mini",
369− "x-ai/grok-3",
370− "mistralai/magistral-small-2506",
371− "mistralai/magistral-medium-2506",
372− "mistralai/magistral-medium-2506:thinking",
373− "google/gemini-2.5-pro-preview",
374− "deepseek/deepseek-r1-0528",
375− "anthropic/claude-opus-4",
376− "anthropic/claude-sonnet-4",
377− "mistralai/devstral-small:free",
378− "mistralai/devstral-small",
379− "google/gemini-2.5-flash-preview-05-20",
380− "google/gemini-2.5-flash-preview-05-20:thinking",
381− "openai/codex-mini",
382− "mistralai/mistral-medium-3",
383− "google/gemini-2.5-pro-preview-05-06",
384− "arcee-ai/caller-large",
385− "arcee-ai/virtuoso-large",
386− "arcee-ai/virtuoso-medium-v2",
387− "qwen/qwen3-30b-a3b",
388− "qwen/qwen3-14b",
389− "qwen/qwen3-32b",
390− "qwen/qwen3-235b-a22b",
391− "google/gemini-2.5-flash-preview",
392− "google/gemini-2.5-flash-preview:thinking",
393− "openai/o4-mini-high",
394− "openai/o3",
395− "openai/o4-mini",
396− "openai/gpt-4.1",
397− "openai/gpt-4.1-mini",
398− "openai/gpt-4.1-nano",
399− "x-ai/grok-3-mini-beta",
400− "x-ai/grok-3-beta",
401− "meta-llama/llama-4-maverick",
402− "meta-llama/llama-4-scout",
403− "all-hands/openhands-lm-32b-v0.1",
404− "google/gemini-2.5-pro-exp-03-25",
405− "deepseek/deepseek-chat-v3-0324:free",
406− "deepseek/deepseek-chat-v3-0324",
407− "mistralai/mistral-small-3.1-24b-instruct:free",
408− "mistralai/mistral-small-3.1-24b-instruct",
409− "ai21/jamba-1.6-large",
410− "ai21/jamba-1.6-mini",
411− "openai/gpt-4.5-preview",
412− "google/gemini-2.0-flash-lite-001",
413− "anthropic/claude-3.7-sonnet",
414− "anthropic/claude-3.7-sonnet:beta",
415− "anthropic/claude-3.7-sonnet:thinking",
416− "mistralai/mistral-saba",
417− "openai/o3-mini-high",
418− "google/gemini-2.0-flash-001",
419− "qwen/qwen-turbo",
420− "qwen/qwen-plus",
421− "qwen/qwen-max",
422− "openai/o3-mini",
423− "mistralai/mistral-small-24b-instruct-2501",
424− "deepseek/deepseek-r1-distill-llama-70b",
425− "deepseek/deepseek-r1",
426− "mistralai/codestral-2501",
427− "deepseek/deepseek-chat",
428− "openai/o1",
429− "x-ai/grok-2-1212",
430− "meta-llama/llama-3.3-70b-instruct",
431− "amazon/nova-lite-v1",
432− "amazon/nova-micro-v1",
433− "amazon/nova-pro-v1",
434− "openai/gpt-4o-2024-11-20",
435− "mistralai/mistral-large-2411",
436− "mistralai/mistral-large-2407",
437− "mistralai/pixtral-large-2411",
438− "thedrummer/unslopnemo-12b",
439− "anthropic/claude-3.5-haiku:beta",
440− "anthropic/claude-3.5-haiku",
441− "anthropic/claude-3.5-haiku-20241022:beta",
442− "anthropic/claude-3.5-haiku-20241022",
443− "anthropic/claude-3.5-sonnet:beta",
444− "anthropic/claude-3.5-sonnet",
445− "x-ai/grok-beta",
446− "mistralai/ministral-8b",
447− "mistralai/ministral-3b",
448− "nvidia/llama-3.1-nemotron-70b-instruct",
449− "google/gemini-flash-1.5-8b",
450− "meta-llama/llama-3.2-11b-vision-instruct",
451− "meta-llama/llama-3.2-3b-instruct",
452− "qwen/qwen-2.5-72b-instruct",
453− "mistralai/pixtral-12b",
454− "cohere/command-r-plus-08-2024",
455− "cohere/command-r-08-2024",
456− "microsoft/phi-3.5-mini-128k-instruct",
457− "nousresearch/hermes-3-llama-3.1-70b",
458− "openai/gpt-4o-2024-08-06",
459− "meta-llama/llama-3.1-405b-instruct",
460− "meta-llama/llama-3.1-70b-instruct",
461− "meta-llama/llama-3.1-8b-instruct",
462− "mistralai/mistral-nemo",
463− "openai/gpt-4o-mini",
464− "openai/gpt-4o-mini-2024-07-18",
465− "anthropic/claude-3.5-sonnet-20240620:beta",
466− "anthropic/claude-3.5-sonnet-20240620",
467− "mistralai/mistral-7b-instruct-v0.3",
468− "mistralai/mistral-7b-instruct:free",
469− "mistralai/mistral-7b-instruct",
470− "microsoft/phi-3-mini-128k-instruct",
471− "microsoft/phi-3-medium-128k-instruct",
472− "google/gemini-flash-1.5",
473− "openai/gpt-4o-2024-05-13",
474− "openai/gpt-4o",
475− "openai/gpt-4o:extended",
476− "meta-llama/llama-3-8b-instruct",
477− "meta-llama/llama-3-70b-instruct",
478− "mistralai/mixtral-8x22b-instruct",
479− "openai/gpt-4-turbo",
480− "google/gemini-pro-1.5",
481− "cohere/command-r-plus",
482− "cohere/command-r-plus-04-2024",
483− "cohere/command-r",
484− "anthropic/claude-3-haiku:beta",
485− "anthropic/claude-3-haiku",
486− "anthropic/claude-3-opus:beta",
487− "anthropic/claude-3-opus",
488− "anthropic/claude-3-sonnet:beta",
489− "anthropic/claude-3-sonnet",
490− "cohere/command-r-03-2024",
491− "mistralai/mistral-large",
492− "openai/gpt-3.5-turbo-0613",
493− "openai/gpt-4-turbo-preview",
494− "mistralai/mistral-small",
495− "mistralai/mistral-tiny",
496− "mistralai/mixtral-8x7b-instruct",
497− "openai/gpt-4-1106-preview",
498− "mistralai/mistral-7b-instruct-v0.1",
499− "openai/gpt-3.5-turbo-16k",
500− "openai/gpt-4",
501− "openai/gpt-4-0314"
502− ],
503− "title": "Model ID",
504− "description": "ID of the preferred model"
505− },
506− "provider": {
507− "type": "string",
508− "enum": [
509− "anthropic",
510− "openai",
511− "gemini",
512− "azure",
513− "bedrock",
514− "vertex",
515− "xai",
516− "openrouter"
517− ],
518− "title": "Provider",
519− "description": "Provider for the preferred model"
520− },
521− "reasoning_effort": {
522− "type": "string",
523− "title": "Reasoning Effort",
524− "description": "Override reasoning effort for this model"
525− },
526− "max_tokens": {
527− "type": "integer",
528− "minimum": 1,
529− "title": "Max Tokens",
530− "description": "Override max tokens for this model"
531− },
532− "think": {
533− "type": "boolean",
534− "title": "Think",
535− "description": "Enable thinking for reasoning models",
536− "default": false
537− }
538− },
539− "type": "object",
540− "required": [
541− "model_id",
542− "provider"
543− ]
544− },
545− "PreferredModels": {
546− "properties": {
547− "large": {
548− "$ref": "#/$defs/PreferredModel",
549− "title": "Large Model",
550− "description": "Preferred model configuration for large model type"
551− },
552− "small": {
553− "$ref": "#/$defs/PreferredModel",
554− "title": "Small Model",
555− "description": "Preferred model configuration for small model type"
556− }
557− },
558− "type": "object"
559− },
560− "ProviderConfig": {
561− "properties": {
562− "id": {
563− "type": "string",
564− "enum": [
565− "anthropic",
566− "openai",
567− "gemini",
568− "azure",
569− "bedrock",
570− "vertex",
571− "xai",
572− "openrouter"
573− ],
574− "title": "Provider ID",
575− "description": "Unique identifier for the provider"
576− },
577− "base_url": {
578− "type": "string",
579− "title": "Base URL",
580− "description": "Base URL for the provider API (required for custom providers)"
581− },
582− "provider_type": {
583− "type": "string",
584− "title": "Provider Type",
585− "description": "Type of the provider (openai"
586− },
587− "api_key": {
588− "type": "string",
589− "title": "API Key",
590− "description": "API key for authenticating with the provider"
591− },
592− "disabled": {
593− "type": "boolean",
594− "title": "Disabled",
595− "description": "Whether this provider is disabled",
596− "default": false
597− },
598− "extra_headers": {
599− "additionalProperties": {
600− "type": "string"
601− },
602− "type": "object",
603− "title": "Extra Headers",
604− "description": "Additional HTTP headers to send with requests"
605− },
606− "extra_params": {
607− "additionalProperties": {
608− "type": "string"
609− },
610− "type": "object",
611− "title": "Extra Parameters",
612− "description": "Additional provider-specific parameters"
613− },
614− "default_large_model": {
615− "type": "string",
616− "title": "Default Large Model",
617− "description": "Default model ID for large model type"
618− },
619− "default_small_model": {
620− "type": "string",
621− "title": "Default Small Model",
622− "description": "Default model ID for small model type"
623− },
624− "models": {
625− "items": {
626− "$ref": "#/$defs/Model"
627− },
628− "type": "array",
629− "title": "Models",
630− "description": "List of available models for this provider"
631− }
632− },
633− "type": "object",
634− "required": [
635− "provider_type"
636− ]
637− },
638− "TUIOptions": {
639− "properties": {
640− "compact_mode": {
641− "type": "boolean",
642− "title": "Compact Mode",
643− "description": "Enable compact mode for the TUI",
644− "default": false
645− }
646− },
647− "type": "object",
648− "required": [
649− "compact_mode"
650− ]
651− }
652− },
653− "properties": {
654− "models": {
655− "$ref": "#/$defs/PreferredModels",
656− "title": "Models",
657− "description": "Preferred model configurations for large and small model types"
658− },
659− "providers": {
660− "additionalProperties": {
661− "$ref": "#/$defs/ProviderConfig"
662− },
663− "type": "object",
664− "title": "Providers",
665− "description": "LLM provider configurations"
666− },
667− "agents": {
668− "additionalProperties": {
669− "$ref": "#/$defs/Agent"
670− },
671− "type": "object",
672− "title": "Agents",
673− "description": "Agent configurations for different tasks"
674− },
675− "mcp": {
676− "additionalProperties": {
677− "$ref": "#/$defs/MCP"
678− },
679− "type": "object",
680− "title": "MCP",
681− "description": "Model Control Protocol server configurations"
682− },
683− "lsp": {
684− "additionalProperties": {
685− "$ref": "#/$defs/LSPConfig"
686− },
687− "type": "object",
688− "title": "LSP",
689− "description": "Language Server Protocol configurations"
690− },
691− "options": {
692− "$ref": "#/$defs/Options",
693− "title": "Options",
694− "description": "General application options and settings"
695− }
696− },
697− "type": "object",
698− "title": "Crush Configuration",
699− "description": "Configuration schema for the Crush application"
700−}
crush.json−8
@@ -1,15 +1,7 @@
11{
2− "$schema": "./crush-schema.json",
32 "lsp": {
43 "go": {
54 "command": "gopls"
65 }
7− },
8− "mcp": {
9− "context7": {
10− "command": "",
11− "url": "https://mcp.context7.com/mcp",
12− "type": "http"
13− }
146 }
157}
cspell.json+1−1
@@ -1 +1 @@
1−{"flagWords":[],"words":["afero","alecthomas","bubbletea","charmbracelet","charmtone","Charple","crush","diffview","Emph","filepicker","Focusable","fsext","GROQ","Guac","imageorient","Lanczos","lipgloss","lsps","lucasb","nfnt","oksvg","Preproc","rasterx","rivo","Sourcegraph","srwiley","Strikethrough","termenv","textinput","trashhalo","uniseg","Unticked","genai","jsonschema"],"version":"0.2","language":"en"}
1+{"words":["afero","alecthomas","bubbletea","charmbracelet","charmtone","Charple","crush","diffview","Emph","filepicker","Focusable","fsext","GROQ","Guac","imageorient","Lanczos","lipgloss","lsps","lucasb","nfnt","oksvg","Preproc","rasterx","rivo","Sourcegraph","srwiley","Strikethrough","termenv","textinput","trashhalo","uniseg","Unticked","genai","jsonschema","preconfigured","jsons","qjebbs","LOCALAPPDATA","USERPROFILE","stretchr","cursorrules","VERTEXAI","VERTEXAI"],"flagWords":[],"language":"en","version":"0.2"}
go.mod+25−13
@@ -2,6 +2,10 @@ module github.com/charmbracelet/crush
22
33go 1.24.3
44
5+replace github.com/charmbracelet/bubbletea/v2 => github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250703182356-a42fb608faaf
6+
7+replace github.com/charmbracelet/lipgloss/v2 => github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250703152138-ff346e83e819
8+
59require (
610 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
711 github.com/JohannesKaufmann/html-to-markdown v1.6.0
@@ -13,34 +17,47 @@ require (
1317 github.com/bmatcuk/doublestar/v4 v4.8.1
1418 github.com/charlievieth/fastwalk v1.0.11
1519 github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e
16− github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250609143341-c76fa36f1b94
20+ github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1
1721 github.com/charmbracelet/fang v0.1.0
1822 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
1923 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.2.0.20250703152125-8e1c474f8a71
20− github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413
24+ github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706
25+ github.com/charmbracelet/x/ansi v0.9.3
2126 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250627134340-c144409e381c
2227 github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a
2328 github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
2429 github.com/fsnotify/fsnotify v1.8.0
25− github.com/go-logfmt/logfmt v0.6.0
2630 github.com/google/uuid v1.6.0
27− github.com/invopop/jsonschema v0.13.0
31+ github.com/joho/godotenv v1.5.1
2832 github.com/mark3labs/mcp-go v0.32.0
2933 github.com/muesli/termenv v0.16.0
3034 github.com/ncruces/go-sqlite3 v0.25.0
3135 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
36+ github.com/nxadm/tail v1.4.11
3237 github.com/openai/openai-go v1.8.2
3338 github.com/pressly/goose/v3 v3.24.2
39+ github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c
3440 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
3541 github.com/sahilm/fuzzy v0.1.1
3642 github.com/spf13/cobra v1.9.1
3743 github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c
3844 github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef
3945 github.com/stretchr/testify v1.10.0
46+ golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
47+ gopkg.in/natefinch/lumberjack.v2 v2.2.1
4048 mvdan.cc/sh/v3 v3.11.0
4149)
4250
43−require github.com/spf13/cast v1.7.1 // indirect
51+require (
52+ github.com/go-logfmt/logfmt v0.6.0 // indirect
53+ github.com/spf13/cast v1.7.1 // indirect
54+ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
55+)
56+
57+require (
58+ github.com/charmbracelet/ultraviolet v0.0.0-20250707134318-0fdaa64b8c5e // indirect
59+ github.com/charmbracelet/x/termios v0.1.1 // indirect
60+)
4461
4562require (
4663 cloud.google.com/go v0.116.0 // indirect
@@ -67,13 +84,10 @@ require (
6784 github.com/aws/smithy-go v1.20.3 // indirect
6885 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
6986 github.com/aymerick/douceur v0.2.0 // indirect
70− github.com/bahlo/generic-list-go v0.2.0 // indirect
71− github.com/buger/jsonparser v1.1.1 // indirect
7287 github.com/charmbracelet/colorprofile v0.3.1 // indirect
7388 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa // indirect
7489 github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef
75− github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86 // indirect
76− github.com/charmbracelet/x/term v0.2.1 // indirect
90+ github.com/charmbracelet/x/term v0.2.1
7791 github.com/charmbracelet/x/windows v0.2.1 // indirect
7892 github.com/davecgh/go-spew v1.1.1 // indirect
7993 github.com/disintegration/gift v1.1.2 // indirect
@@ -92,7 +106,6 @@ require (
92106 github.com/inconshreveable/mousetrap v1.1.0 // indirect
93107 github.com/kylelemons/godebug v1.1.0 // indirect
94108 github.com/lucasb-eyer/go-colorful v1.2.0
95− github.com/mailru/easyjson v0.7.7 // indirect
96109 github.com/mattn/go-isatty v0.0.20 // indirect
97110 github.com/mattn/go-runewidth v0.0.16 // indirect
98111 github.com/mfridman/interpolate v0.0.2 // indirect
@@ -114,7 +127,6 @@ require (
114127 github.com/tidwall/match v1.1.1 // indirect
115128 github.com/tidwall/pretty v1.2.1 // indirect
116129 github.com/tidwall/sjson v1.2.5 // indirect
117− github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
118130 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
119131 github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
120132 github.com/yuin/goldmark v1.7.8 // indirect
@@ -128,8 +140,8 @@ require (
128140 golang.org/x/crypto v0.37.0 // indirect
129141 golang.org/x/image v0.26.0 // indirect
130142 golang.org/x/net v0.39.0 // indirect
131− golang.org/x/sync v0.13.0 // indirect
132− golang.org/x/sys v0.32.0 // indirect
143+ golang.org/x/sync v0.15.0 // indirect
144+ golang.org/x/sys v0.33.0 // indirect
133145 golang.org/x/term v0.31.0 // indirect
134146 golang.org/x/text v0.24.0 // indirect
135147 google.golang.org/genai v1.3.0
go.sum+28−25
@@ -64,30 +64,28 @@ github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3v
6464github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
6565github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
6666github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
67−github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
68−github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
6967github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
7068github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
71−github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
72−github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
7369github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr2u/ufj8=
7470github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
7571github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e h1:99Ugtt633rqauFsXjZobZmtkNpeaWialfj8dl6COC6A=
7672github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
77−github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250609143341-c76fa36f1b94 h1:QIi50k+uNTJmp2sMs+33D1m/EWr/7OPTJ8x92AY3eOc=
78−github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250609143341-c76fa36f1b94/go.mod h1:oOn1YZGZyJHxJfh4sFAna9vDzxJRNuErLETr/lnlB/I=
73+github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250703182356-a42fb608faaf h1:td6+JCUKCFW0liyAotsxnGicPxyar4cggCUCT/g0mfQ=
74+github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250703182356-a42fb608faaf/go.mod h1:LTnqM6sOjG1yY5MDFlqefgN+0EFGTV3PU5QoDq9EUDU=
7975github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
8076github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
8177github.com/charmbracelet/fang v0.1.0 h1:SlZS2crf3/zQh7Mr4+W+7QR1k+L08rrPX5rm5z3d7Wg=
8278github.com/charmbracelet/fang v0.1.0/go.mod h1:Zl/zeUQ8EtQuGyiV0ZKZlZPDowKRTzu8s/367EpN/fc=
8379github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe h1:i6ce4CcAlPpTj2ER69m1DBeLZ3RRcHnKExuwhKa3GfY=
8480github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe/go.mod h1:p3Q+aN4eQKeM5jhrmXPMgPrlKbmc59rWSnMsSA3udhk=
85−github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c h1:177KMz8zHRlEZJsWzafbKYh6OdjgvTspoH+UjaxgIXY=
86−github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ=
87−github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.2.0.20250703152125-8e1c474f8a71 h1:X0tsNa2UHCKNw+illiavosasVzqioRo32SRV35iwr2I=
88−github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.2.0.20250703152125-8e1c474f8a71/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ=
89−github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413 h1:L07QkDqRF274IZ2UJ/mCTL8DR95efU9BNWLYCDXEjvQ=
90−github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
81+github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250703152138-ff346e83e819 h1:2i1WGevIAY9eV/vHWwe5Diu58DQ6h9RvKTDpo13zmdA=
82+github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250703152138-ff346e83e819/go.mod h1:kz1eyf3LJ8c5/R2RH/1Pk0kb9BjvbVcY49WCQemk4sg=
83+github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 h1:WkwO6Ks3mSIGnGuSdKl9qDSyfbYK50z2wc2gGMggegE=
84+github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706/go.mod h1:mjJGp00cxcfvD5xdCa+bso251Jt4owrQvuimJtVmEmM=
85+github.com/charmbracelet/ultraviolet v0.0.0-20250707134318-0fdaa64b8c5e h1:wimsDgs/7NRciipaniyugwqHJNTCjJna68bobyPqYx8=
86+github.com/charmbracelet/ultraviolet v0.0.0-20250707134318-0fdaa64b8c5e/go.mod h1:/O+B00+dYG6lqRAWIaNxSvywnDrIH6dmLYQAsH0LRTg=
87+github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
88+github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
9189github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa h1:lphz0Z3rsiOtMYiz8axkT24i9yFiueDhJbzyNUADmME=
9290github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa/go.mod h1:xBlh2Yi3DL3zy/2n15kITpg0YZardf/aa/hgUaIM6Rk=
9391github.com/charmbracelet/x/exp/charmtone v0.0.0-20250627134340-c144409e381c h1:2GELBLPgfSbHU53bsQhR9XIgNuVZ6w+Rz8RWV5Lq+A4=
@@ -96,10 +94,10 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHE
9694github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
9795github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef h1:v7qwsZ2OxzlwvpKwz8dtZXp7fIJlcDEUOyFBNE4fz4Q=
9896github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms=
99−github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86 h1:BxAEmOBIDajkgao3EsbBxKQCYvgYPGdT62WASLvtf4Y=
100−github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86/go.mod h1:62Rp/6EtTxoeJDSdtpA3tJp3y3ZRpsiekBSje+K8htA=
10197github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
10298github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
99+github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
100+github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
103101github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I=
104102github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M=
105103github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -120,6 +118,7 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
120118github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
121119github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
122120github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
121+github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
123122github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
124123github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
125124github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
@@ -153,9 +152,8 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq
153152github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
154153github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
155154github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
156−github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
157−github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
158−github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
155+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
156+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
159157github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
160158github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
161159github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -167,8 +165,6 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
167165github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
168166github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
169167github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
170−github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
171−github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
172168github.com/mark3labs/mcp-go v0.32.0 h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8=
173169github.com/mark3labs/mcp-go v0.32.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
174170github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -199,6 +195,8 @@ github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt
199195github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
200196github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
201197github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
198+github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
199+github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
202200github.com/openai/openai-go v1.8.2 h1:UqSkJ1vCOPUpz9Ka5tS0324EJFEuOvMc+lA/EarJWP8=
203201github.com/openai/openai-go v1.8.2/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
204202github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
@@ -208,6 +206,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
208206github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
209207github.com/pressly/goose/v3 v3.24.2 h1:c/ie0Gm8rnIVKvnDQ/scHErv46jrDv9b4I0WRcFJzYU=
210208github.com/pressly/goose/v3 v3.24.2/go.mod h1:kjefwFB0eR4w30Td2Gj2Mznyw94vSP+2jJYkOVNbD1k=
209+github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c h1:kmzxiX+OB0knCo1V0dkEkdPelzCdAzCURCfmFArn2/A=
210+github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c/go.mod h1:wNJrtinHyC3YSf6giEh4FJN8+yZV7nXBjvmfjhBIcw4=
211211github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
212212github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
213213github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -256,8 +256,6 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
256256github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
257257github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
258258github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
259−github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
260−github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
261259github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
262260github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
263261github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
@@ -311,13 +309,14 @@ golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
311309golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
312310golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
313311golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
314−golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
315−golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
312+golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
313+golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
316314golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
317315golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
318316golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
319317golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
320318golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
319+golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
321320golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
322321golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
323322golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -326,8 +325,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
326325golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
327326golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
328327golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
329−golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
330−golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
328+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
329+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
331330golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
332331golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
333332golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -364,6 +363,10 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
364363gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
365364gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
366365gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
366+gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
367+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
368+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
369+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
367370gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
368371gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
369372gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
internal/app/app.go+15−12
@@ -5,6 +5,7 @@ import (
55 "database/sql"
66 "errors"
77 "fmt"
8+ "log/slog"
89 "maps"
910 "sync"
1011 "time"
@@ -14,7 +15,7 @@ import (
1415 "github.com/charmbracelet/crush/internal/format"
1516 "github.com/charmbracelet/crush/internal/history"
1617 "github.com/charmbracelet/crush/internal/llm/agent"
17− "github.com/charmbracelet/crush/internal/logging"
18+
1819 "github.com/charmbracelet/crush/internal/lsp"
1920 "github.com/charmbracelet/crush/internal/message"
2021 "github.com/charmbracelet/crush/internal/permission"
@@ -36,9 +37,11 @@ type App struct {
3637 watcherCancelFuncs []context.CancelFunc
3738 cancelFuncsMutex sync.Mutex
3839 watcherWG sync.WaitGroup
40+
41+ config *config.Config
3942}
4043
41−func New(ctx context.Context, conn *sql.DB) (*App, error) {
44+func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
4245 q := db.New(conn)
4346 sessions := session.NewService(q)
4447 messages := message.NewService(q)
@@ -48,16 +51,16 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
4851 Sessions: sessions,
4952 Messages: messages,
5053 History: files,
51− Permissions: permission.NewPermissionService(),
54+ Permissions: permission.NewPermissionService(cfg.WorkingDir()),
5255 LSPClients: make(map[string]*lsp.Client),
56+ config: cfg,
5357 }
5458
5559 // Initialize LSP clients in the background
5660 go app.initLSPClients(ctx)
5761
58− cfg := config.Get()
59−
60− coderAgentCfg := cfg.Agents[config.AgentCoder]
62+ // TODO: remove the concept of agent config most likely
63+ coderAgentCfg := cfg.Agents["coder"]
6164 if coderAgentCfg.ID == "" {
6265 return nil, fmt.Errorf("coder agent configuration is missing")
6366 }
@@ -72,7 +75,7 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
7275 app.LSPClients,
7376 )
7477 if err != nil {
75− logging.Error("Failed to create coder agent", err)
78+ slog.Error("Failed to create coder agent", "err", err)
7679 return nil, err
7780 }
7881
@@ -81,7 +84,7 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
8184
8285// RunNonInteractive handles the execution flow when a prompt is provided via CLI flag.
8386func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat string, quiet bool) error {
84− logging.Info("Running in non-interactive mode")
87+ slog.Info("Running in non-interactive mode")
8588
8689 // Start spinner if not in quiet mode
8790 var spinner *format.Spinner
@@ -106,7 +109,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
106109 if err != nil {
107110 return fmt.Errorf("failed to create session for non-interactive mode: %w", err)
108111 }
109− logging.Info("Created session for non-interactive run", "session_id", sess.ID)
112+ slog.Info("Created session for non-interactive run", "session_id", sess.ID)
110113
111114 // Automatically approve all permission requests for this non-interactive session
112115 a.Permissions.AutoApproveSession(sess.ID)
@@ -119,7 +122,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
119122 result := <-done
120123 if result.Error != nil {
121124 if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) {
122− logging.Info("Agent processing cancelled", "session_id", sess.ID)
125+ slog.Info("Agent processing cancelled", "session_id", sess.ID)
123126 return nil
124127 }
125128 return fmt.Errorf("agent processing failed: %w", result.Error)
@@ -138,7 +141,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
138141
139142 fmt.Println(format.FormatOutput(content, outputFormat))
140143
141− logging.Info("Non-interactive run completed", "session_id", sess.ID)
144+ slog.Info("Non-interactive run completed", "session_id", sess.ID)
142145
143146 return nil
144147}
@@ -162,7 +165,7 @@ func (app *App) Shutdown() {
162165 for name, client := range clients {
163166 shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
164167 if err := client.Shutdown(shutdownCtx); err != nil {
165− logging.Error("Failed to shutdown LSP client", "name", name, "error", err)
168+ slog.Error("Failed to shutdown LSP client", "name", name, "error", err)
166169 }
167170 cancel()
168171 }
internal/app/lsp.go+17−20
@@ -2,34 +2,32 @@ package app
22
33import (
44 "context"
5+ "log/slog"
56 "time"
67
7− "github.com/charmbracelet/crush/internal/config"
8− "github.com/charmbracelet/crush/internal/logging"
8+ "github.com/charmbracelet/crush/internal/log"
99 "github.com/charmbracelet/crush/internal/lsp"
1010 "github.com/charmbracelet/crush/internal/lsp/watcher"
1111)
1212
1313func (app *App) initLSPClients(ctx context.Context) {
14− cfg := config.Get()
15−
1614 // Initialize LSP clients
17− for name, clientConfig := range cfg.LSP {
15+ for name, clientConfig := range app.config.LSP {
1816 // Start each client initialization in its own goroutine
1917 go app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
2018 }
21− logging.Info("LSP clients initialization started in background")
19+ slog.Info("LSP clients initialization started in background")
2220}
2321
2422// createAndStartLSPClient creates a new LSP client, initializes it, and starts its workspace watcher
2523func (app *App) createAndStartLSPClient(ctx context.Context, name string, command string, args ...string) {
2624 // Create a specific context for initialization with a timeout
27− logging.Info("Creating LSP client", "name", name, "command", command, "args", args)
25+ slog.Info("Creating LSP client", "name", name, "command", command, "args", args)
2826
2927 // Create the LSP client
3028 lspClient, err := lsp.NewClient(ctx, command, args...)
3129 if err != nil {
32− logging.Error("Failed to create LSP client for", name, err)
30+ slog.Error("Failed to create LSP client for", name, err)
3331 return
3432 }
3533
@@ -38,9 +36,9 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, comman
3836 defer cancel()
3937
4038 // Initialize with the initialization context
41− _, err = lspClient.InitializeLSPClient(initCtx, config.WorkingDirectory())
39+ _, err = lspClient.InitializeLSPClient(initCtx, app.config.WorkingDir())
4240 if err != nil {
43− logging.Error("Initialize failed", "name", name, "error", err)
41+ slog.Error("Initialize failed", "name", name, "error", err)
4442 // Clean up the client to prevent resource leaks
4543 lspClient.Close()
4644 return
@@ -48,15 +46,15 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, comman
4846
4947 // Wait for the server to be ready
5048 if err := lspClient.WaitForServerReady(initCtx); err != nil {
51− logging.Error("Server failed to become ready", "name", name, "error", err)
49+ slog.Error("Server failed to become ready", "name", name, "error", err)
5250 // We'll continue anyway, as some functionality might still work
5351 lspClient.SetServerState(lsp.StateError)
5452 } else {
55− logging.Info("LSP server is ready", "name", name)
53+ slog.Info("LSP server is ready", "name", name)
5654 lspClient.SetServerState(lsp.StateReady)
5755 }
5856
59− logging.Info("LSP client initialized", "name", name)
57+ slog.Info("LSP client initialized", "name", name)
6058
6159 // Create a child context that can be canceled when the app is shutting down
6260 watchCtx, cancelFunc := context.WithCancel(ctx)
@@ -86,22 +84,21 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, comman
8684// runWorkspaceWatcher executes the workspace watcher for an LSP client
8785func (app *App) runWorkspaceWatcher(ctx context.Context, name string, workspaceWatcher *watcher.WorkspaceWatcher) {
8886 defer app.watcherWG.Done()
89− defer logging.RecoverPanic("LSP-"+name, func() {
87+ defer log.RecoverPanic("LSP-"+name, func() {
9088 // Try to restart the client
9189 app.restartLSPClient(ctx, name)
9290 })
9391
94− workspaceWatcher.WatchWorkspace(ctx, config.WorkingDirectory())
95− logging.Info("Workspace watcher stopped", "client", name)
92+ workspaceWatcher.WatchWorkspace(ctx, app.config.WorkingDir())
93+ slog.Info("Workspace watcher stopped", "client", name)
9694}
9795
9896// restartLSPClient attempts to restart a crashed or failed LSP client
9997func (app *App) restartLSPClient(ctx context.Context, name string) {
10098 // Get the original configuration
101− cfg := config.Get()
102− clientConfig, exists := cfg.LSP[name]
99+ clientConfig, exists := app.config.LSP[name]
103100 if !exists {
104− logging.Error("Cannot restart client, configuration not found", "client", name)
101+ slog.Error("Cannot restart client, configuration not found", "client", name)
105102 return
106103 }
107104
@@ -122,5 +119,5 @@ func (app *App) restartLSPClient(ctx context.Context, name string) {
122119
123120 // Create a new client using the shared function
124121 app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
125− logging.Info("Successfully restarted LSP client", "client", name)
122+ slog.Info("Successfully restarted LSP client", "client", name)
126123}
internal/config/config.go+159−1354
@@ -1,28 +1,17 @@
11package config
22
33import (
4− "encoding/json"
5− "errors"
64 "fmt"
7− "log/slog"
8− "maps"
9− "os"
10− "path/filepath"
115 "slices"
126 "strings"
13− "sync"
147
158 "github.com/charmbracelet/crush/internal/fur/provider"
16− "github.com/charmbracelet/crush/internal/logging"
17− "github.com/invopop/jsonschema"
189)
1910
2011const (
12+ appName = "crush"
2113 defaultDataDirectory = ".crush"
2214 defaultLogLevel = "info"
23− appName = "crush"
24−
25− MaxTokensFallbackDefault = 4096
2615)
2716
2817var defaultContextPaths = []string{
@@ -41,82 +30,51 @@ var defaultContextPaths = []string{
4130 "CRUSH.local.md",
4231}
4332
44−type AgentID string
33+type SelectedModelType string
4534
4635const (
47− AgentCoder AgentID = "coder"
48− AgentTask AgentID = "task"
36+ SelectedModelTypeLarge SelectedModelType = "large"
37+ SelectedModelTypeSmall SelectedModelType = "small"
4938)
5039
51−type ModelType string
40+type SelectedModel struct {
41+ // The model id as used by the provider API.
42+ // Required.
43+ Model string `json:"model"`
44+ // The model provider, same as the key/id used in the providers config.
45+ // Required.
46+ Provider string `json:"provider"`
5247
53−const (
54− LargeModel ModelType = "large"
55− SmallModel ModelType = "small"
56−)
48+ // Only used by models that use the openai provider and need this set.
49+ ReasoningEffort string `json:"reasoning_effort,omitempty"`
5750
58−type Model struct {
59− ID string `json:"id" jsonschema:"title=Model ID,description=Unique identifier for the model, the API model"`
60− Name string `json:"name" jsonschema:"title=Model Name,description=Display name of the model"`
61− CostPer1MIn float64 `json:"cost_per_1m_in,omitempty" jsonschema:"title=Input Cost,description=Cost per 1 million input tokens,minimum=0"`
62− CostPer1MOut float64 `json:"cost_per_1m_out,omitempty" jsonschema:"title=Output Cost,description=Cost per 1 million output tokens,minimum=0"`
63− CostPer1MInCached float64 `json:"cost_per_1m_in_cached,omitempty" jsonschema:"title=Cached Input Cost,description=Cost per 1 million cached input tokens,minimum=0"`
64− CostPer1MOutCached float64 `json:"cost_per_1m_out_cached,omitempty" jsonschema:"title=Cached Output Cost,description=Cost per 1 million cached output tokens,minimum=0"`
65− ContextWindow int64 `json:"context_window" jsonschema:"title=Context Window,description=Maximum context window size in tokens,minimum=1"`
66− DefaultMaxTokens int64 `json:"default_max_tokens" jsonschema:"title=Default Max Tokens,description=Default maximum tokens for responses,minimum=1"`
67− CanReason bool `json:"can_reason,omitempty" jsonschema:"title=Can Reason,description=Whether the model supports reasoning capabilities"`
68− ReasoningEffort string `json:"reasoning_effort,omitempty" jsonschema:"title=Reasoning Effort,description=Default reasoning effort level for reasoning models"`
69− HasReasoningEffort bool `json:"has_reasoning_effort,omitempty" jsonschema:"title=Has Reasoning Effort,description=Whether the model supports reasoning effort configuration"`
70− SupportsImages bool `json:"supports_attachments,omitempty" jsonschema:"title=Supports Images,description=Whether the model supports image attachments"`
71−}
51+ // Overrides the default model configuration.
52+ MaxTokens int64 `json:"max_tokens,omitempty"`
7253
73−type VertexAIOptions struct {
74− APIKey string `json:"api_key,omitempty"`
75− Project string `json:"project,omitempty"`
76− Location string `json:"location,omitempty"`
54+ // Used by anthropic models that can reason to indicate if the model should think.
55+ Think bool `json:"think,omitempty"`
7756}
7857
7958type ProviderConfig struct {
80− ID provider.InferenceProvider `json:"id,omitempty" jsonschema:"title=Provider ID,description=Unique identifier for the provider"`
81− BaseURL string `json:"base_url,omitempty" jsonschema:"title=Base URL,description=Base URL for the provider API (required for custom providers)"`
82− ProviderType provider.Type `json:"provider_type" jsonschema:"title=Provider Type,description=Type of the provider (openai, anthropic, etc.)"`
83− APIKey string `json:"api_key,omitempty" jsonschema:"title=API Key,description=API key for authenticating with the provider"`
84− Disabled bool `json:"disabled,omitempty" jsonschema:"title=Disabled,description=Whether this provider is disabled,default=false"`
85− ExtraHeaders map[string]string `json:"extra_headers,omitempty" jsonschema:"title=Extra Headers,description=Additional HTTP headers to send with requests"`
86− // used for e.x for vertex to set the project
87− ExtraParams map[string]string `json:"extra_params,omitempty" jsonschema:"title=Extra Parameters,description=Additional provider-specific parameters"`
88−
89− DefaultLargeModel string `json:"default_large_model,omitempty" jsonschema:"title=Default Large Model,description=Default model ID for large model type"`
90− DefaultSmallModel string `json:"default_small_model,omitempty" jsonschema:"title=Default Small Model,description=Default model ID for small model type"`
91−
92− Models []Model `json:"models,omitempty" jsonschema:"title=Models,description=List of available models for this provider"`
93−}
94−
95−type Agent struct {
96− ID AgentID `json:"id,omitempty" jsonschema:"title=Agent ID,description=Unique identifier for the agent,enum=coder,enum=task"`
97− Name string `json:"name,omitempty" jsonschema:"title=Name,description=Display name of the agent"`
98− Description string `json:"description,omitempty" jsonschema:"title=Description,description=Description of what the agent does"`
99− // This is the id of the system prompt used by the agent
100− Disabled bool `json:"disabled,omitempty" jsonschema:"title=Disabled,description=Whether this agent is disabled,default=false"`
101−
102− Model ModelType `json:"model" jsonschema:"title=Model Type,description=Type of model to use (large or small),enum=large,enum=small"`
59+ // The provider's id.
60+ ID string `json:"id,omitempty"`
61+ // The provider's API endpoint.
62+ BaseURL string `json:"base_url,omitempty"`
63+ // The provider type, e.g. "openai", "anthropic", etc. if empty it defaults to openai.
64+ Type provider.Type `json:"type,omitempty"`
65+ // The provider's API key.
66+ APIKey string `json:"api_key,omitempty"`
67+ // Marks the provider as disabled.
68+ Disable bool `json:"disable,omitempty"`
10369
104− // The available tools for the agent
105− // if this is nil, all tools are available
106− AllowedTools []string `json:"allowed_tools,omitempty" jsonschema:"title=Allowed Tools,description=List of tools this agent is allowed to use (if nil all tools are allowed)"`
70+ // Extra headers to send with each request to the provider.
71+ ExtraHeaders map[string]string
10772
108− // this tells us which MCPs are available for this agent
109− // if this is empty all mcps are available
110− // the string array is the list of tools from the AllowedMCP the agent has available
111− // if the string array is nil, all tools from the AllowedMCP are available
112− AllowedMCP map[string][]string `json:"allowed_mcp,omitempty" jsonschema:"title=Allowed MCP,description=Map of MCP servers this agent can use and their allowed tools"`
73+ // Used to pass extra parameters to the provider.
74+ ExtraParams map[string]string `json:"-"`
11375
114− // The list of LSPs that this agent can use
115− // if this is nil, all LSPs are available
116− AllowedLSP []string `json:"allowed_lsp,omitempty" jsonschema:"title=Allowed LSP,description=List of LSP servers this agent can use (if nil all LSPs are allowed)"`
117−
118− // Overrides the context paths for this agent
119− ContextPaths []string `json:"context_paths,omitempty" jsonschema:"title=Context Paths,description=Custom context paths for this agent (additive to global context paths)"`
76+ // The provider models
77+ Models []provider.Model `json:"models,omitempty"`
12078}
12179
12280type MCPType string
@@ -127,1358 +85,205 @@ const (
12785 MCPHttp MCPType = "http"
12886)
12987
130−type MCP struct {
131− Command string `json:"command,omitempty" jsonschema:"title=Command,description=Command to execute for stdio MCP servers"`
132− Env []string `json:"env,omitempty" jsonschema:"title=Environment,description=Environment variables for the MCP server"`
133− Args []string `json:"args,omitempty" jsonschema:"title=Arguments,description=Command line arguments for the MCP server"`
134− Type MCPType `json:"type" jsonschema:"title=Type,description=Type of MCP connection,enum=stdio,enum=sse,enum=http,default=stdio"`
135− URL string `json:"url,omitempty" jsonschema:"title=URL,description=URL for SSE MCP servers"`
88+type MCPConfig struct {
89+ Command string `json:"command,omitempty" `
90+ Env []string `json:"env,omitempty"`
91+ Args []string `json:"args,omitempty"`
92+ Type MCPType `json:"type"`
93+ URL string `json:"url,omitempty"`
94+
13695 // TODO: maybe make it possible to get the value from the env
137− Headers map[string]string `json:"headers,omitempty" jsonschema:"title=Headers,description=HTTP headers for SSE MCP servers"`
96+ Headers map[string]string `json:"headers,omitempty"`
13897}
13998
14099type LSPConfig struct {
141− Disabled bool `json:"enabled,omitempty" jsonschema:"title=Enabled,description=Whether this LSP server is enabled,default=true"`
142− Command string `json:"command" jsonschema:"title=Command,description=Command to execute for the LSP server"`
143− Args []string `json:"args,omitempty" jsonschema:"title=Arguments,description=Command line arguments for the LSP server"`
144− Options any `json:"options,omitempty" jsonschema:"title=Options,description=LSP server specific options"`
100+ Disabled bool `json:"enabled,omitempty"`
101+ Command string `json:"command"`
102+ Args []string `json:"args,omitempty"`
103+ Options any `json:"options,omitempty"`
145104}
146105
147106type TUIOptions struct {
148− CompactMode bool `json:"compact_mode" jsonschema:"title=Compact Mode,description=Enable compact mode for the TUI,default=false"`
107+ CompactMode bool `json:"compact_mode,omitempty"`
149108 // Here we can add themes later or any TUI related options
150109}
151110
152111type Options struct {
153− ContextPaths []string `json:"context_paths,omitempty" jsonschema:"title=Context Paths,description=List of paths to search for context files"`
154− TUI TUIOptions `json:"tui,omitempty" jsonschema:"title=TUI Options,description=Terminal UI configuration options"`
155− Debug bool `json:"debug,omitempty" jsonschema:"title=Debug,description=Enable debug logging,default=false"`
156− DebugLSP bool `json:"debug_lsp,omitempty" jsonschema:"title=Debug LSP,description=Enable LSP debug logging,default=false"`
157− DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty" jsonschema:"title=Disable Auto Summarize,description=Disable automatic conversation summarization,default=false"`
112+ ContextPaths []string `json:"context_paths,omitempty"`
113+ TUI *TUIOptions `json:"tui,omitempty"`
114+ Debug bool `json:"debug,omitempty"`
115+ DebugLSP bool `json:"debug_lsp,omitempty"`
116+ DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty"`
158117 // Relative to the cwd
159− DataDirectory string `json:"data_directory,omitempty" jsonschema:"title=Data Directory,description=Directory for storing application data,default=.crush"`
118+ DataDirectory string `json:"data_directory,omitempty"`
160119}
161120
162−type PreferredModel struct {
163− ModelID string `json:"model_id" jsonschema:"title=Model ID,description=ID of the preferred model"`
164− Provider provider.InferenceProvider `json:"provider" jsonschema:"title=Provider,description=Provider for the preferred model"`
165− // ReasoningEffort overrides the default reasoning effort for this model
166− ReasoningEffort string `json:"reasoning_effort,omitempty" jsonschema:"title=Reasoning Effort,description=Override reasoning effort for this model"`
167− // MaxTokens overrides the default max tokens for this model
168− MaxTokens int64 `json:"max_tokens,omitempty" jsonschema:"title=Max Tokens,description=Override max tokens for this model,minimum=1"`
121+type MCPs map[string]MCPConfig
169122
170− // Think indicates if the model should think, only applicable for anthropic reasoning models
171− Think bool `json:"think,omitempty" jsonschema:"title=Think,description=Enable thinking for reasoning models,default=false"`
172−}
173−
174−type PreferredModels struct {
175− Large PreferredModel `json:"large,omitempty" jsonschema:"title=Large Model,description=Preferred model configuration for large model type"`
176− Small PreferredModel `json:"small,omitempty" jsonschema:"title=Small Model,description=Preferred model configuration for small model type"`
177−}
178−
179−type Config struct {
180− Models PreferredModels `json:"models,omitempty" jsonschema:"title=Models,description=Preferred model configurations for large and small model types"`
181− // List of configured providers
182− Providers map[provider.InferenceProvider]ProviderConfig `json:"providers,omitempty" jsonschema:"title=Providers,description=LLM provider configurations"`
183−
184− // List of configured agents
185− Agents map[AgentID]Agent `json:"agents,omitempty" jsonschema:"title=Agents,description=Agent configurations for different tasks"`
186−
187− // List of configured MCPs
188− MCP map[string]MCP `json:"mcp,omitempty" jsonschema:"title=MCP,description=Model Control Protocol server configurations"`
189−
190− // List of configured LSPs
191− LSP map[string]LSPConfig `json:"lsp,omitempty" jsonschema:"title=LSP,description=Language Server Protocol configurations"`
192−
193− // Miscellaneous options
194− Options Options `json:"options,omitempty" jsonschema:"title=Options,description=General application options and settings"`
195−}
196−
197−var (
198− instance *Config // The single instance of the Singleton
199− cwd string
200− once sync.Once // Ensures the initialization happens only once
201−
202−)
203−
204−func readConfigFile(path string) (*Config, error) {
205− var cfg *Config
206− if _, err := os.Stat(path); err != nil && !os.IsNotExist(err) {
207− // some other error occurred while checking the file
208− return nil, err
209− } else if err == nil {
210− // config file exists, read it
211− file, err := os.ReadFile(path)
212− if err != nil {
213− return nil, err
214− }
215− cfg = &Config{}
216− if err := json.Unmarshal(file, cfg); err != nil {
217− return nil, err
218− }
219− } else {
220− // config file does not exist, create a new one
221− cfg = &Config{}
222− }
223− return cfg, nil
123+type MCP struct {
124+ Name string `json:"name"`
125+ MCP MCPConfig `json:"mcp"`
224126}
225127
226−func loadConfig(cwd string, debug bool) (*Config, error) {
227− // First read the global config file
228− cfgPath := ConfigPath()
229−
230− cfg := defaultConfigBasedOnEnv()
231− cfg.Options.Debug = debug
232− defaultLevel := slog.LevelInfo
233− if cfg.Options.Debug {
234− defaultLevel = slog.LevelDebug
128+func (m MCPs) Sorted() []MCP {
129+ sorted := make([]MCP, 0, len(m))
130+ for k, v := range m {
131+ sorted = append(sorted, MCP{
132+ Name: k,
133+ MCP: v,
134+ })
235135 }
236− if os.Getenv("CRUSH_DEV_DEBUG") == "true" {
237− loggingFile := fmt.Sprintf("%s/%s", cfg.Options.DataDirectory, "debug.log")
238−
239− // if file does not exist create it
240− if _, err := os.Stat(loggingFile); os.IsNotExist(err) {
241− if err := os.MkdirAll(cfg.Options.DataDirectory, 0o755); err != nil {
242− return cfg, fmt.Errorf("failed to create directory: %w", err)
243− }
244− if _, err := os.Create(loggingFile); err != nil {
245− return cfg, fmt.Errorf("failed to create log file: %w", err)
246− }
247− }
248−
249− messagesPath := fmt.Sprintf("%s/%s", cfg.Options.DataDirectory, "messages")
250−
251− if _, err := os.Stat(messagesPath); os.IsNotExist(err) {
252− if err := os.MkdirAll(messagesPath, 0o756); err != nil {
253− return cfg, fmt.Errorf("failed to create directory: %w", err)
254− }
255− }
256− logging.MessageDir = messagesPath
257−
258− sloggingFileWriter, err := os.OpenFile(loggingFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
259− if err != nil {
260− return cfg, fmt.Errorf("failed to open log file: %w", err)
261− }
262− // Configure logger
263− logger := slog.New(slog.NewTextHandler(sloggingFileWriter, &slog.HandlerOptions{
264− Level: defaultLevel,
265− }))
266− slog.SetDefault(logger)
267− } else {
268− // Configure logger
269− logger := slog.New(slog.NewTextHandler(logging.NewWriter(), &slog.HandlerOptions{
270− Level: defaultLevel,
271− }))
272− slog.SetDefault(logger)
273− }
274−
275− priorityOrderedConfigFiles := []string{
276− cfgPath, // Global config file
277− filepath.Join(cwd, "crush.json"), // Local config file
278− filepath.Join(cwd, ".crush.json"), // Local config file
279− }
280−
281− configs := make([]*Config, 0)
282− for _, path := range priorityOrderedConfigFiles {
283− localConfig, err := readConfigFile(path)
284− if err != nil {
285− return nil, fmt.Errorf("failed to read config file %s: %w", path, err)
286− }
287− if localConfig != nil {
288− // If the config file was read successfully, add it to the list
289− configs = append(configs, localConfig)
290− }
291− }
292−
293− // merge options
294− mergeOptions(cfg, configs...)
295−
296− mergeProviderConfigs(cfg, configs...)
297− // no providers found the app is not initialized yet
298− if len(cfg.Providers) == 0 {
299− return cfg, nil
300− }
301− preferredProvider := getPreferredProvider(cfg.Providers)
302− if preferredProvider != nil {
303− cfg.Models = PreferredModels{
304− Large: PreferredModel{
305− ModelID: preferredProvider.DefaultLargeModel,
306− Provider: preferredProvider.ID,
307− },
308− Small: PreferredModel{
309− ModelID: preferredProvider.DefaultSmallModel,
310− Provider: preferredProvider.ID,
311− },
312− }
313− } else {
314− // No valid providers found, set empty models
315− cfg.Models = PreferredModels{}
316− }
317−
318− mergeModels(cfg, configs...)
319−
320− agents := map[AgentID]Agent{
321− AgentCoder: {
322− ID: AgentCoder,
323− Name: "Coder",
324− Description: "An agent that helps with executing coding tasks.",
325− Model: LargeModel,
326− ContextPaths: cfg.Options.ContextPaths,
327− // All tools allowed
328− },
329− AgentTask: {
330− ID: AgentTask,
331− Name: "Task",
332− Description: "An agent that helps with searching for context and finding implementation details.",
333− Model: LargeModel,
334− ContextPaths: cfg.Options.ContextPaths,
335− AllowedTools: []string{
336− "glob",
337− "grep",
338− "ls",
339− "sourcegraph",
340− "view",
341− },
342− // NO MCPs or LSPs by default
343− AllowedMCP: map[string][]string{},
344− AllowedLSP: []string{},
345− },
346− }
347− cfg.Agents = agents
348− mergeAgents(cfg, configs...)
349− mergeMCPs(cfg, configs...)
350− mergeLSPs(cfg, configs...)
351−
352− // Validate the final configuration
353− if err := cfg.Validate(); err != nil {
354− return cfg, fmt.Errorf("configuration validation failed: %w", err)
355− }
356−
357− return cfg, nil
358−}
359−
360−func Init(workingDir string, debug bool) (*Config, error) {
361− var err error
362− once.Do(func() {
363− cwd = workingDir
364− instance, err = loadConfig(cwd, debug)
365− if err != nil {
366− logging.Error("Failed to load config", "error", err)
367− }
136+ slices.SortFunc(sorted, func(a, b MCP) int {
137+ return strings.Compare(a.Name, b.Name)
368138 })
369−
370− return instance, err
139+ return sorted
371140}
372141
373−func Get() *Config {
374− if instance == nil {
375− // TODO: Handle this better
376− panic("Config not initialized. Call InitConfig first.")
377− }
378− return instance
379−}
380−
381−func getPreferredProvider(configuredProviders map[provider.InferenceProvider]ProviderConfig) *ProviderConfig {
382− providers := Providers()
383− for _, p := range providers {
384− if providerConfig, ok := configuredProviders[p.ID]; ok && !providerConfig.Disabled {
385− return &providerConfig
386− }
387− }
388− // if none found return the first configured provider
389− for _, providerConfig := range configuredProviders {
390− if !providerConfig.Disabled {
391− return &providerConfig
392− }
393− }
394− return nil
395−}
396−
397−func mergeProviderConfig(p provider.InferenceProvider, base, other ProviderConfig) ProviderConfig {
398− if other.APIKey != "" {
399− base.APIKey = other.APIKey
400− }
401− // Only change these options if the provider is not a known provider
402− if !slices.Contains(provider.KnownProviders(), p) {
403− if other.BaseURL != "" {
404− base.BaseURL = other.BaseURL
405− }
406− if other.ProviderType != "" {
407− base.ProviderType = other.ProviderType
408− }
409− if len(other.ExtraHeaders) > 0 {
410− if base.ExtraHeaders == nil {
411− base.ExtraHeaders = make(map[string]string)
412− }
413− maps.Copy(base.ExtraHeaders, other.ExtraHeaders)
414− }
415− if len(other.ExtraParams) > 0 {
416− if base.ExtraParams == nil {
417− base.ExtraParams = make(map[string]string)
418− }
419− maps.Copy(base.ExtraParams, other.ExtraParams)
420− }
421− }
422−
423− if other.Disabled {
424− base.Disabled = other.Disabled
425− }
426−
427− if other.DefaultLargeModel != "" {
428− base.DefaultLargeModel = other.DefaultLargeModel
429− }
430− // Add new models if they don't exist
431− if other.Models != nil {
432− for _, model := range other.Models {
433− // check if the model already exists
434− exists := false
435− for _, existingModel := range base.Models {
436− if existingModel.ID == model.ID {
437− exists = true
438− break
439− }
440− }
441− if !exists {
442− base.Models = append(base.Models, model)
443− }
444− }
445− }
446−
447− return base
448−}
142+type LSPs map[string]LSPConfig
449143
450−func validateProvider(p provider.InferenceProvider, providerConfig ProviderConfig) error {
451− if !slices.Contains(provider.KnownProviders(), p) {
452− if providerConfig.ProviderType != provider.TypeOpenAI {
453− return errors.New("invalid provider type: " + string(providerConfig.ProviderType))
454− }
455− if providerConfig.BaseURL == "" {
456− return errors.New("base URL must be set for custom providers")
457− }
458− if providerConfig.APIKey == "" {
459− return errors.New("API key must be set for custom providers")
460− }
461− }
462− return nil
144+type LSP struct {
145+ Name string `json:"name"`
146+ LSP LSPConfig `json:"lsp"`
463147}
464148
465−func mergeModels(base *Config, others ...*Config) {
466− for _, cfg := range others {
467− if cfg == nil {
468− continue
469− }
470− if cfg.Models.Large.ModelID != "" && cfg.Models.Large.Provider != "" {
471− base.Models.Large = cfg.Models.Large
472− }
473−
474− if cfg.Models.Small.ModelID != "" && cfg.Models.Small.Provider != "" {
475− base.Models.Small = cfg.Models.Small
476− }
149+func (l LSPs) Sorted() []LSP {
150+ sorted := make([]LSP, 0, len(l))
151+ for k, v := range l {
152+ sorted = append(sorted, LSP{
153+ Name: k,
154+ LSP: v,
155+ })
477156 }
157+ slices.SortFunc(sorted, func(a, b LSP) int {
158+ return strings.Compare(a.Name, b.Name)
159+ })
160+ return sorted
478161}
479162
480−func mergeOptions(base *Config, others ...*Config) {
481− for _, cfg := range others {
482− if cfg == nil {
483− continue
484− }
485− baseOptions := base.Options
486− other := cfg.Options
487− if len(other.ContextPaths) > 0 {
488− baseOptions.ContextPaths = append(baseOptions.ContextPaths, other.ContextPaths...)
489− }
163+type Agent struct {
164+ ID string `json:"id,omitempty"`
165+ Name string `json:"name,omitempty"`
166+ Description string `json:"description,omitempty"`
167+ // This is the id of the system prompt used by the agent
168+ Disabled bool `json:"disabled,omitempty"`
490169
491− if other.TUI.CompactMode {
492− baseOptions.TUI.CompactMode = other.TUI.CompactMode
493− }
170+ Model SelectedModelType `json:"model"`
494171
495− if other.Debug {
496− baseOptions.Debug = other.Debug
497− }
172+ // The available tools for the agent
173+ // if this is nil, all tools are available
174+ AllowedTools []string `json:"allowed_tools,omitempty"`
498175
499− if other.DebugLSP {
500− baseOptions.DebugLSP = other.DebugLSP
501− }
176+ // this tells us which MCPs are available for this agent
177+ // if this is empty all mcps are available
178+ // the string array is the list of tools from the AllowedMCP the agent has available
179+ // if the string array is nil, all tools from the AllowedMCP are available
180+ AllowedMCP map[string][]string `json:"allowed_mcp,omitempty"`
502181
503− if other.DisableAutoSummarize {
504− baseOptions.DisableAutoSummarize = other.DisableAutoSummarize
505− }
182+ // The list of LSPs that this agent can use
183+ // if this is nil, all LSPs are available
184+ AllowedLSP []string `json:"allowed_lsp,omitempty"`
506185
507− if other.DataDirectory != "" {
508− baseOptions.DataDirectory = other.DataDirectory
509− }
510− base.Options = baseOptions
511− }
186+ // Overrides the context paths for this agent
187+ ContextPaths []string `json:"context_paths,omitempty"`
512188}
513189
514−func mergeAgents(base *Config, others ...*Config) {
515− for _, cfg := range others {
516− if cfg == nil {
517− continue
518− }
519− for agentID, newAgent := range cfg.Agents {
520− if _, ok := base.Agents[agentID]; !ok {
521− newAgent.ID = agentID
522− if newAgent.Model == "" {
523− newAgent.Model = LargeModel
524− }
525− if len(newAgent.ContextPaths) > 0 {
526− newAgent.ContextPaths = append(base.Options.ContextPaths, newAgent.ContextPaths...)
527− } else {
528− newAgent.ContextPaths = base.Options.ContextPaths
529− }
530− base.Agents[agentID] = newAgent
531− } else {
532− baseAgent := base.Agents[agentID]
190+// Config holds the configuration for crush.
191+type Config struct {
192+ // We currently only support large/small as values here.
193+ Models map[SelectedModelType]SelectedModel `json:"models,omitempty"`
533194
534− if agentID == AgentCoder || agentID == AgentTask {
535− if newAgent.Model != "" {
536− baseAgent.Model = newAgent.Model
537− }
538− if newAgent.AllowedMCP != nil {
539− baseAgent.AllowedMCP = newAgent.AllowedMCP
540− }
541− if newAgent.AllowedLSP != nil {
542− baseAgent.AllowedLSP = newAgent.AllowedLSP
543− }
544− // Context paths are additive for known agents too
545− if len(newAgent.ContextPaths) > 0 {
546− baseAgent.ContextPaths = append(baseAgent.ContextPaths, newAgent.ContextPaths...)
547− }
548− } else {
549− if newAgent.Name != "" {
550− baseAgent.Name = newAgent.Name
551− }
552− if newAgent.Description != "" {
553− baseAgent.Description = newAgent.Description
554− }
555− if newAgent.Model != "" {
556− baseAgent.Model = newAgent.Model
557− } else if baseAgent.Model == "" {
558− baseAgent.Model = LargeModel
559− }
195+ // The providers that are configured
196+ Providers map[string]ProviderConfig `json:"providers,omitempty"`
560197
561− baseAgent.Disabled = newAgent.Disabled
198+ MCP MCPs `json:"mcp,omitempty"`
562199
563− if newAgent.AllowedTools != nil {
564− baseAgent.AllowedTools = newAgent.AllowedTools
565− }
566− if newAgent.AllowedMCP != nil {
567− baseAgent.AllowedMCP = newAgent.AllowedMCP
568− }
569− if newAgent.AllowedLSP != nil {
570− baseAgent.AllowedLSP = newAgent.AllowedLSP
571− }
572− if len(newAgent.ContextPaths) > 0 {
573− baseAgent.ContextPaths = append(baseAgent.ContextPaths, newAgent.ContextPaths...)
574− }
575− }
200+ LSP LSPs `json:"lsp,omitempty"`
576201
577− base.Agents[agentID] = baseAgent
578− }
579− }
580− }
581−}
202+ Options *Options `json:"options,omitempty"`
582203
583−func mergeMCPs(base *Config, others ...*Config) {
584− for _, cfg := range others {
585− if cfg == nil {
586− continue
587− }
588− maps.Copy(base.MCP, cfg.MCP)
589− }
204+ // Internal
205+ workingDir string `json:"-"`
206+ // TODO: most likely remove this concept when I come back to it
207+ Agents map[string]Agent `json:"-"`
208+ // TODO: find a better way to do this this should probably not be part of the config
209+ resolver VariableResolver
590210}
591211
592−func mergeLSPs(base *Config, others ...*Config) {
593− for _, cfg := range others {
594− if cfg == nil {
595− continue
596− }
597− maps.Copy(base.LSP, cfg.LSP)
598− }
212+func (c *Config) WorkingDir() string {
213+ return c.workingDir
599214}
600215
601−func mergeProviderConfigs(base *Config, others ...*Config) {
602− for _, cfg := range others {
603− if cfg == nil {
604− continue
605− }
606− for providerName, p := range cfg.Providers {
607− p.ID = providerName
608− if _, ok := base.Providers[providerName]; !ok {
609− if slices.Contains(provider.KnownProviders(), providerName) {
610− providers := Providers()
611− for _, providerDef := range providers {
612− if providerDef.ID == providerName {
613− logging.Info("Using default provider config for", "provider", providerName)
614− baseProvider := getDefaultProviderConfig(providerDef, providerDef.APIKey)
615− base.Providers[providerName] = mergeProviderConfig(providerName, baseProvider, p)
616− break
617− }
618− }
619− } else {
620− base.Providers[providerName] = p
621− }
622− } else {
623− base.Providers[providerName] = mergeProviderConfig(providerName, base.Providers[providerName], p)
624− }
625− }
626− }
627−
628− finalProviders := make(map[provider.InferenceProvider]ProviderConfig)
629− for providerName, providerConfig := range base.Providers {
630− err := validateProvider(providerName, providerConfig)
631− if err != nil {
632− logging.Warn("Skipping provider", "name", providerName, "error", err)
633− continue // Skip invalid providers
216+func (c *Config) EnabledProviders() []ProviderConfig {
217+ enabled := make([]ProviderConfig, 0, len(c.Providers))
218+ for _, p := range c.Providers {
219+ if !p.Disable {
220+ enabled = append(enabled, p)
634221 }
635− finalProviders[providerName] = providerConfig
636222 }
637− base.Providers = finalProviders
223+ return enabled
638224}
639225
640−func providerDefaultConfig(providerID provider.InferenceProvider) ProviderConfig {
641− switch providerID {
642− case provider.InferenceProviderAnthropic:
643− return ProviderConfig{
644− ID: providerID,
645− ProviderType: provider.TypeAnthropic,
646− }
647− case provider.InferenceProviderOpenAI:
648− return ProviderConfig{
649− ID: providerID,
650− ProviderType: provider.TypeOpenAI,
651− }
652− case provider.InferenceProviderGemini:
653− return ProviderConfig{
654− ID: providerID,
655− ProviderType: provider.TypeGemini,
656− }
657− case provider.InferenceProviderBedrock:
658− return ProviderConfig{
659− ID: providerID,
660− ProviderType: provider.TypeBedrock,
661− }
662− case provider.InferenceProviderAzure:
663− return ProviderConfig{
664− ID: providerID,
665− ProviderType: provider.TypeAzure,
666− }
667− case provider.InferenceProviderOpenRouter:
668− return ProviderConfig{
669− ID: providerID,
670− ProviderType: provider.TypeOpenAI,
671− BaseURL: "https://openrouter.ai/api/v1",
672− ExtraHeaders: map[string]string{
673− "HTTP-Referer": "crush.charm.land",
674− "X-Title": "Crush",
675− },
676− }
677− case provider.InferenceProviderXAI:
678− return ProviderConfig{
679− ID: providerID,
680− ProviderType: provider.TypeXAI,
681− BaseURL: "https://api.x.ai/v1",
682− }
683− case provider.InferenceProviderVertexAI:
684− return ProviderConfig{
685− ID: providerID,
686− ProviderType: provider.TypeVertexAI,
687− }
688− default:
689− return ProviderConfig{
690− ID: providerID,
691− ProviderType: provider.TypeOpenAI,
692− }
693− }
226+// IsConfigured return true if at least one provider is configured
227+func (c *Config) IsConfigured() bool {
228+ return len(c.EnabledProviders()) > 0
694229}
695230
696−func getDefaultProviderConfig(p provider.Provider, apiKey string) ProviderConfig {
697− providerConfig := providerDefaultConfig(p.ID)
698− providerConfig.APIKey = apiKey
699− providerConfig.DefaultLargeModel = p.DefaultLargeModelID
700− providerConfig.DefaultSmallModel = p.DefaultSmallModelID
701− baseURL := p.APIEndpoint
702− if strings.HasPrefix(baseURL, "$") {
703− envVar := strings.TrimPrefix(baseURL, "$")
704− baseURL = os.Getenv(envVar)
705− }
706− providerConfig.BaseURL = baseURL
707− for _, model := range p.Models {
708− configModel := Model{
709− ID: model.ID,
710− Name: model.Name,
711− CostPer1MIn: model.CostPer1MIn,
712− CostPer1MOut: model.CostPer1MOut,
713− CostPer1MInCached: model.CostPer1MInCached,
714− CostPer1MOutCached: model.CostPer1MOutCached,
715− ContextWindow: model.ContextWindow,
716− DefaultMaxTokens: model.DefaultMaxTokens,
717− CanReason: model.CanReason,
718− SupportsImages: model.SupportsImages,
719− }
720− // Set reasoning effort for reasoning models
721− if model.HasReasoningEffort && model.DefaultReasoningEffort != "" {
722− configModel.HasReasoningEffort = model.HasReasoningEffort
723− configModel.ReasoningEffort = model.DefaultReasoningEffort
724− }
725− providerConfig.Models = append(providerConfig.Models, configModel)
726− }
727− return providerConfig
728−}
729−
730−func defaultConfigBasedOnEnv() *Config {
731− cfg := &Config{
732− Options: Options{
733− DataDirectory: defaultDataDirectory,
734− ContextPaths: defaultContextPaths,
735− },
736− Providers: make(map[provider.InferenceProvider]ProviderConfig),
737− Agents: make(map[AgentID]Agent),
738− LSP: make(map[string]LSPConfig),
739− MCP: make(map[string]MCP),
740− }
741−
742− providers := Providers()
743−
744− for _, p := range providers {
745− if strings.HasPrefix(p.APIKey, "$") {
746− envVar := strings.TrimPrefix(p.APIKey, "$")
747− if apiKey := os.Getenv(envVar); apiKey != "" {
748− cfg.Providers[p.ID] = getDefaultProviderConfig(p, apiKey)
749− }
750− }
751− }
752− // TODO: support local models
753−
754− if useVertexAI := os.Getenv("GOOGLE_GENAI_USE_VERTEXAI"); useVertexAI == "true" {
755− providerConfig := providerDefaultConfig(provider.InferenceProviderVertexAI)
756− providerConfig.ExtraParams = map[string]string{
757− "project": os.Getenv("GOOGLE_CLOUD_PROJECT"),
758− "location": os.Getenv("GOOGLE_CLOUD_LOCATION"),
759− }
760− // Find the VertexAI provider definition to get default models
761− for _, p := range providers {
762− if p.ID == provider.InferenceProviderVertexAI {
763− providerConfig.DefaultLargeModel = p.DefaultLargeModelID
764− providerConfig.DefaultSmallModel = p.DefaultSmallModelID
765− for _, model := range p.Models {
766− configModel := Model{
767− ID: model.ID,
768− Name: model.Name,
769− CostPer1MIn: model.CostPer1MIn,
770− CostPer1MOut: model.CostPer1MOut,
771− CostPer1MInCached: model.CostPer1MInCached,
772− CostPer1MOutCached: model.CostPer1MOutCached,
773− ContextWindow: model.ContextWindow,
774− DefaultMaxTokens: model.DefaultMaxTokens,
775− CanReason: model.CanReason,
776− SupportsImages: model.SupportsImages,
777− }
778− // Set reasoning effort for reasoning models
779− if model.HasReasoningEffort && model.DefaultReasoningEffort != "" {
780− configModel.HasReasoningEffort = model.HasReasoningEffort
781− configModel.ReasoningEffort = model.DefaultReasoningEffort
782− }
783− providerConfig.Models = append(providerConfig.Models, configModel)
784− }
785− break
786− }
787− }
788− cfg.Providers[provider.InferenceProviderVertexAI] = providerConfig
789− }
790−
791− if hasAWSCredentials() {
792− providerConfig := providerDefaultConfig(provider.InferenceProviderBedrock)
793− providerConfig.ExtraParams = map[string]string{
794− "region": os.Getenv("AWS_DEFAULT_REGION"),
795− }
796− if providerConfig.ExtraParams["region"] == "" {
797− providerConfig.ExtraParams["region"] = os.Getenv("AWS_REGION")
798− }
799− // Find the Bedrock provider definition to get default models
800− for _, p := range providers {
801− if p.ID == provider.InferenceProviderBedrock {
802− providerConfig.DefaultLargeModel = p.DefaultLargeModelID
803− providerConfig.DefaultSmallModel = p.DefaultSmallModelID
804− for _, model := range p.Models {
805− configModel := Model{
806− ID: model.ID,
807− Name: model.Name,
808− CostPer1MIn: model.CostPer1MIn,
809− CostPer1MOut: model.CostPer1MOut,
810− CostPer1MInCached: model.CostPer1MInCached,
811− CostPer1MOutCached: model.CostPer1MOutCached,
812− ContextWindow: model.ContextWindow,
813− DefaultMaxTokens: model.DefaultMaxTokens,
814− CanReason: model.CanReason,
815− SupportsImages: model.SupportsImages,
816− }
817− // Set reasoning effort for reasoning models
818− if model.HasReasoningEffort && model.DefaultReasoningEffort != "" {
819− configModel.HasReasoningEffort = model.HasReasoningEffort
820− configModel.ReasoningEffort = model.DefaultReasoningEffort
821− }
822− providerConfig.Models = append(providerConfig.Models, configModel)
823− }
824− break
231+func (c *Config) GetModel(provider, model string) *provider.Model {
232+ if providerConfig, ok := c.Providers[provider]; ok {
233+ for _, m := range providerConfig.Models {
234+ if m.ID == model {
235+ return &m
825236 }
826237 }
827− cfg.Providers[provider.InferenceProviderBedrock] = providerConfig
828− }
829− return cfg
830−}
831−
832−func hasAWSCredentials() bool {
833− if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
834− return true
835238 }
836−
837− if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" {
838− return true
839− }
840−
841− if os.Getenv("AWS_REGION") != "" || os.Getenv("AWS_DEFAULT_REGION") != "" {
842− return true
843− }
844−
845− if os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
846− os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
847− return true
848− }
849−
850− return false
851−}
852−
853−func WorkingDirectory() string {
854− return cwd
239+ return nil
855240}
856241
857−// TODO: Handle error state
858−
859−func GetAgentModel(agentID AgentID) Model {
860− cfg := Get()
861− agent, ok := cfg.Agents[agentID]
862− if !ok {
863− logging.Error("Agent not found", "agent_id", agentID)
864− return Model{}
865− }
866−
867− var model PreferredModel
868− switch agent.Model {
869− case LargeModel:
870− model = cfg.Models.Large
871− case SmallModel:
872− model = cfg.Models.Small
873− default:
874− logging.Warn("Unknown model type for agent", "agent_id", agentID, "model_type", agent.Model)
875− model = cfg.Models.Large // Fallback to large model
876− }
877− providerConfig, ok := cfg.Providers[model.Provider]
242+func (c *Config) GetProviderForModel(modelType SelectedModelType) *ProviderConfig {
243+ model, ok := c.Models[modelType]
878244 if !ok {
879− logging.Error("Provider not found for agent", "agent_id", agentID, "provider", model.Provider)
880− return Model{}
245+ return nil
881246 }
882−
883− for _, m := range providerConfig.Models {
884− if m.ID == model.ModelID {
885− return m
886− }
247+ if providerConfig, ok := c.Providers[model.Provider]; ok {
248+ return &providerConfig
887249 }
888−
889− logging.Error("Model not found for agent", "agent_id", agentID, "model", agent.Model)
890− return Model{}
250+ return nil
891251}
892252
893−func GetAgentProvider(agentID AgentID) ProviderConfig {
894− cfg := Get()
895− agent, ok := cfg.Agents[agentID]
253+func (c *Config) GetModelByType(modelType SelectedModelType) *provider.Model {
254+ model, ok := c.Models[modelType]
896255 if !ok {
897− logging.Error("Agent not found", "agent_id", agentID)
898− return ProviderConfig{}
256+ return nil
899257 }
900−
901− var model PreferredModel
902− switch agent.Model {
903− case LargeModel:
904− model = cfg.Models.Large
905− case SmallModel:
906− model = cfg.Models.Small
907− default:
908− logging.Warn("Unknown model type for agent", "agent_id", agentID, "model_type", agent.Model)
909− model = cfg.Models.Large // Fallback to large model
910− }
911−
912− providerConfig, ok := cfg.Providers[model.Provider]
913− if !ok {
914− logging.Error("Provider not found for agent", "agent_id", agentID, "provider", model.Provider)
915− return ProviderConfig{}
916− }
917−
918− return providerConfig
258+ return c.GetModel(model.Provider, model.Model)
919259}
920260
921−func GetProviderModel(provider provider.InferenceProvider, modelID string) Model {
922− cfg := Get()
923− providerConfig, ok := cfg.Providers[provider]
261+func (c *Config) LargeModel() *provider.Model {
262+ model, ok := c.Models[SelectedModelTypeLarge]
924263 if !ok {
925− logging.Error("Provider not found", "provider", provider)
926− return Model{}
264+ return nil
927265 }
928−
929− for _, model := range providerConfig.Models {
930− if model.ID == modelID {
931− return model
932− }
933− }
934−
935− logging.Error("Model not found for provider", "provider", provider, "model_id", modelID)
936− return Model{}
266+ return c.GetModel(model.Provider, model.Model)
937267}
938268
939−func GetModel(modelType ModelType) Model {
940− cfg := Get()
941− var model PreferredModel
942− switch modelType {
943− case LargeModel:
944− model = cfg.Models.Large
945− case SmallModel:
946− model = cfg.Models.Small
947− default:
948− model = cfg.Models.Large // Fallback to large model
949− }
950− providerConfig, ok := cfg.Providers[model.Provider]
269+func (c *Config) SmallModel() *provider.Model {
270+ model, ok := c.Models[SelectedModelTypeSmall]
951271 if !ok {
952− return Model{}
272+ return nil
953273 }
954−
955− for _, m := range providerConfig.Models {
956− if m.ID == model.ModelID {
957− return m
958− }
959− }
960− return Model{}
274+ return c.GetModel(model.Provider, model.Model)
961275}
962276
963−func UpdatePreferredModel(modelType ModelType, model PreferredModel) error {
964− cfg := Get()
965− switch modelType {
966− case LargeModel:
967− cfg.Models.Large = model
968− case SmallModel:
969− cfg.Models.Small = model
970− default:
971− return fmt.Errorf("unknown model type: %s", modelType)
277+func (c *Config) Resolve(key string) (string, error) {
278+ if c.resolver == nil {
279+ return "", fmt.Errorf("no variable resolver configured")
972280 }
973− return nil
974−}
975−
976−// ValidationError represents a configuration validation error
977−type ValidationError struct {
978− Field string
979− Message string
281+ return c.resolver.ResolveValue(key)
980282}
981283
982−func (e ValidationError) Error() string {
983− return fmt.Sprintf("validation error in %s: %s", e.Field, e.Message)
984−}
985−
986−// ValidationErrors represents multiple validation errors
987−type ValidationErrors []ValidationError
988−
989−func (e ValidationErrors) Error() string {
990− if len(e) == 0 {
991− return "no validation errors"
992− }
993− if len(e) == 1 {
994− return e[0].Error()
995− }
996−
997− var messages []string
998− for _, err := range e {
999− messages = append(messages, err.Error())
1000− }
1001− return fmt.Sprintf("multiple validation errors: %s", strings.Join(messages, "; "))
1002−}
1003−
1004−// HasErrors returns true if there are any validation errors
1005−func (e ValidationErrors) HasErrors() bool {
1006− return len(e) > 0
1007−}
1008−
1009−// Add appends a new validation error
1010−func (e *ValidationErrors) Add(field, message string) {
1011− *e = append(*e, ValidationError{Field: field, Message: message})
1012−}
1013−
1014−// Validate performs comprehensive validation of the configuration
1015−func (c *Config) Validate() error {
1016− var errors ValidationErrors
1017−
1018− // Validate providers
1019− c.validateProviders(&errors)
1020−
1021− // Validate models
1022− c.validateModels(&errors)
1023−
1024− // Validate agents
1025− c.validateAgents(&errors)
1026−
1027− // Validate options
1028− c.validateOptions(&errors)
1029−
1030− // Validate MCP configurations
1031− c.validateMCPs(&errors)
1032−
1033− // Validate LSP configurations
1034− c.validateLSPs(&errors)
1035−
1036− // Validate cross-references
1037− c.validateCrossReferences(&errors)
1038−
1039− // Validate completeness
1040− c.validateCompleteness(&errors)
1041−
1042− if errors.HasErrors() {
1043− return errors
1044− }
1045−
284+// TODO: maybe handle this better
285+func UpdatePreferredModel(modelType SelectedModelType, model SelectedModel) error {
286+ cfg := Get()
287+ cfg.Models[modelType] = model
1046288 return nil
1047289}
1048−
1049−// validateProviders validates all provider configurations
1050−func (c *Config) validateProviders(errors *ValidationErrors) {
1051− if c.Providers == nil {
1052− c.Providers = make(map[provider.InferenceProvider]ProviderConfig)
1053− }
1054−
1055− knownProviders := provider.KnownProviders()
1056− validTypes := []provider.Type{
1057− provider.TypeOpenAI,
1058− provider.TypeAnthropic,
1059− provider.TypeGemini,
1060− provider.TypeAzure,
1061− provider.TypeBedrock,
1062− provider.TypeVertexAI,
1063− provider.TypeXAI,
1064− }
1065−
1066− for providerID, providerConfig := range c.Providers {
1067− fieldPrefix := fmt.Sprintf("providers.%s", providerID)
1068−
1069− // Validate API key for non-disabled providers
1070− if !providerConfig.Disabled && providerConfig.APIKey == "" {
1071− // Special case for AWS Bedrock and VertexAI which may use other auth methods
1072− if providerID != provider.InferenceProviderBedrock && providerID != provider.InferenceProviderVertexAI {
1073− errors.Add(fieldPrefix+".api_key", "API key is required for non-disabled providers")
1074− }
1075− }
1076−
1077− // Validate provider type
1078− validType := slices.Contains(validTypes, providerConfig.ProviderType)
1079− if !validType {
1080− errors.Add(fieldPrefix+".provider_type", fmt.Sprintf("invalid provider type: %s", providerConfig.ProviderType))
1081− }
1082−
1083− // Validate custom providers
1084− isKnownProvider := slices.Contains(knownProviders, providerID)
1085−
1086− if !isKnownProvider {
1087− // Custom provider validation
1088− if providerConfig.BaseURL == "" {
1089− errors.Add(fieldPrefix+".base_url", "BaseURL is required for custom providers")
1090− }
1091− if providerConfig.ProviderType != provider.TypeOpenAI {
1092− errors.Add(fieldPrefix+".provider_type", "custom providers currently only support OpenAI type")
1093− }
1094− }
1095−
1096− // Validate models
1097− modelIDs := make(map[string]bool)
1098− for i, model := range providerConfig.Models {
1099− modelFieldPrefix := fmt.Sprintf("%s.models[%d]", fieldPrefix, i)
1100−
1101− // Check for duplicate model IDs
1102− if modelIDs[model.ID] {
1103− errors.Add(modelFieldPrefix+".id", fmt.Sprintf("duplicate model ID: %s", model.ID))
1104− }
1105− modelIDs[model.ID] = true
1106−
1107− // Validate required model fields
1108− if model.ID == "" {
1109− errors.Add(modelFieldPrefix+".id", "model ID is required")
1110− }
1111− if model.Name == "" {
1112− errors.Add(modelFieldPrefix+".name", "model name is required")
1113− }
1114− if model.ContextWindow <= 0 {
1115− errors.Add(modelFieldPrefix+".context_window", "context window must be positive")
1116− }
1117− if model.DefaultMaxTokens <= 0 {
1118− errors.Add(modelFieldPrefix+".default_max_tokens", "default max tokens must be positive")
1119− }
1120− if model.DefaultMaxTokens > model.ContextWindow {
1121− errors.Add(modelFieldPrefix+".default_max_tokens", "default max tokens cannot exceed context window")
1122− }
1123−
1124− // Validate cost fields
1125− if model.CostPer1MIn < 0 {
1126− errors.Add(modelFieldPrefix+".cost_per_1m_in", "cost per 1M input tokens cannot be negative")
1127− }
1128− if model.CostPer1MOut < 0 {
1129− errors.Add(modelFieldPrefix+".cost_per_1m_out", "cost per 1M output tokens cannot be negative")
1130− }
1131− if model.CostPer1MInCached < 0 {
1132− errors.Add(modelFieldPrefix+".cost_per_1m_in_cached", "cached cost per 1M input tokens cannot be negative")
1133− }
1134− if model.CostPer1MOutCached < 0 {
1135− errors.Add(modelFieldPrefix+".cost_per_1m_out_cached", "cached cost per 1M output tokens cannot be negative")
1136− }
1137− }
1138−
1139− // Validate default model references
1140− if providerConfig.DefaultLargeModel != "" {
1141− if !modelIDs[providerConfig.DefaultLargeModel] {
1142− errors.Add(fieldPrefix+".default_large_model", fmt.Sprintf("default large model '%s' not found in provider models", providerConfig.DefaultLargeModel))
1143− }
1144− }
1145− if providerConfig.DefaultSmallModel != "" {
1146− if !modelIDs[providerConfig.DefaultSmallModel] {
1147− errors.Add(fieldPrefix+".default_small_model", fmt.Sprintf("default small model '%s' not found in provider models", providerConfig.DefaultSmallModel))
1148− }
1149− }
1150−
1151− // Validate provider-specific requirements
1152− c.validateProviderSpecific(providerID, providerConfig, errors)
1153− }
1154−}
1155−
1156−// validateProviderSpecific validates provider-specific requirements
1157−func (c *Config) validateProviderSpecific(providerID provider.InferenceProvider, providerConfig ProviderConfig, errors *ValidationErrors) {
1158− fieldPrefix := fmt.Sprintf("providers.%s", providerID)
1159−
1160− switch providerID {
1161− case provider.InferenceProviderVertexAI:
1162− if !providerConfig.Disabled {
1163− if providerConfig.ExtraParams == nil {
1164− errors.Add(fieldPrefix+".extra_params", "VertexAI requires extra_params configuration")
1165− } else {
1166− if providerConfig.ExtraParams["project"] == "" {
1167− errors.Add(fieldPrefix+".extra_params.project", "VertexAI requires project parameter")
1168− }
1169− if providerConfig.ExtraParams["location"] == "" {
1170− errors.Add(fieldPrefix+".extra_params.location", "VertexAI requires location parameter")
1171− }
1172− }
1173− }
1174− case provider.InferenceProviderBedrock:
1175− if !providerConfig.Disabled {
1176− if providerConfig.ExtraParams == nil || providerConfig.ExtraParams["region"] == "" {
1177− errors.Add(fieldPrefix+".extra_params.region", "Bedrock requires region parameter")
1178− }
1179− // Check for AWS credentials in environment
1180− if !hasAWSCredentials() {
1181− errors.Add(fieldPrefix, "Bedrock requires AWS credentials in environment")
1182− }
1183− }
1184− }
1185−}
1186−
1187−// validateModels validates preferred model configurations
1188−func (c *Config) validateModels(errors *ValidationErrors) {
1189− // Validate large model
1190− if c.Models.Large.ModelID != "" || c.Models.Large.Provider != "" {
1191− if c.Models.Large.ModelID == "" {
1192− errors.Add("models.large.model_id", "large model ID is required when provider is set")
1193− }
1194− if c.Models.Large.Provider == "" {
1195− errors.Add("models.large.provider", "large model provider is required when model ID is set")
1196− }
1197−
1198− // Check if provider exists and is not disabled
1199− if providerConfig, exists := c.Providers[c.Models.Large.Provider]; exists {
1200− if providerConfig.Disabled {
1201− errors.Add("models.large.provider", "large model provider is disabled")
1202− }
1203−
1204− // Check if model exists in provider
1205− modelExists := false
1206− for _, model := range providerConfig.Models {
1207− if model.ID == c.Models.Large.ModelID {
1208− modelExists = true
1209− break
1210− }
1211− }
1212− if !modelExists {
1213− errors.Add("models.large.model_id", fmt.Sprintf("large model '%s' not found in provider '%s'", c.Models.Large.ModelID, c.Models.Large.Provider))
1214− }
1215− } else {
1216− errors.Add("models.large.provider", fmt.Sprintf("large model provider '%s' not found", c.Models.Large.Provider))
1217− }
1218− }
1219−
1220− // Validate small model
1221− if c.Models.Small.ModelID != "" || c.Models.Small.Provider != "" {
1222− if c.Models.Small.ModelID == "" {
1223− errors.Add("models.small.model_id", "small model ID is required when provider is set")
1224− }
1225− if c.Models.Small.Provider == "" {
1226− errors.Add("models.small.provider", "small model provider is required when model ID is set")
1227− }
1228−
1229− // Check if provider exists and is not disabled
1230− if providerConfig, exists := c.Providers[c.Models.Small.Provider]; exists {
1231− if providerConfig.Disabled {
1232− errors.Add("models.small.provider", "small model provider is disabled")
1233− }
1234−
1235− // Check if model exists in provider
1236− modelExists := false
1237− for _, model := range providerConfig.Models {
1238− if model.ID == c.Models.Small.ModelID {
1239− modelExists = true
1240− break
1241− }
1242− }
1243− if !modelExists {
1244− errors.Add("models.small.model_id", fmt.Sprintf("small model '%s' not found in provider '%s'", c.Models.Small.ModelID, c.Models.Small.Provider))
1245− }
1246− } else {
1247− errors.Add("models.small.provider", fmt.Sprintf("small model provider '%s' not found", c.Models.Small.Provider))
1248− }
1249− }
1250−}
1251−
1252−// validateAgents validates agent configurations
1253−func (c *Config) validateAgents(errors *ValidationErrors) {
1254− if c.Agents == nil {
1255− c.Agents = make(map[AgentID]Agent)
1256− }
1257−
1258− validTools := []string{
1259− "bash", "edit", "fetch", "glob", "grep", "ls", "sourcegraph", "view", "write", "agent",
1260− }
1261−
1262− for agentID, agent := range c.Agents {
1263− fieldPrefix := fmt.Sprintf("agents.%s", agentID)
1264−
1265− // Validate agent ID consistency
1266− if agent.ID != agentID {
1267− errors.Add(fieldPrefix+".id", fmt.Sprintf("agent ID mismatch: expected '%s', got '%s'", agentID, agent.ID))
1268− }
1269−
1270− // Validate required fields
1271− if agent.ID == "" {
1272− errors.Add(fieldPrefix+".id", "agent ID is required")
1273− }
1274− if agent.Name == "" {
1275− errors.Add(fieldPrefix+".name", "agent name is required")
1276− }
1277−
1278− // Validate model type
1279− if agent.Model != LargeModel && agent.Model != SmallModel {
1280− errors.Add(fieldPrefix+".model", fmt.Sprintf("invalid model type: %s (must be 'large' or 'small')", agent.Model))
1281− }
1282−
1283− // Validate allowed tools
1284− if agent.AllowedTools != nil {
1285− for i, tool := range agent.AllowedTools {
1286− validTool := slices.Contains(validTools, tool)
1287− if !validTool {
1288− errors.Add(fmt.Sprintf("%s.allowed_tools[%d]", fieldPrefix, i), fmt.Sprintf("unknown tool: %s", tool))
1289− }
1290− }
1291− }
1292−
1293− // Validate MCP references
1294− if agent.AllowedMCP != nil {
1295− for mcpName := range agent.AllowedMCP {
1296− if _, exists := c.MCP[mcpName]; !exists {
1297− errors.Add(fieldPrefix+".allowed_mcp", fmt.Sprintf("referenced MCP '%s' not found", mcpName))
1298− }
1299− }
1300− }
1301−
1302− // Validate LSP references
1303− if agent.AllowedLSP != nil {
1304− for _, lspName := range agent.AllowedLSP {
1305− if _, exists := c.LSP[lspName]; !exists {
1306− errors.Add(fieldPrefix+".allowed_lsp", fmt.Sprintf("referenced LSP '%s' not found", lspName))
1307− }
1308− }
1309− }
1310−
1311− // Validate context paths (basic path validation)
1312− for i, contextPath := range agent.ContextPaths {
1313− if contextPath == "" {
1314− errors.Add(fmt.Sprintf("%s.context_paths[%d]", fieldPrefix, i), "context path cannot be empty")
1315− }
1316− // Check for invalid characters in path
1317− if strings.Contains(contextPath, "\x00") {
1318− errors.Add(fmt.Sprintf("%s.context_paths[%d]", fieldPrefix, i), "context path contains invalid characters")
1319− }
1320− }
1321−
1322− // Validate known agents maintain their core properties
1323− if agentID == AgentCoder {
1324− if agent.Name != "Coder" {
1325− errors.Add(fieldPrefix+".name", "coder agent name cannot be changed")
1326− }
1327− if agent.Description != "An agent that helps with executing coding tasks." {
1328− errors.Add(fieldPrefix+".description", "coder agent description cannot be changed")
1329− }
1330− } else if agentID == AgentTask {
1331− if agent.Name != "Task" {
1332− errors.Add(fieldPrefix+".name", "task agent name cannot be changed")
1333− }
1334− if agent.Description != "An agent that helps with searching for context and finding implementation details." {
1335− errors.Add(fieldPrefix+".description", "task agent description cannot be changed")
1336− }
1337− expectedTools := []string{"glob", "grep", "ls", "sourcegraph", "view"}
1338− if agent.AllowedTools != nil && !slices.Equal(agent.AllowedTools, expectedTools) {
1339− errors.Add(fieldPrefix+".allowed_tools", "task agent allowed tools cannot be changed")
1340− }
1341− }
1342− }
1343−}
1344−
1345−// validateOptions validates configuration options
1346−func (c *Config) validateOptions(errors *ValidationErrors) {
1347− // Validate data directory
1348− if c.Options.DataDirectory == "" {
1349− errors.Add("options.data_directory", "data directory is required")
1350− }
1351−
1352− // Validate context paths
1353− for i, contextPath := range c.Options.ContextPaths {
1354− if contextPath == "" {
1355− errors.Add(fmt.Sprintf("options.context_paths[%d]", i), "context path cannot be empty")
1356− }
1357− if strings.Contains(contextPath, "\x00") {
1358− errors.Add(fmt.Sprintf("options.context_paths[%d]", i), "context path contains invalid characters")
1359− }
1360− }
1361−}
1362−
1363−// validateMCPs validates MCP configurations
1364−func (c *Config) validateMCPs(errors *ValidationErrors) {
1365− if c.MCP == nil {
1366− c.MCP = make(map[string]MCP)
1367− }
1368−
1369− for mcpName, mcpConfig := range c.MCP {
1370− fieldPrefix := fmt.Sprintf("mcp.%s", mcpName)
1371−
1372− // Validate MCP type
1373− if mcpConfig.Type != MCPStdio && mcpConfig.Type != MCPSse && mcpConfig.Type != MCPHttp {
1374− errors.Add(fieldPrefix+".type", fmt.Sprintf("invalid MCP type: %s (must be 'stdio' or 'sse' or 'http')", mcpConfig.Type))
1375− }
1376−
1377− // Validate based on type
1378− if mcpConfig.Type == MCPStdio {
1379− if mcpConfig.Command == "" {
1380− errors.Add(fieldPrefix+".command", "command is required for stdio MCP")
1381− }
1382− } else if mcpConfig.Type == MCPSse {
1383− if mcpConfig.URL == "" {
1384− errors.Add(fieldPrefix+".url", "URL is required for SSE MCP")
1385− }
1386− }
1387− }
1388−}
1389−
1390−// validateLSPs validates LSP configurations
1391−func (c *Config) validateLSPs(errors *ValidationErrors) {
1392− if c.LSP == nil {
1393− c.LSP = make(map[string]LSPConfig)
1394− }
1395−
1396− for lspName, lspConfig := range c.LSP {
1397− fieldPrefix := fmt.Sprintf("lsp.%s", lspName)
1398−
1399− if lspConfig.Command == "" {
1400− errors.Add(fieldPrefix+".command", "command is required for LSP")
1401− }
1402− }
1403−}
1404−
1405−// validateCrossReferences validates cross-references between different config sections
1406−func (c *Config) validateCrossReferences(errors *ValidationErrors) {
1407− // Validate that agents can use their assigned model types
1408− for agentID, agent := range c.Agents {
1409− fieldPrefix := fmt.Sprintf("agents.%s", agentID)
1410−
1411− var preferredModel PreferredModel
1412− switch agent.Model {
1413− case LargeModel:
1414− preferredModel = c.Models.Large
1415− case SmallModel:
1416− preferredModel = c.Models.Small
1417− }
1418−
1419− if preferredModel.Provider != "" {
1420− if providerConfig, exists := c.Providers[preferredModel.Provider]; exists {
1421− if providerConfig.Disabled {
1422− errors.Add(fieldPrefix+".model", fmt.Sprintf("agent cannot use model type '%s' because provider '%s' is disabled", agent.Model, preferredModel.Provider))
1423− }
1424− }
1425− }
1426− }
1427−}
1428−
1429−// validateCompleteness validates that the configuration is complete and usable
1430−func (c *Config) validateCompleteness(errors *ValidationErrors) {
1431− // Check for at least one valid, non-disabled provider
1432− hasValidProvider := false
1433− for _, providerConfig := range c.Providers {
1434− if !providerConfig.Disabled {
1435− hasValidProvider = true
1436− break
1437− }
1438− }
1439− if !hasValidProvider {
1440− errors.Add("providers", "at least one non-disabled provider is required")
1441− }
1442−
1443− // Check that default agents exist
1444− if _, exists := c.Agents[AgentCoder]; !exists {
1445− errors.Add("agents", "coder agent is required")
1446− }
1447− if _, exists := c.Agents[AgentTask]; !exists {
1448− errors.Add("agents", "task agent is required")
1449− }
1450−
1451− // Check that preferred models are set if providers exist
1452− if hasValidProvider {
1453− if c.Models.Large.ModelID == "" || c.Models.Large.Provider == "" {
1454− errors.Add("models.large", "large preferred model must be configured when providers are available")
1455− }
1456− if c.Models.Small.ModelID == "" || c.Models.Small.Provider == "" {
1457− errors.Add("models.small", "small preferred model must be configured when providers are available")
1458− }
1459− }
1460−}
1461−
1462−// JSONSchemaExtend adds custom schema properties for AgentID
1463−func (AgentID) JSONSchemaExtend(schema *jsonschema.Schema) {
1464− schema.Enum = []any{
1465− string(AgentCoder),
1466− string(AgentTask),
1467− }
1468−}
1469−
1470−// JSONSchemaExtend adds custom schema properties for ModelType
1471−func (ModelType) JSONSchemaExtend(schema *jsonschema.Schema) {
1472− schema.Enum = []any{
1473− string(LargeModel),
1474− string(SmallModel),
1475− }
1476−}
1477−
1478−// JSONSchemaExtend adds custom schema properties for MCPType
1479−func (MCPType) JSONSchemaExtend(schema *jsonschema.Schema) {
1480− schema.Enum = []any{
1481− string(MCPStdio),
1482− string(MCPSse),
1483− }
1484−}
internal/config/config_test.godeleted−2075
@@ -1,2075 +0,0 @@
1−package config
2−
3−import (
4− "encoding/json"
5− "os"
6− "path/filepath"
7− "sync"
8− "testing"
9−
10− "github.com/charmbracelet/crush/internal/fur/provider"
11− "github.com/stretchr/testify/assert"
12− "github.com/stretchr/testify/require"
13−)
14−
15−func reset() {
16− // Clear all environment variables that could affect config
17− envVarsToUnset := []string{
18− // API Keys
19− "ANTHROPIC_API_KEY",
20− "OPENAI_API_KEY",
21− "GEMINI_API_KEY",
22− "XAI_API_KEY",
23− "OPENROUTER_API_KEY",
24−
25− // Google Cloud / VertexAI
26− "GOOGLE_GENAI_USE_VERTEXAI",
27− "GOOGLE_CLOUD_PROJECT",
28− "GOOGLE_CLOUD_LOCATION",
29−
30− // AWS Credentials
31− "AWS_ACCESS_KEY_ID",
32− "AWS_SECRET_ACCESS_KEY",
33− "AWS_REGION",
34− "AWS_DEFAULT_REGION",
35− "AWS_PROFILE",
36− "AWS_DEFAULT_PROFILE",
37− "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
38− "AWS_CONTAINER_CREDENTIALS_FULL_URI",
39−
40− // Other
41− "CRUSH_DEV_DEBUG",
42− }
43−
44− for _, envVar := range envVarsToUnset {
45− os.Unsetenv(envVar)
46− }
47−
48− // Reset singleton
49− once = sync.Once{}
50− instance = nil
51− cwd = ""
52− testConfigDir = ""
53−
54− // Enable mock providers for all tests to avoid API calls
55− UseMockProviders = true
56− ResetProviders()
57−}
58−
59−// Core Configuration Loading Tests
60−
61−func TestInit_ValidWorkingDirectory(t *testing.T) {
62− reset()
63− testConfigDir = t.TempDir()
64− cwdDir := t.TempDir()
65−
66− cfg, err := Init(cwdDir, false)
67−
68− require.NoError(t, err)
69− assert.NotNil(t, cfg)
70− assert.Equal(t, cwdDir, WorkingDirectory())
71− assert.Equal(t, defaultDataDirectory, cfg.Options.DataDirectory)
72− assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
73−}
74−
75−func TestInit_WithDebugFlag(t *testing.T) {
76− reset()
77− testConfigDir = t.TempDir()
78− cwdDir := t.TempDir()
79−
80− cfg, err := Init(cwdDir, true)
81−
82− require.NoError(t, err)
83− assert.True(t, cfg.Options.Debug)
84−}
85−
86−func TestInit_SingletonBehavior(t *testing.T) {
87− reset()
88− testConfigDir = t.TempDir()
89− cwdDir := t.TempDir()
90−
91− cfg1, err1 := Init(cwdDir, false)
92− cfg2, err2 := Init(cwdDir, false)
93−
94− require.NoError(t, err1)
95− require.NoError(t, err2)
96− assert.Same(t, cfg1, cfg2)
97−}
98−
99−func TestGet_BeforeInitialization(t *testing.T) {
100− reset()
101−
102− assert.Panics(t, func() {
103− Get()
104− })
105−}
106−
107−func TestGet_AfterInitialization(t *testing.T) {
108− reset()
109− testConfigDir = t.TempDir()
110− cwdDir := t.TempDir()
111−
112− cfg1, err := Init(cwdDir, false)
113− require.NoError(t, err)
114−
115− cfg2 := Get()
116− assert.Same(t, cfg1, cfg2)
117−}
118−
119−func TestLoadConfig_NoConfigFiles(t *testing.T) {
120− reset()
121− testConfigDir = t.TempDir()
122− cwdDir := t.TempDir()
123−
124− cfg, err := Init(cwdDir, false)
125−
126− require.NoError(t, err)
127− assert.Len(t, cfg.Providers, 0)
128− assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
129−}
130−
131−func TestLoadConfig_OnlyGlobalConfig(t *testing.T) {
132− reset()
133− testConfigDir = t.TempDir()
134− cwdDir := t.TempDir()
135−
136− globalConfig := Config{
137− Providers: map[provider.InferenceProvider]ProviderConfig{
138− provider.InferenceProviderOpenAI: {
139− ID: provider.InferenceProviderOpenAI,
140− APIKey: "test-key",
141− ProviderType: provider.TypeOpenAI,
142− DefaultLargeModel: "gpt-4",
143− DefaultSmallModel: "gpt-3.5-turbo",
144− Models: []Model{
145− {
146− ID: "gpt-4",
147− Name: "GPT-4",
148− CostPer1MIn: 30.0,
149− CostPer1MOut: 60.0,
150− ContextWindow: 8192,
151− DefaultMaxTokens: 4096,
152− },
153− {
154− ID: "gpt-3.5-turbo",
155− Name: "GPT-3.5 Turbo",
156− CostPer1MIn: 1.0,
157− CostPer1MOut: 2.0,
158− ContextWindow: 4096,
159− DefaultMaxTokens: 4096,
160− },
161− },
162− },
163− },
164− Options: Options{
165− ContextPaths: []string{"custom-context.md"},
166− },
167− }
168−
169− configPath := filepath.Join(testConfigDir, "crush.json")
170− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
171−
172− data, err := json.Marshal(globalConfig)
173− require.NoError(t, err)
174− require.NoError(t, os.WriteFile(configPath, data, 0o644))
175−
176− cfg, err := Init(cwdDir, false)
177−
178− require.NoError(t, err)
179− assert.Len(t, cfg.Providers, 1)
180− assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
181− assert.Contains(t, cfg.Options.ContextPaths, "custom-context.md")
182−}
183−
184−func TestLoadConfig_OnlyLocalConfig(t *testing.T) {
185− reset()
186− testConfigDir = t.TempDir()
187− cwdDir := t.TempDir()
188−
189− localConfig := Config{
190− Providers: map[provider.InferenceProvider]ProviderConfig{
191− provider.InferenceProviderAnthropic: {
192− ID: provider.InferenceProviderAnthropic,
193− APIKey: "local-key",
194− ProviderType: provider.TypeAnthropic,
195− DefaultLargeModel: "claude-3-opus",
196− DefaultSmallModel: "claude-3-haiku",
197− Models: []Model{
198− {
199− ID: "claude-3-opus",
200− Name: "Claude 3 Opus",
201− CostPer1MIn: 15.0,
202− CostPer1MOut: 75.0,
203− ContextWindow: 200000,
204− DefaultMaxTokens: 4096,
205− },
206− {
207− ID: "claude-3-haiku",
208− Name: "Claude 3 Haiku",
209− CostPer1MIn: 0.25,
210− CostPer1MOut: 1.25,
211− ContextWindow: 200000,
212− DefaultMaxTokens: 4096,
213− },
214− },
215− },
216− },
217− Options: Options{
218− TUI: TUIOptions{CompactMode: true},
219− },
220− }
221−
222− localConfigPath := filepath.Join(cwdDir, "crush.json")
223− data, err := json.Marshal(localConfig)
224− require.NoError(t, err)
225− require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
226−
227− cfg, err := Init(cwdDir, false)
228−
229− require.NoError(t, err)
230− assert.Len(t, cfg.Providers, 1)
231− assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
232− assert.True(t, cfg.Options.TUI.CompactMode)
233−}
234−
235−func TestLoadConfig_BothGlobalAndLocal(t *testing.T) {
236− reset()
237− testConfigDir = t.TempDir()
238− cwdDir := t.TempDir()
239−
240− globalConfig := Config{
241− Providers: map[provider.InferenceProvider]ProviderConfig{
242− provider.InferenceProviderOpenAI: {
243− ID: provider.InferenceProviderOpenAI,
244− APIKey: "global-key",
245− ProviderType: provider.TypeOpenAI,
246− DefaultLargeModel: "gpt-4",
247− DefaultSmallModel: "gpt-3.5-turbo",
248− Models: []Model{
249− {
250− ID: "gpt-4",
251− Name: "GPT-4",
252− CostPer1MIn: 30.0,
253− CostPer1MOut: 60.0,
254− ContextWindow: 8192,
255− DefaultMaxTokens: 4096,
256− },
257− {
258− ID: "gpt-3.5-turbo",
259− Name: "GPT-3.5 Turbo",
260− CostPer1MIn: 1.0,
261− CostPer1MOut: 2.0,
262− ContextWindow: 4096,
263− DefaultMaxTokens: 4096,
264− },
265− },
266− },
267− },
268− Options: Options{
269− ContextPaths: []string{"global-context.md"},
270− },
271− }
272−
273− configPath := filepath.Join(testConfigDir, "crush.json")
274− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
275− data, err := json.Marshal(globalConfig)
276− require.NoError(t, err)
277− require.NoError(t, os.WriteFile(configPath, data, 0o644))
278−
279− localConfig := Config{
280− Providers: map[provider.InferenceProvider]ProviderConfig{
281− provider.InferenceProviderOpenAI: {
282− APIKey: "local-key", // Override global
283− },
284− provider.InferenceProviderAnthropic: {
285− ID: provider.InferenceProviderAnthropic,
286− APIKey: "anthropic-key",
287− ProviderType: provider.TypeAnthropic,
288− DefaultLargeModel: "claude-3-opus",
289− DefaultSmallModel: "claude-3-haiku",
290− Models: []Model{
291− {
292− ID: "claude-3-opus",
293− Name: "Claude 3 Opus",
294− CostPer1MIn: 15.0,
295− CostPer1MOut: 75.0,
296− ContextWindow: 200000,
297− DefaultMaxTokens: 4096,
298− },
299− {
300− ID: "claude-3-haiku",
301− Name: "Claude 3 Haiku",
302− CostPer1MIn: 0.25,
303− CostPer1MOut: 1.25,
304− ContextWindow: 200000,
305− DefaultMaxTokens: 4096,
306− },
307− },
308− },
309− },
310− Options: Options{
311− ContextPaths: []string{"local-context.md"},
312− TUI: TUIOptions{CompactMode: true},
313− },
314− }
315−
316− localConfigPath := filepath.Join(cwdDir, "crush.json")
317− data, err = json.Marshal(localConfig)
318− require.NoError(t, err)
319− require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
320−
321− cfg, err := Init(cwdDir, false)
322−
323− require.NoError(t, err)
324− assert.Len(t, cfg.Providers, 2)
325−
326− openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
327− assert.Equal(t, "local-key", openaiProvider.APIKey)
328−
329− assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
330−
331− assert.Contains(t, cfg.Options.ContextPaths, "global-context.md")
332− assert.Contains(t, cfg.Options.ContextPaths, "local-context.md")
333− assert.True(t, cfg.Options.TUI.CompactMode)
334−}
335−
336−func TestLoadConfig_MalformedGlobalJSON(t *testing.T) {
337− reset()
338− testConfigDir = t.TempDir()
339− cwdDir := t.TempDir()
340−
341− configPath := filepath.Join(testConfigDir, "crush.json")
342− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
343− require.NoError(t, os.WriteFile(configPath, []byte(`{invalid json`), 0o644))
344−
345− _, err := Init(cwdDir, false)
346− assert.Error(t, err)
347−}
348−
349−func TestLoadConfig_MalformedLocalJSON(t *testing.T) {
350− reset()
351− testConfigDir = t.TempDir()
352− cwdDir := t.TempDir()
353−
354− localConfigPath := filepath.Join(cwdDir, "crush.json")
355− require.NoError(t, os.WriteFile(localConfigPath, []byte(`{invalid json`), 0o644))
356−
357− _, err := Init(cwdDir, false)
358− assert.Error(t, err)
359−}
360−
361−func TestConfigWithoutEnv(t *testing.T) {
362− reset()
363− testConfigDir = t.TempDir()
364− cwdDir := t.TempDir()
365−
366− cfg, _ := Init(cwdDir, false)
367− assert.Len(t, cfg.Providers, 0)
368−}
369−
370−func TestConfigWithEnv(t *testing.T) {
371− reset()
372− testConfigDir = t.TempDir()
373− cwdDir := t.TempDir()
374−
375− os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
376− os.Setenv("OPENAI_API_KEY", "test-openai-key")
377− os.Setenv("GEMINI_API_KEY", "test-gemini-key")
378− os.Setenv("XAI_API_KEY", "test-xai-key")
379− os.Setenv("OPENROUTER_API_KEY", "test-openrouter-key")
380−
381− cfg, _ := Init(cwdDir, false)
382− assert.Len(t, cfg.Providers, 5)
383−}
384−
385−// Environment Variable Tests
386−
387−func TestEnvVars_NoEnvironmentVariables(t *testing.T) {
388− reset()
389− testConfigDir = t.TempDir()
390− cwdDir := t.TempDir()
391−
392− cfg, err := Init(cwdDir, false)
393−
394− require.NoError(t, err)
395− assert.Len(t, cfg.Providers, 0)
396−}
397−
398−func TestEnvVars_AllSupportedAPIKeys(t *testing.T) {
399− reset()
400− testConfigDir = t.TempDir()
401− cwdDir := t.TempDir()
402−
403− os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
404− os.Setenv("OPENAI_API_KEY", "test-openai-key")
405− os.Setenv("GEMINI_API_KEY", "test-gemini-key")
406− os.Setenv("XAI_API_KEY", "test-xai-key")
407− os.Setenv("OPENROUTER_API_KEY", "test-openrouter-key")
408−
409− cfg, err := Init(cwdDir, false)
410−
411− require.NoError(t, err)
412− assert.Len(t, cfg.Providers, 5)
413−
414− anthropicProvider := cfg.Providers[provider.InferenceProviderAnthropic]
415− assert.Equal(t, "test-anthropic-key", anthropicProvider.APIKey)
416− assert.Equal(t, provider.TypeAnthropic, anthropicProvider.ProviderType)
417−
418− openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
419− assert.Equal(t, "test-openai-key", openaiProvider.APIKey)
420− assert.Equal(t, provider.TypeOpenAI, openaiProvider.ProviderType)
421−
422− geminiProvider := cfg.Providers[provider.InferenceProviderGemini]
423− assert.Equal(t, "test-gemini-key", geminiProvider.APIKey)
424− assert.Equal(t, provider.TypeGemini, geminiProvider.ProviderType)
425−
426− xaiProvider := cfg.Providers[provider.InferenceProviderXAI]
427− assert.Equal(t, "test-xai-key", xaiProvider.APIKey)
428− assert.Equal(t, provider.TypeXAI, xaiProvider.ProviderType)
429−
430− openrouterProvider := cfg.Providers[provider.InferenceProviderOpenRouter]
431− assert.Equal(t, "test-openrouter-key", openrouterProvider.APIKey)
432− assert.Equal(t, provider.TypeOpenAI, openrouterProvider.ProviderType)
433− assert.Equal(t, "https://openrouter.ai/api/v1", openrouterProvider.BaseURL)
434−}
435−
436−func TestEnvVars_PartialEnvironmentVariables(t *testing.T) {
437− reset()
438− testConfigDir = t.TempDir()
439− cwdDir := t.TempDir()
440−
441− os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
442− os.Setenv("OPENAI_API_KEY", "test-openai-key")
443−
444− cfg, err := Init(cwdDir, false)
445−
446− require.NoError(t, err)
447− assert.Len(t, cfg.Providers, 2)
448− assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
449− assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
450− assert.NotContains(t, cfg.Providers, provider.InferenceProviderGemini)
451−}
452−
453−func TestEnvVars_VertexAIConfiguration(t *testing.T) {
454− reset()
455− testConfigDir = t.TempDir()
456− cwdDir := t.TempDir()
457−
458− os.Setenv("GOOGLE_GENAI_USE_VERTEXAI", "true")
459− os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
460− os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
461−
462− cfg, err := Init(cwdDir, false)
463−
464− require.NoError(t, err)
465− assert.Contains(t, cfg.Providers, provider.InferenceProviderVertexAI)
466−
467− vertexProvider := cfg.Providers[provider.InferenceProviderVertexAI]
468− assert.Equal(t, provider.TypeVertexAI, vertexProvider.ProviderType)
469− assert.Equal(t, "test-project", vertexProvider.ExtraParams["project"])
470− assert.Equal(t, "us-central1", vertexProvider.ExtraParams["location"])
471−}
472−
473−func TestEnvVars_VertexAIWithoutUseFlag(t *testing.T) {
474− reset()
475− testConfigDir = t.TempDir()
476− cwdDir := t.TempDir()
477−
478− os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
479− os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
480−
481− cfg, err := Init(cwdDir, false)
482−
483− require.NoError(t, err)
484− assert.NotContains(t, cfg.Providers, provider.InferenceProviderVertexAI)
485−}
486−
487−func TestEnvVars_AWSBedrockWithAccessKeys(t *testing.T) {
488− reset()
489− testConfigDir = t.TempDir()
490− cwdDir := t.TempDir()
491−
492− os.Setenv("AWS_ACCESS_KEY_ID", "test-access-key")
493− os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret-key")
494− os.Setenv("AWS_DEFAULT_REGION", "us-east-1")
495−
496− cfg, err := Init(cwdDir, false)
497−
498− require.NoError(t, err)
499− assert.Contains(t, cfg.Providers, provider.InferenceProviderBedrock)
500−
501− bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
502− assert.Equal(t, provider.TypeBedrock, bedrockProvider.ProviderType)
503− assert.Equal(t, "us-east-1", bedrockProvider.ExtraParams["region"])
504−}
505−
506−func TestEnvVars_AWSBedrockWithProfile(t *testing.T) {
507− reset()
508− testConfigDir = t.TempDir()
509− cwdDir := t.TempDir()
510−
511− os.Setenv("AWS_PROFILE", "test-profile")
512− os.Setenv("AWS_REGION", "eu-west-1")
513−
514− cfg, err := Init(cwdDir, false)
515−
516− require.NoError(t, err)
517− assert.Contains(t, cfg.Providers, provider.InferenceProviderBedrock)
518−
519− bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
520− assert.Equal(t, "eu-west-1", bedrockProvider.ExtraParams["region"])
521−}
522−
523−func TestEnvVars_AWSBedrockWithContainerCredentials(t *testing.T) {
524− reset()
525− testConfigDir = t.TempDir()
526− cwdDir := t.TempDir()
527−
528− os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/v2/credentials/test")
529− os.Setenv("AWS_DEFAULT_REGION", "ap-southeast-1")
530−
531− cfg, err := Init(cwdDir, false)
532−
533− require.NoError(t, err)
534− assert.Contains(t, cfg.Providers, provider.InferenceProviderBedrock)
535−}
536−
537−func TestEnvVars_AWSBedrockRegionPriority(t *testing.T) {
538− reset()
539− testConfigDir = t.TempDir()
540− cwdDir := t.TempDir()
541−
542− os.Setenv("AWS_ACCESS_KEY_ID", "test-key")
543− os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
544− os.Setenv("AWS_DEFAULT_REGION", "us-west-2")
545− os.Setenv("AWS_REGION", "us-east-1")
546−
547− cfg, err := Init(cwdDir, false)
548−
549− require.NoError(t, err)
550− bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
551− assert.Equal(t, "us-west-2", bedrockProvider.ExtraParams["region"])
552−}
553−
554−func TestEnvVars_AWSBedrockFallbackRegion(t *testing.T) {
555− reset()
556− testConfigDir = t.TempDir()
557− cwdDir := t.TempDir()
558−
559− os.Setenv("AWS_ACCESS_KEY_ID", "test-key")
560− os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
561− os.Setenv("AWS_REGION", "us-east-1")
562−
563− cfg, err := Init(cwdDir, false)
564−
565− require.NoError(t, err)
566− bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
567− assert.Equal(t, "us-east-1", bedrockProvider.ExtraParams["region"])
568−}
569−
570−func TestEnvVars_NoAWSCredentials(t *testing.T) {
571− reset()
572− testConfigDir = t.TempDir()
573− cwdDir := t.TempDir()
574−
575− cfg, err := Init(cwdDir, false)
576−
577− require.NoError(t, err)
578− assert.NotContains(t, cfg.Providers, provider.InferenceProviderBedrock)
579−}
580−
581−func TestEnvVars_CustomEnvironmentVariables(t *testing.T) {
582− reset()
583− testConfigDir = t.TempDir()
584− cwdDir := t.TempDir()
585−
586− os.Setenv("ANTHROPIC_API_KEY", "resolved-anthropic-key")
587−
588− cfg, err := Init(cwdDir, false)
589−
590− require.NoError(t, err)
591− if len(cfg.Providers) > 0 {
592− if anthropicProvider, exists := cfg.Providers[provider.InferenceProviderAnthropic]; exists {
593− assert.Equal(t, "resolved-anthropic-key", anthropicProvider.APIKey)
594− }
595− }
596−}
597−
598−func TestEnvVars_CombinedEnvironmentVariables(t *testing.T) {
599− reset()
600− testConfigDir = t.TempDir()
601− cwdDir := t.TempDir()
602−
603− os.Setenv("ANTHROPIC_API_KEY", "test-anthropic")
604− os.Setenv("OPENAI_API_KEY", "test-openai")
605− os.Setenv("GOOGLE_GENAI_USE_VERTEXAI", "true")
606− os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
607− os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
608− os.Setenv("AWS_ACCESS_KEY_ID", "test-aws-key")
609− os.Setenv("AWS_SECRET_ACCESS_KEY", "test-aws-secret")
610− os.Setenv("AWS_DEFAULT_REGION", "us-west-1")
611−
612− cfg, err := Init(cwdDir, false)
613−
614− require.NoError(t, err)
615−
616− expectedProviders := []provider.InferenceProvider{
617− provider.InferenceProviderAnthropic,
618− provider.InferenceProviderOpenAI,
619− provider.InferenceProviderVertexAI,
620− provider.InferenceProviderBedrock,
621− }
622−
623− for _, expectedProvider := range expectedProviders {
624− assert.Contains(t, cfg.Providers, expectedProvider)
625− }
626−}
627−
628−func TestHasAWSCredentials_AccessKeys(t *testing.T) {
629− reset()
630−
631− os.Setenv("AWS_ACCESS_KEY_ID", "test-key")
632− os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
633−
634− assert.True(t, hasAWSCredentials())
635−}
636−
637−func TestHasAWSCredentials_Profile(t *testing.T) {
638− reset()
639−
640− os.Setenv("AWS_PROFILE", "test-profile")
641−
642− assert.True(t, hasAWSCredentials())
643−}
644−
645−func TestHasAWSCredentials_DefaultProfile(t *testing.T) {
646− reset()
647−
648− os.Setenv("AWS_DEFAULT_PROFILE", "default")
649−
650− assert.True(t, hasAWSCredentials())
651−}
652−
653−func TestHasAWSCredentials_Region(t *testing.T) {
654− reset()
655−
656− os.Setenv("AWS_REGION", "us-east-1")
657−
658− assert.True(t, hasAWSCredentials())
659−}
660−
661−func TestHasAWSCredentials_ContainerCredentials(t *testing.T) {
662− reset()
663−
664− os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/v2/credentials/test")
665−
666− assert.True(t, hasAWSCredentials())
667−}
668−
669−func TestHasAWSCredentials_NoCredentials(t *testing.T) {
670− reset()
671−
672− assert.False(t, hasAWSCredentials())
673−}
674−
675−func TestProviderMerging_GlobalToBase(t *testing.T) {
676− reset()
677− testConfigDir = t.TempDir()
678− cwdDir := t.TempDir()
679−
680− globalConfig := Config{
681− Providers: map[provider.InferenceProvider]ProviderConfig{
682− provider.InferenceProviderOpenAI: {
683− ID: provider.InferenceProviderOpenAI,
684− APIKey: "global-openai-key",
685− ProviderType: provider.TypeOpenAI,
686− DefaultLargeModel: "gpt-4",
687− DefaultSmallModel: "gpt-3.5-turbo",
688− Models: []Model{
689− {
690− ID: "gpt-4",
691− Name: "GPT-4",
692− ContextWindow: 8192,
693− DefaultMaxTokens: 4096,
694− },
695− {
696− ID: "gpt-3.5-turbo",
697− Name: "GPT-3.5 Turbo",
698− ContextWindow: 4096,
699− DefaultMaxTokens: 2048,
700− },
701− },
702− },
703− },
704− }
705−
706− configPath := filepath.Join(testConfigDir, "crush.json")
707− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
708− data, err := json.Marshal(globalConfig)
709− require.NoError(t, err)
710− require.NoError(t, os.WriteFile(configPath, data, 0o644))
711−
712− cfg, err := Init(cwdDir, false)
713−
714− require.NoError(t, err)
715− assert.Len(t, cfg.Providers, 1)
716−
717− openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
718− assert.Equal(t, "global-openai-key", openaiProvider.APIKey)
719− assert.Equal(t, "gpt-4", openaiProvider.DefaultLargeModel)
720− assert.Equal(t, "gpt-4o", openaiProvider.DefaultSmallModel)
721− assert.GreaterOrEqual(t, len(openaiProvider.Models), 2)
722−}
723−
724−func TestProviderMerging_LocalToBase(t *testing.T) {
725− reset()
726− testConfigDir = t.TempDir()
727− cwdDir := t.TempDir()
728−
729− localConfig := Config{
730− Providers: map[provider.InferenceProvider]ProviderConfig{
731− provider.InferenceProviderAnthropic: {
732− ID: provider.InferenceProviderAnthropic,
733− APIKey: "local-anthropic-key",
734− ProviderType: provider.TypeAnthropic,
735− DefaultLargeModel: "claude-3-opus",
736− DefaultSmallModel: "claude-3-haiku",
737− Models: []Model{
738− {
739− ID: "claude-3-opus",
740− Name: "Claude 3 Opus",
741− ContextWindow: 200000,
742− DefaultMaxTokens: 4096,
743− CostPer1MIn: 15.0,
744− CostPer1MOut: 75.0,
745− },
746− {
747− ID: "claude-3-haiku",
748− Name: "Claude 3 Haiku",
749− ContextWindow: 200000,
750− DefaultMaxTokens: 4096,
751− CostPer1MIn: 0.25,
752− CostPer1MOut: 1.25,
753− },
754− },
755− },
756− },
757− }
758−
759− localConfigPath := filepath.Join(cwdDir, "crush.json")
760− data, err := json.Marshal(localConfig)
761− require.NoError(t, err)
762− require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
763−
764− cfg, err := Init(cwdDir, false)
765−
766− require.NoError(t, err)
767− assert.Len(t, cfg.Providers, 1)
768−
769− anthropicProvider := cfg.Providers[provider.InferenceProviderAnthropic]
770− assert.Equal(t, "local-anthropic-key", anthropicProvider.APIKey)
771− assert.Equal(t, "claude-3-opus", anthropicProvider.DefaultLargeModel)
772− assert.Equal(t, "claude-3-5-haiku-20241022", anthropicProvider.DefaultSmallModel)
773− assert.GreaterOrEqual(t, len(anthropicProvider.Models), 2)
774−}
775−
776−func TestProviderMerging_ConflictingSettings(t *testing.T) {
777− reset()
778− testConfigDir = t.TempDir()
779− cwdDir := t.TempDir()
780−
781− globalConfig := Config{
782− Providers: map[provider.InferenceProvider]ProviderConfig{
783− provider.InferenceProviderOpenAI: {
784− ID: provider.InferenceProviderOpenAI,
785− APIKey: "global-key",
786− ProviderType: provider.TypeOpenAI,
787− DefaultLargeModel: "gpt-4",
788− DefaultSmallModel: "gpt-3.5-turbo",
789− Models: []Model{
790− {
791− ID: "gpt-4",
792− Name: "GPT-4",
793− ContextWindow: 8192,
794− DefaultMaxTokens: 4096,
795− },
796− {
797− ID: "gpt-3.5-turbo",
798− Name: "GPT-3.5 Turbo",
799− ContextWindow: 4096,
800− DefaultMaxTokens: 2048,
801− },
802− {
803− ID: "gpt-4-turbo",
804− Name: "GPT-4 Turbo",
805− ContextWindow: 128000,
806− DefaultMaxTokens: 4096,
807− },
808− },
809− },
810− },
811− }
812−
813− configPath := filepath.Join(testConfigDir, "crush.json")
814− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
815− data, err := json.Marshal(globalConfig)
816− require.NoError(t, err)
817− require.NoError(t, os.WriteFile(configPath, data, 0o644))
818−
819− // Create local config that overrides
820− localConfig := Config{
821− Providers: map[provider.InferenceProvider]ProviderConfig{
822− provider.InferenceProviderOpenAI: {
823− APIKey: "local-key",
824− DefaultLargeModel: "gpt-4-turbo",
825− },
826− },
827− }
828−
829− localConfigPath := filepath.Join(cwdDir, "crush.json")
830− data, err = json.Marshal(localConfig)
831− require.NoError(t, err)
832− require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
833−
834− cfg, err := Init(cwdDir, false)
835−
836− require.NoError(t, err)
837−
838− openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
839− assert.Equal(t, "local-key", openaiProvider.APIKey)
840− assert.Equal(t, "gpt-4-turbo", openaiProvider.DefaultLargeModel)
841− assert.False(t, openaiProvider.Disabled)
842− assert.Equal(t, "gpt-4o", openaiProvider.DefaultSmallModel)
843−}
844−
845−func TestProviderMerging_CustomVsKnownProviders(t *testing.T) {
846− reset()
847− testConfigDir = t.TempDir()
848− cwdDir := t.TempDir()
849−
850− customProviderID := provider.InferenceProvider("custom-provider")
851−
852− globalConfig := Config{
853− Providers: map[provider.InferenceProvider]ProviderConfig{
854− provider.InferenceProviderOpenAI: {
855− ID: provider.InferenceProviderOpenAI,
856− APIKey: "openai-key",
857− BaseURL: "should-not-override",
858− ProviderType: provider.TypeAnthropic,
859− DefaultLargeModel: "gpt-4",
860− DefaultSmallModel: "gpt-3.5-turbo",
861− Models: []Model{
862− {
863− ID: "gpt-4",
864− Name: "GPT-4",
865− ContextWindow: 8192,
866− DefaultMaxTokens: 4096,
867− },
868− {
869− ID: "gpt-3.5-turbo",
870− Name: "GPT-3.5 Turbo",
871− ContextWindow: 4096,
872− DefaultMaxTokens: 2048,
873− },
874− },
875− },
876− customProviderID: {
877− ID: customProviderID,
878− APIKey: "custom-key",
879− BaseURL: "https://custom.api.com",
880− ProviderType: provider.TypeOpenAI,
881− DefaultLargeModel: "custom-large",
882− DefaultSmallModel: "custom-small",
883− Models: []Model{
884− {
885− ID: "custom-large",
886− Name: "Custom Large",
887− ContextWindow: 8192,
888− DefaultMaxTokens: 4096,
889− },
890− {
891− ID: "custom-small",
892− Name: "Custom Small",
893− ContextWindow: 4096,
894− DefaultMaxTokens: 2048,
895− },
896− },
897− },
898− },
899− }
900−
901− localConfig := Config{
902− Providers: map[provider.InferenceProvider]ProviderConfig{
903− provider.InferenceProviderOpenAI: {
904− BaseURL: "https://should-not-change.com",
905− ProviderType: provider.TypeGemini, // Should not change
906− },
907− customProviderID: {
908− BaseURL: "https://updated-custom.api.com",
909− ProviderType: provider.TypeOpenAI,
910− },
911− },
912− }
913−
914− configPath := filepath.Join(testConfigDir, "crush.json")
915− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
916− data, err := json.Marshal(globalConfig)
917− require.NoError(t, err)
918− require.NoError(t, os.WriteFile(configPath, data, 0o644))
919−
920− localConfigPath := filepath.Join(cwdDir, "crush.json")
921− data, err = json.Marshal(localConfig)
922− require.NoError(t, err)
923− require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
924−
925− cfg, err := Init(cwdDir, false)
926−
927− require.NoError(t, err)
928−
929− openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
930− assert.NotEqual(t, "https://should-not-change.com", openaiProvider.BaseURL)
931− assert.NotEqual(t, provider.TypeGemini, openaiProvider.ProviderType)
932−
933− customProvider := cfg.Providers[customProviderID]
934− assert.Equal(t, "custom-key", customProvider.APIKey)
935− assert.Equal(t, "https://updated-custom.api.com", customProvider.BaseURL)
936− assert.Equal(t, provider.TypeOpenAI, customProvider.ProviderType)
937−}
938−
939−func TestProviderValidation_CustomProviderMissingBaseURL(t *testing.T) {
940− reset()
941− testConfigDir = t.TempDir()
942− cwdDir := t.TempDir()
943−
944− customProviderID := provider.InferenceProvider("custom-provider")
945−
946− globalConfig := Config{
947− Providers: map[provider.InferenceProvider]ProviderConfig{
948− customProviderID: {
949− ID: customProviderID,
950− APIKey: "custom-key",
951− ProviderType: provider.TypeOpenAI,
952− },
953− },
954− }
955−
956− configPath := filepath.Join(testConfigDir, "crush.json")
957− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
958− data, err := json.Marshal(globalConfig)
959− require.NoError(t, err)
960− require.NoError(t, os.WriteFile(configPath, data, 0o644))
961−
962− cfg, err := Init(cwdDir, false)
963−
964− require.NoError(t, err)
965− assert.NotContains(t, cfg.Providers, customProviderID)
966−}
967−
968−func TestProviderValidation_CustomProviderMissingAPIKey(t *testing.T) {
969− reset()
970− testConfigDir = t.TempDir()
971− cwdDir := t.TempDir()
972−
973− customProviderID := provider.InferenceProvider("custom-provider")
974−
975− globalConfig := Config{
976− Providers: map[provider.InferenceProvider]ProviderConfig{
977− customProviderID: {
978− ID: customProviderID,
979− BaseURL: "https://custom.api.com",
980− ProviderType: provider.TypeOpenAI,
981− },
982− },
983− }
984−
985− configPath := filepath.Join(testConfigDir, "crush.json")
986− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
987− data, err := json.Marshal(globalConfig)
988− require.NoError(t, err)
989− require.NoError(t, os.WriteFile(configPath, data, 0o644))
990−
991− cfg, err := Init(cwdDir, false)
992−
993− require.NoError(t, err)
994− assert.NotContains(t, cfg.Providers, customProviderID)
995−}
996−
997−func TestProviderValidation_CustomProviderInvalidType(t *testing.T) {
998− reset()
999− testConfigDir = t.TempDir()
1000− cwdDir := t.TempDir()
1001−
1002− customProviderID := provider.InferenceProvider("custom-provider")
1003−
1004− globalConfig := Config{
1005− Providers: map[provider.InferenceProvider]ProviderConfig{
1006− customProviderID: {
1007− ID: customProviderID,
1008− APIKey: "custom-key",
1009− BaseURL: "https://custom.api.com",
1010− ProviderType: provider.Type("invalid-type"),
1011− },
1012− },
1013− }
1014−
1015− configPath := filepath.Join(testConfigDir, "crush.json")
1016− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1017− data, err := json.Marshal(globalConfig)
1018− require.NoError(t, err)
1019− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1020−
1021− cfg, err := Init(cwdDir, false)
1022−
1023− require.NoError(t, err)
1024− assert.NotContains(t, cfg.Providers, customProviderID)
1025−}
1026−
1027−func TestProviderValidation_KnownProviderValid(t *testing.T) {
1028− reset()
1029− testConfigDir = t.TempDir()
1030− cwdDir := t.TempDir()
1031−
1032− globalConfig := Config{
1033− Providers: map[provider.InferenceProvider]ProviderConfig{
1034− provider.InferenceProviderOpenAI: {
1035− ID: provider.InferenceProviderOpenAI,
1036− APIKey: "openai-key",
1037− ProviderType: provider.TypeOpenAI,
1038− DefaultLargeModel: "gpt-4",
1039− DefaultSmallModel: "gpt-3.5-turbo",
1040− Models: []Model{
1041− {
1042− ID: "gpt-4",
1043− Name: "GPT-4",
1044− ContextWindow: 8192,
1045− DefaultMaxTokens: 4096,
1046− },
1047− {
1048− ID: "gpt-3.5-turbo",
1049− Name: "GPT-3.5 Turbo",
1050− ContextWindow: 4096,
1051− DefaultMaxTokens: 2048,
1052− },
1053− },
1054− },
1055− },
1056− }
1057−
1058− configPath := filepath.Join(testConfigDir, "crush.json")
1059− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1060− data, err := json.Marshal(globalConfig)
1061− require.NoError(t, err)
1062− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1063−
1064− cfg, err := Init(cwdDir, false)
1065−
1066− require.NoError(t, err)
1067− assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
1068−}
1069−
1070−func TestProviderValidation_DisabledProvider(t *testing.T) {
1071− reset()
1072− testConfigDir = t.TempDir()
1073− cwdDir := t.TempDir()
1074−
1075− globalConfig := Config{
1076− Providers: map[provider.InferenceProvider]ProviderConfig{
1077− provider.InferenceProviderOpenAI: {
1078− ID: provider.InferenceProviderOpenAI,
1079− APIKey: "openai-key",
1080− ProviderType: provider.TypeOpenAI,
1081− Disabled: true,
1082− DefaultLargeModel: "gpt-4",
1083− DefaultSmallModel: "gpt-3.5-turbo",
1084− Models: []Model{
1085− {
1086− ID: "gpt-4",
1087− Name: "GPT-4",
1088− ContextWindow: 8192,
1089− DefaultMaxTokens: 4096,
1090− },
1091− {
1092− ID: "gpt-3.5-turbo",
1093− Name: "GPT-3.5 Turbo",
1094− ContextWindow: 4096,
1095− DefaultMaxTokens: 2048,
1096− },
1097− },
1098− },
1099− provider.InferenceProviderAnthropic: {
1100− ID: provider.InferenceProviderAnthropic,
1101− APIKey: "anthropic-key",
1102− ProviderType: provider.TypeAnthropic,
1103− Disabled: false, // This one is enabled
1104− DefaultLargeModel: "claude-3-opus",
1105− DefaultSmallModel: "claude-3-haiku",
1106− Models: []Model{
1107− {
1108− ID: "claude-3-opus",
1109− Name: "Claude 3 Opus",
1110− ContextWindow: 200000,
1111− DefaultMaxTokens: 4096,
1112− },
1113− {
1114− ID: "claude-3-haiku",
1115− Name: "Claude 3 Haiku",
1116− ContextWindow: 200000,
1117− DefaultMaxTokens: 4096,
1118− },
1119− },
1120− },
1121− },
1122− }
1123−
1124− configPath := filepath.Join(testConfigDir, "crush.json")
1125− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1126− data, err := json.Marshal(globalConfig)
1127− require.NoError(t, err)
1128− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1129−
1130− cfg, err := Init(cwdDir, false)
1131−
1132− require.NoError(t, err)
1133− assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
1134− assert.True(t, cfg.Providers[provider.InferenceProviderOpenAI].Disabled)
1135− assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
1136− assert.False(t, cfg.Providers[provider.InferenceProviderAnthropic].Disabled)
1137−}
1138−
1139−func TestProviderModels_AddingNewModels(t *testing.T) {
1140− reset()
1141− testConfigDir = t.TempDir()
1142− cwdDir := t.TempDir()
1143−
1144− globalConfig := Config{
1145− Providers: map[provider.InferenceProvider]ProviderConfig{
1146− provider.InferenceProviderOpenAI: {
1147− ID: provider.InferenceProviderOpenAI,
1148− APIKey: "openai-key",
1149− ProviderType: provider.TypeOpenAI,
1150− DefaultLargeModel: "gpt-4",
1151− DefaultSmallModel: "gpt-4-turbo",
1152− Models: []Model{
1153− {
1154− ID: "gpt-4",
1155− Name: "GPT-4",
1156− ContextWindow: 8192,
1157− DefaultMaxTokens: 4096,
1158− },
1159− },
1160− },
1161− },
1162− }
1163−
1164− localConfig := Config{
1165− Providers: map[provider.InferenceProvider]ProviderConfig{
1166− provider.InferenceProviderOpenAI: {
1167− Models: []Model{
1168− {
1169− ID: "gpt-4-turbo",
1170− Name: "GPT-4 Turbo",
1171− ContextWindow: 128000,
1172− DefaultMaxTokens: 4096,
1173− },
1174− },
1175− },
1176− },
1177− }
1178−
1179− configPath := filepath.Join(testConfigDir, "crush.json")
1180− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1181− data, err := json.Marshal(globalConfig)
1182− require.NoError(t, err)
1183− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1184−
1185− localConfigPath := filepath.Join(cwdDir, "crush.json")
1186− data, err = json.Marshal(localConfig)
1187− require.NoError(t, err)
1188− require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1189−
1190− cfg, err := Init(cwdDir, false)
1191−
1192− require.NoError(t, err)
1193−
1194− openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1195− assert.GreaterOrEqual(t, len(openaiProvider.Models), 2)
1196−
1197− modelIDs := make([]string, len(openaiProvider.Models))
1198− for i, model := range openaiProvider.Models {
1199− modelIDs[i] = model.ID
1200− }
1201− assert.Contains(t, modelIDs, "gpt-4")
1202− assert.Contains(t, modelIDs, "gpt-4-turbo")
1203−}
1204−
1205−func TestProviderModels_DuplicateModelHandling(t *testing.T) {
1206− reset()
1207− testConfigDir = t.TempDir()
1208− cwdDir := t.TempDir()
1209−
1210− globalConfig := Config{
1211− Providers: map[provider.InferenceProvider]ProviderConfig{
1212− provider.InferenceProviderOpenAI: {
1213− ID: provider.InferenceProviderOpenAI,
1214− APIKey: "openai-key",
1215− ProviderType: provider.TypeOpenAI,
1216− DefaultLargeModel: "gpt-4",
1217− DefaultSmallModel: "gpt-4",
1218− Models: []Model{
1219− {
1220− ID: "gpt-4",
1221− Name: "GPT-4",
1222− ContextWindow: 8192,
1223− DefaultMaxTokens: 4096,
1224− },
1225− },
1226− },
1227− },
1228− }
1229−
1230− localConfig := Config{
1231− Providers: map[provider.InferenceProvider]ProviderConfig{
1232− provider.InferenceProviderOpenAI: {
1233− Models: []Model{
1234− {
1235− ID: "gpt-4",
1236− Name: "GPT-4 Updated",
1237− ContextWindow: 16384,
1238− DefaultMaxTokens: 8192,
1239− },
1240− },
1241− },
1242− },
1243− }
1244−
1245− configPath := filepath.Join(testConfigDir, "crush.json")
1246− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1247− data, err := json.Marshal(globalConfig)
1248− require.NoError(t, err)
1249− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1250−
1251− localConfigPath := filepath.Join(cwdDir, "crush.json")
1252− data, err = json.Marshal(localConfig)
1253− require.NoError(t, err)
1254− require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1255−
1256− cfg, err := Init(cwdDir, false)
1257−
1258− require.NoError(t, err)
1259−
1260− openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1261− assert.GreaterOrEqual(t, len(openaiProvider.Models), 1)
1262−
1263− // Find the first model that matches our test data
1264− var testModel *Model
1265− for _, model := range openaiProvider.Models {
1266− if model.ID == "gpt-4" {
1267− testModel = &model
1268− break
1269− }
1270− }
1271−
1272− // If gpt-4 not found, use the first available model
1273− if testModel == nil {
1274− testModel = &openaiProvider.Models[0]
1275− }
1276−
1277− assert.NotEmpty(t, testModel.ID)
1278− assert.NotEmpty(t, testModel.Name)
1279− assert.Greater(t, testModel.ContextWindow, int64(0))
1280−}
1281−
1282−func TestProviderModels_ModelCostAndCapabilities(t *testing.T) {
1283− reset()
1284− testConfigDir = t.TempDir()
1285− cwdDir := t.TempDir()
1286−
1287− globalConfig := Config{
1288− Providers: map[provider.InferenceProvider]ProviderConfig{
1289− provider.InferenceProviderOpenAI: {
1290− ID: provider.InferenceProviderOpenAI,
1291− APIKey: "openai-key",
1292− ProviderType: provider.TypeOpenAI,
1293− DefaultLargeModel: "gpt-4",
1294− DefaultSmallModel: "gpt-4",
1295− Models: []Model{
1296− {
1297− ID: "gpt-4",
1298− Name: "GPT-4",
1299− CostPer1MIn: 30.0,
1300− CostPer1MOut: 60.0,
1301− CostPer1MInCached: 15.0,
1302− CostPer1MOutCached: 30.0,
1303− ContextWindow: 8192,
1304− DefaultMaxTokens: 4096,
1305− CanReason: true,
1306− ReasoningEffort: "medium",
1307− SupportsImages: true,
1308− },
1309− },
1310− },
1311− },
1312− }
1313−
1314− configPath := filepath.Join(testConfigDir, "crush.json")
1315− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1316− data, err := json.Marshal(globalConfig)
1317− require.NoError(t, err)
1318− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1319−
1320− cfg, err := Init(cwdDir, false)
1321−
1322− require.NoError(t, err)
1323−
1324− openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1325− require.GreaterOrEqual(t, len(openaiProvider.Models), 1)
1326−
1327− // Find the test model or use the first one
1328− var testModel *Model
1329− for _, model := range openaiProvider.Models {
1330− if model.ID == "gpt-4" {
1331− testModel = &model
1332− break
1333− }
1334− }
1335−
1336− if testModel == nil {
1337− testModel = &openaiProvider.Models[0]
1338− }
1339−
1340− // Only test the custom properties if this is actually our test model
1341− if testModel.ID == "gpt-4" {
1342− assert.Equal(t, 30.0, testModel.CostPer1MIn)
1343− assert.Equal(t, 60.0, testModel.CostPer1MOut)
1344− assert.Equal(t, 15.0, testModel.CostPer1MInCached)
1345− assert.Equal(t, 30.0, testModel.CostPer1MOutCached)
1346− assert.True(t, testModel.CanReason)
1347− assert.Equal(t, "medium", testModel.ReasoningEffort)
1348− assert.True(t, testModel.SupportsImages)
1349− }
1350−}
1351−
1352−func TestDefaultAgents_CoderAgent(t *testing.T) {
1353− reset()
1354− testConfigDir = t.TempDir()
1355− cwdDir := t.TempDir()
1356−
1357− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1358−
1359− cfg, err := Init(cwdDir, false)
1360−
1361− require.NoError(t, err)
1362− assert.Contains(t, cfg.Agents, AgentCoder)
1363−
1364− coderAgent := cfg.Agents[AgentCoder]
1365− assert.Equal(t, AgentCoder, coderAgent.ID)
1366− assert.Equal(t, "Coder", coderAgent.Name)
1367− assert.Equal(t, "An agent that helps with executing coding tasks.", coderAgent.Description)
1368− assert.Equal(t, LargeModel, coderAgent.Model)
1369− assert.False(t, coderAgent.Disabled)
1370− assert.Equal(t, cfg.Options.ContextPaths, coderAgent.ContextPaths)
1371− assert.Nil(t, coderAgent.AllowedTools)
1372−}
1373−
1374−func TestDefaultAgents_TaskAgent(t *testing.T) {
1375− reset()
1376− testConfigDir = t.TempDir()
1377− cwdDir := t.TempDir()
1378−
1379− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1380−
1381− cfg, err := Init(cwdDir, false)
1382−
1383− require.NoError(t, err)
1384− assert.Contains(t, cfg.Agents, AgentTask)
1385−
1386− taskAgent := cfg.Agents[AgentTask]
1387− assert.Equal(t, AgentTask, taskAgent.ID)
1388− assert.Equal(t, "Task", taskAgent.Name)
1389− assert.Equal(t, "An agent that helps with searching for context and finding implementation details.", taskAgent.Description)
1390− assert.Equal(t, LargeModel, taskAgent.Model)
1391− assert.False(t, taskAgent.Disabled)
1392− assert.Equal(t, cfg.Options.ContextPaths, taskAgent.ContextPaths)
1393−
1394− expectedTools := []string{"glob", "grep", "ls", "sourcegraph", "view"}
1395− assert.Equal(t, expectedTools, taskAgent.AllowedTools)
1396−
1397− assert.Equal(t, map[string][]string{}, taskAgent.AllowedMCP)
1398− assert.Equal(t, []string{}, taskAgent.AllowedLSP)
1399−}
1400−
1401−func TestAgentMerging_CustomAgent(t *testing.T) {
1402− reset()
1403− testConfigDir = t.TempDir()
1404− cwdDir := t.TempDir()
1405−
1406− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1407−
1408− globalConfig := Config{
1409− Agents: map[AgentID]Agent{
1410− AgentID("custom-agent"): {
1411− ID: AgentID("custom-agent"),
1412− Name: "Custom Agent",
1413− Description: "A custom agent for testing",
1414− Model: SmallModel,
1415− AllowedTools: []string{"glob", "grep"},
1416− AllowedMCP: map[string][]string{"mcp1": {"tool1", "tool2"}},
1417− AllowedLSP: []string{"typescript", "go"},
1418− ContextPaths: []string{"custom-context.md"},
1419− },
1420− },
1421− MCP: map[string]MCP{
1422− "mcp1": {
1423− Type: MCPStdio,
1424− Command: "test-mcp-command",
1425− Args: []string{"--test"},
1426− },
1427− },
1428− LSP: map[string]LSPConfig{
1429− "typescript": {
1430− Command: "typescript-language-server",
1431− Args: []string{"--stdio"},
1432− },
1433− "go": {
1434− Command: "gopls",
1435− Args: []string{},
1436− },
1437− },
1438− }
1439−
1440− configPath := filepath.Join(testConfigDir, "crush.json")
1441− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1442− data, err := json.Marshal(globalConfig)
1443− require.NoError(t, err)
1444− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1445−
1446− cfg, err := Init(cwdDir, false)
1447−
1448− require.NoError(t, err)
1449−
1450− assert.Contains(t, cfg.Agents, AgentCoder)
1451− assert.Contains(t, cfg.Agents, AgentTask)
1452− assert.Contains(t, cfg.Agents, AgentID("custom-agent"))
1453−
1454− customAgent := cfg.Agents[AgentID("custom-agent")]
1455− assert.Equal(t, "Custom Agent", customAgent.Name)
1456− assert.Equal(t, "A custom agent for testing", customAgent.Description)
1457− assert.Equal(t, SmallModel, customAgent.Model)
1458− assert.Equal(t, []string{"glob", "grep"}, customAgent.AllowedTools)
1459− assert.Equal(t, map[string][]string{"mcp1": {"tool1", "tool2"}}, customAgent.AllowedMCP)
1460− assert.Equal(t, []string{"typescript", "go"}, customAgent.AllowedLSP)
1461− expectedContextPaths := append(defaultContextPaths, "custom-context.md")
1462− assert.Equal(t, expectedContextPaths, customAgent.ContextPaths)
1463−}
1464−
1465−func TestAgentMerging_ModifyDefaultCoderAgent(t *testing.T) {
1466− reset()
1467− testConfigDir = t.TempDir()
1468− cwdDir := t.TempDir()
1469−
1470− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1471−
1472− globalConfig := Config{
1473− Agents: map[AgentID]Agent{
1474− AgentCoder: {
1475− Model: SmallModel,
1476− AllowedMCP: map[string][]string{"mcp1": {"tool1"}},
1477− AllowedLSP: []string{"typescript"},
1478− ContextPaths: []string{"coder-specific.md"},
1479− },
1480− },
1481− MCP: map[string]MCP{
1482− "mcp1": {
1483− Type: MCPStdio,
1484− Command: "test-mcp-command",
1485− Args: []string{"--test"},
1486− },
1487− },
1488− LSP: map[string]LSPConfig{
1489− "typescript": {
1490− Command: "typescript-language-server",
1491− Args: []string{"--stdio"},
1492− },
1493− },
1494− }
1495−
1496− configPath := filepath.Join(testConfigDir, "crush.json")
1497− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1498− data, err := json.Marshal(globalConfig)
1499− require.NoError(t, err)
1500− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1501−
1502− cfg, err := Init(cwdDir, false)
1503−
1504− require.NoError(t, err)
1505−
1506− coderAgent := cfg.Agents[AgentCoder]
1507− assert.Equal(t, AgentCoder, coderAgent.ID)
1508− assert.Equal(t, "Coder", coderAgent.Name)
1509− assert.Equal(t, "An agent that helps with executing coding tasks.", coderAgent.Description)
1510−
1511− expectedContextPaths := append(cfg.Options.ContextPaths, "coder-specific.md")
1512− assert.Equal(t, expectedContextPaths, coderAgent.ContextPaths)
1513−
1514− assert.Equal(t, SmallModel, coderAgent.Model)
1515− assert.Equal(t, map[string][]string{"mcp1": {"tool1"}}, coderAgent.AllowedMCP)
1516− assert.Equal(t, []string{"typescript"}, coderAgent.AllowedLSP)
1517−}
1518−
1519−func TestAgentMerging_ModifyDefaultTaskAgent(t *testing.T) {
1520− reset()
1521− testConfigDir = t.TempDir()
1522− cwdDir := t.TempDir()
1523−
1524− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1525−
1526− globalConfig := Config{
1527− Agents: map[AgentID]Agent{
1528− AgentTask: {
1529− Model: SmallModel,
1530− AllowedMCP: map[string][]string{"search-mcp": nil},
1531− AllowedLSP: []string{"python"},
1532− Name: "Search Agent",
1533− Description: "Custom search agent",
1534− Disabled: true,
1535− AllowedTools: []string{"glob", "grep", "view"},
1536− },
1537− },
1538− MCP: map[string]MCP{
1539− "search-mcp": {
1540− Type: MCPStdio,
1541− Command: "search-mcp-command",
1542− Args: []string{"--search"},
1543− },
1544− },
1545− LSP: map[string]LSPConfig{
1546− "python": {
1547− Command: "pylsp",
1548− Args: []string{},
1549− },
1550− },
1551− }
1552−
1553− configPath := filepath.Join(testConfigDir, "crush.json")
1554− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1555− data, err := json.Marshal(globalConfig)
1556− require.NoError(t, err)
1557− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1558−
1559− cfg, err := Init(cwdDir, false)
1560−
1561− require.NoError(t, err)
1562−
1563− taskAgent := cfg.Agents[AgentTask]
1564− assert.Equal(t, "Task", taskAgent.Name)
1565− assert.Equal(t, "An agent that helps with searching for context and finding implementation details.", taskAgent.Description)
1566− assert.False(t, taskAgent.Disabled)
1567− assert.Equal(t, []string{"glob", "grep", "ls", "sourcegraph", "view"}, taskAgent.AllowedTools)
1568−
1569− assert.Equal(t, SmallModel, taskAgent.Model)
1570− assert.Equal(t, map[string][]string{"search-mcp": nil}, taskAgent.AllowedMCP)
1571− assert.Equal(t, []string{"python"}, taskAgent.AllowedLSP)
1572−}
1573−
1574−func TestAgentMerging_LocalOverridesGlobal(t *testing.T) {
1575− reset()
1576− testConfigDir = t.TempDir()
1577− cwdDir := t.TempDir()
1578−
1579− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1580−
1581− globalConfig := Config{
1582− Agents: map[AgentID]Agent{
1583− AgentID("test-agent"): {
1584− ID: AgentID("test-agent"),
1585− Name: "Global Agent",
1586− Description: "Global description",
1587− Model: LargeModel,
1588− AllowedTools: []string{"glob"},
1589− },
1590− },
1591− }
1592−
1593− configPath := filepath.Join(testConfigDir, "crush.json")
1594− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1595− data, err := json.Marshal(globalConfig)
1596− require.NoError(t, err)
1597− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1598−
1599− // Create local config that overrides
1600− localConfig := Config{
1601− Agents: map[AgentID]Agent{
1602− AgentID("test-agent"): {
1603− Name: "Local Agent",
1604− Description: "Local description",
1605− Model: SmallModel,
1606− Disabled: true,
1607− AllowedTools: []string{"grep", "view"},
1608− AllowedMCP: map[string][]string{"local-mcp": {"tool1"}},
1609− },
1610− },
1611− MCP: map[string]MCP{
1612− "local-mcp": {
1613− Type: MCPStdio,
1614− Command: "local-mcp-command",
1615− Args: []string{"--local"},
1616− },
1617− },
1618− }
1619−
1620− localConfigPath := filepath.Join(cwdDir, "crush.json")
1621− data, err = json.Marshal(localConfig)
1622− require.NoError(t, err)
1623− require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1624−
1625− cfg, err := Init(cwdDir, false)
1626−
1627− require.NoError(t, err)
1628−
1629− testAgent := cfg.Agents[AgentID("test-agent")]
1630− assert.Equal(t, "Local Agent", testAgent.Name)
1631− assert.Equal(t, "Local description", testAgent.Description)
1632− assert.Equal(t, SmallModel, testAgent.Model)
1633− assert.True(t, testAgent.Disabled)
1634− assert.Equal(t, []string{"grep", "view"}, testAgent.AllowedTools)
1635− assert.Equal(t, map[string][]string{"local-mcp": {"tool1"}}, testAgent.AllowedMCP)
1636−}
1637−
1638−func TestAgentModelTypeAssignment(t *testing.T) {
1639− reset()
1640− testConfigDir = t.TempDir()
1641− cwdDir := t.TempDir()
1642−
1643− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1644−
1645− globalConfig := Config{
1646− Agents: map[AgentID]Agent{
1647− AgentID("large-agent"): {
1648− ID: AgentID("large-agent"),
1649− Name: "Large Model Agent",
1650− Model: LargeModel,
1651− },
1652− AgentID("small-agent"): {
1653− ID: AgentID("small-agent"),
1654− Name: "Small Model Agent",
1655− Model: SmallModel,
1656− },
1657− AgentID("default-agent"): {
1658− ID: AgentID("default-agent"),
1659− Name: "Default Model Agent",
1660− },
1661− },
1662− }
1663−
1664− configPath := filepath.Join(testConfigDir, "crush.json")
1665− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1666− data, err := json.Marshal(globalConfig)
1667− require.NoError(t, err)
1668− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1669−
1670− cfg, err := Init(cwdDir, false)
1671−
1672− require.NoError(t, err)
1673−
1674− assert.Equal(t, LargeModel, cfg.Agents[AgentID("large-agent")].Model)
1675− assert.Equal(t, SmallModel, cfg.Agents[AgentID("small-agent")].Model)
1676− assert.Equal(t, LargeModel, cfg.Agents[AgentID("default-agent")].Model)
1677−}
1678−
1679−func TestAgentContextPathOverrides(t *testing.T) {
1680− reset()
1681− testConfigDir = t.TempDir()
1682− cwdDir := t.TempDir()
1683−
1684− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1685−
1686− globalConfig := Config{
1687− Options: Options{
1688− ContextPaths: []string{"global-context.md", "shared-context.md"},
1689− },
1690− Agents: map[AgentID]Agent{
1691− AgentID("custom-context-agent"): {
1692− ID: AgentID("custom-context-agent"),
1693− Name: "Custom Context Agent",
1694− ContextPaths: []string{"agent-specific.md", "custom.md"},
1695− },
1696− AgentID("default-context-agent"): {
1697− ID: AgentID("default-context-agent"),
1698− Name: "Default Context Agent",
1699− },
1700− },
1701− }
1702−
1703− configPath := filepath.Join(testConfigDir, "crush.json")
1704− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1705− data, err := json.Marshal(globalConfig)
1706− require.NoError(t, err)
1707− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1708−
1709− cfg, err := Init(cwdDir, false)
1710−
1711− require.NoError(t, err)
1712−
1713− customAgent := cfg.Agents[AgentID("custom-context-agent")]
1714− expectedCustomPaths := append(defaultContextPaths, "global-context.md", "shared-context.md", "agent-specific.md", "custom.md")
1715− assert.Equal(t, expectedCustomPaths, customAgent.ContextPaths)
1716−
1717− defaultAgent := cfg.Agents[AgentID("default-context-agent")]
1718− expectedContextPaths := append(defaultContextPaths, "global-context.md", "shared-context.md")
1719− assert.Equal(t, expectedContextPaths, defaultAgent.ContextPaths)
1720−
1721− coderAgent := cfg.Agents[AgentCoder]
1722− assert.Equal(t, expectedContextPaths, coderAgent.ContextPaths)
1723−}
1724−
1725−func TestOptionsMerging_ContextPaths(t *testing.T) {
1726− reset()
1727− testConfigDir = t.TempDir()
1728− cwdDir := t.TempDir()
1729−
1730− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1731−
1732− globalConfig := Config{
1733− Options: Options{
1734− ContextPaths: []string{"global1.md", "global2.md"},
1735− },
1736− }
1737−
1738− configPath := filepath.Join(testConfigDir, "crush.json")
1739− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1740− data, err := json.Marshal(globalConfig)
1741− require.NoError(t, err)
1742− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1743−
1744− localConfig := Config{
1745− Options: Options{
1746− ContextPaths: []string{"local1.md", "local2.md"},
1747− },
1748− }
1749−
1750− localConfigPath := filepath.Join(cwdDir, "crush.json")
1751− data, err = json.Marshal(localConfig)
1752− require.NoError(t, err)
1753− require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1754−
1755− cfg, err := Init(cwdDir, false)
1756−
1757− require.NoError(t, err)
1758−
1759− expectedContextPaths := append(defaultContextPaths, "global1.md", "global2.md", "local1.md", "local2.md")
1760− assert.Equal(t, expectedContextPaths, cfg.Options.ContextPaths)
1761−}
1762−
1763−func TestOptionsMerging_TUIOptions(t *testing.T) {
1764− reset()
1765− testConfigDir = t.TempDir()
1766− cwdDir := t.TempDir()
1767−
1768− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1769−
1770− globalConfig := Config{
1771− Options: Options{
1772− TUI: TUIOptions{
1773− CompactMode: false,
1774− },
1775− },
1776− }
1777−
1778− configPath := filepath.Join(testConfigDir, "crush.json")
1779− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1780− data, err := json.Marshal(globalConfig)
1781− require.NoError(t, err)
1782− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1783−
1784− localConfig := Config{
1785− Options: Options{
1786− TUI: TUIOptions{
1787− CompactMode: true,
1788− },
1789− },
1790− }
1791−
1792− localConfigPath := filepath.Join(cwdDir, "crush.json")
1793− data, err = json.Marshal(localConfig)
1794− require.NoError(t, err)
1795− require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1796−
1797− cfg, err := Init(cwdDir, false)
1798−
1799− require.NoError(t, err)
1800−
1801− assert.True(t, cfg.Options.TUI.CompactMode)
1802−}
1803−
1804−func TestOptionsMerging_DebugFlags(t *testing.T) {
1805− reset()
1806− testConfigDir = t.TempDir()
1807− cwdDir := t.TempDir()
1808−
1809− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1810−
1811− globalConfig := Config{
1812− Options: Options{
1813− Debug: false,
1814− DebugLSP: false,
1815− DisableAutoSummarize: false,
1816− },
1817− }
1818−
1819− configPath := filepath.Join(testConfigDir, "crush.json")
1820− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1821− data, err := json.Marshal(globalConfig)
1822− require.NoError(t, err)
1823− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1824−
1825− localConfig := Config{
1826− Options: Options{
1827− DebugLSP: true,
1828− DisableAutoSummarize: true,
1829− },
1830− }
1831−
1832− localConfigPath := filepath.Join(cwdDir, "crush.json")
1833− data, err = json.Marshal(localConfig)
1834− require.NoError(t, err)
1835− require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1836−
1837− cfg, err := Init(cwdDir, false)
1838−
1839− require.NoError(t, err)
1840−
1841− assert.False(t, cfg.Options.Debug)
1842− assert.True(t, cfg.Options.DebugLSP)
1843− assert.True(t, cfg.Options.DisableAutoSummarize)
1844−}
1845−
1846−func TestOptionsMerging_DataDirectory(t *testing.T) {
1847− reset()
1848− testConfigDir = t.TempDir()
1849− cwdDir := t.TempDir()
1850−
1851− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1852−
1853− globalConfig := Config{
1854− Options: Options{
1855− DataDirectory: "global-data",
1856− },
1857− }
1858−
1859− configPath := filepath.Join(testConfigDir, "crush.json")
1860− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1861− data, err := json.Marshal(globalConfig)
1862− require.NoError(t, err)
1863− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1864−
1865− localConfig := Config{
1866− Options: Options{
1867− DataDirectory: "local-data",
1868− },
1869− }
1870−
1871− localConfigPath := filepath.Join(cwdDir, "crush.json")
1872− data, err = json.Marshal(localConfig)
1873− require.NoError(t, err)
1874− require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1875−
1876− cfg, err := Init(cwdDir, false)
1877−
1878− require.NoError(t, err)
1879−
1880− assert.Equal(t, "local-data", cfg.Options.DataDirectory)
1881−}
1882−
1883−func TestOptionsMerging_DefaultValues(t *testing.T) {
1884− reset()
1885− testConfigDir = t.TempDir()
1886− cwdDir := t.TempDir()
1887−
1888− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1889−
1890− cfg, err := Init(cwdDir, false)
1891−
1892− require.NoError(t, err)
1893−
1894− assert.Equal(t, defaultDataDirectory, cfg.Options.DataDirectory)
1895− assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
1896− assert.False(t, cfg.Options.TUI.CompactMode)
1897− assert.False(t, cfg.Options.Debug)
1898− assert.False(t, cfg.Options.DebugLSP)
1899− assert.False(t, cfg.Options.DisableAutoSummarize)
1900−}
1901−
1902−func TestOptionsMerging_DebugFlagFromInit(t *testing.T) {
1903− reset()
1904− testConfigDir = t.TempDir()
1905− cwdDir := t.TempDir()
1906−
1907− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1908−
1909− globalConfig := Config{
1910− Options: Options{
1911− Debug: false,
1912− },
1913− }
1914−
1915− configPath := filepath.Join(testConfigDir, "crush.json")
1916− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1917− data, err := json.Marshal(globalConfig)
1918− require.NoError(t, err)
1919− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1920−
1921− cfg, err := Init(cwdDir, true)
1922−
1923− require.NoError(t, err)
1924−
1925− // Debug flag from Init should take precedence
1926− assert.True(t, cfg.Options.Debug)
1927−}
1928−
1929−func TestOptionsMerging_ComplexScenario(t *testing.T) {
1930− reset()
1931− testConfigDir = t.TempDir()
1932− cwdDir := t.TempDir()
1933−
1934− // Set up a provider
1935− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1936−
1937− // Create global config with various options
1938− globalConfig := Config{
1939− Options: Options{
1940− ContextPaths: []string{"global-context.md"},
1941− DataDirectory: "global-data",
1942− Debug: false,
1943− DebugLSP: false,
1944− DisableAutoSummarize: false,
1945− TUI: TUIOptions{
1946− CompactMode: false,
1947− },
1948− },
1949− }
1950−
1951− configPath := filepath.Join(testConfigDir, "crush.json")
1952− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1953− data, err := json.Marshal(globalConfig)
1954− require.NoError(t, err)
1955− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1956−
1957− // Create local config that partially overrides
1958− localConfig := Config{
1959− Options: Options{
1960− ContextPaths: []string{"local-context.md"},
1961− DebugLSP: true, // Override
1962− DisableAutoSummarize: true, // Override
1963− TUI: TUIOptions{
1964− CompactMode: true, // Override
1965− },
1966− // DataDirectory and Debug not specified - should keep global values
1967− },
1968− }
1969−
1970− localConfigPath := filepath.Join(cwdDir, "crush.json")
1971− data, err = json.Marshal(localConfig)
1972− require.NoError(t, err)
1973− require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1974−
1975− cfg, err := Init(cwdDir, false)
1976−
1977− require.NoError(t, err)
1978−
1979− // Check merged results
1980− expectedContextPaths := append(defaultContextPaths, "global-context.md", "local-context.md")
1981− assert.Equal(t, expectedContextPaths, cfg.Options.ContextPaths)
1982− assert.Equal(t, "global-data", cfg.Options.DataDirectory) // From global
1983− assert.False(t, cfg.Options.Debug) // From global
1984− assert.True(t, cfg.Options.DebugLSP) // From local
1985− assert.True(t, cfg.Options.DisableAutoSummarize) // From local
1986− assert.True(t, cfg.Options.TUI.CompactMode) // From local
1987−}
1988−
1989−// Model Selection Tests
1990−
1991−func TestModelSelection_PreferredModelSelection(t *testing.T) {
1992− reset()
1993− testConfigDir = t.TempDir()
1994− cwdDir := t.TempDir()
1995−
1996− // Set up multiple providers to test selection logic
1997− os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
1998− os.Setenv("OPENAI_API_KEY", "test-openai-key")
1999−
2000− cfg, err := Init(cwdDir, false)
2001−
2002− require.NoError(t, err)
2003− require.Len(t, cfg.Providers, 2)
2004−
2005− // Should have preferred models set
2006− assert.NotEmpty(t, cfg.Models.Large.ModelID)
2007− assert.NotEmpty(t, cfg.Models.Large.Provider)
2008− assert.NotEmpty(t, cfg.Models.Small.ModelID)
2009− assert.NotEmpty(t, cfg.Models.Small.Provider)
2010−
2011− // Both should use the same provider (first available)
2012− assert.Equal(t, cfg.Models.Large.Provider, cfg.Models.Small.Provider)
2013−}
2014−
2015−func TestValidation_InvalidModelReference(t *testing.T) {
2016− reset()
2017− testConfigDir = t.TempDir()
2018− cwdDir := t.TempDir()
2019−
2020− globalConfig := Config{
2021− Providers: map[provider.InferenceProvider]ProviderConfig{
2022− provider.InferenceProviderOpenAI: {
2023− ID: provider.InferenceProviderOpenAI,
2024− APIKey: "test-key",
2025− ProviderType: provider.TypeOpenAI,
2026− DefaultLargeModel: "non-existent-model",
2027− DefaultSmallModel: "gpt-3.5-turbo",
2028− Models: []Model{
2029− {
2030− ID: "gpt-3.5-turbo",
2031− Name: "GPT-3.5 Turbo",
2032− ContextWindow: 4096,
2033− DefaultMaxTokens: 2048,
2034− },
2035− },
2036− },
2037− },
2038− }
2039−
2040− configPath := filepath.Join(testConfigDir, "crush.json")
2041− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
2042− data, err := json.Marshal(globalConfig)
2043− require.NoError(t, err)
2044− require.NoError(t, os.WriteFile(configPath, data, 0o644))
2045−
2046− _, err = Init(cwdDir, false)
2047− assert.Error(t, err)
2048−}
2049−
2050−func TestValidation_InvalidAgentModelType(t *testing.T) {
2051− reset()
2052− testConfigDir = t.TempDir()
2053− cwdDir := t.TempDir()
2054−
2055− os.Setenv("ANTHROPIC_API_KEY", "test-key")
2056−
2057− globalConfig := Config{
2058− Agents: map[AgentID]Agent{
2059− AgentID("invalid-agent"): {
2060− ID: AgentID("invalid-agent"),
2061− Name: "Invalid Agent",
2062− Model: ModelType("invalid"),
2063− },
2064− },
2065− }
2066−
2067− configPath := filepath.Join(testConfigDir, "crush.json")
2068− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
2069− data, err := json.Marshal(globalConfig)
2070− require.NoError(t, err)
2071− require.NoError(t, os.WriteFile(configPath, data, 0o644))
2072−
2073− _, err = Init(cwdDir, false)
2074− assert.Error(t, err)
2075−}
internal/config/fs.godeleted−71
@@ -1,71 +0,0 @@
1−package config
2−
3−import (
4− "fmt"
5− "os"
6− "path/filepath"
7− "runtime"
8−)
9−
10−var testConfigDir string
11−
12−func baseConfigPath() string {
13− if testConfigDir != "" {
14− return testConfigDir
15− }
16−
17− xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
18− if xdgConfigHome != "" {
19− return filepath.Join(xdgConfigHome, "crush")
20− }
21−
22− // return the path to the main config directory
23− // for windows, it should be in `%LOCALAPPDATA%/crush/`
24− // for linux and macOS, it should be in `$HOME/.config/crush/`
25− if runtime.GOOS == "windows" {
26− localAppData := os.Getenv("LOCALAPPDATA")
27− if localAppData == "" {
28− localAppData = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local")
29− }
30− return filepath.Join(localAppData, appName)
31− }
32−
33− return filepath.Join(os.Getenv("HOME"), ".config", appName)
34−}
35−
36−func baseDataPath() string {
37− if testConfigDir != "" {
38− return testConfigDir
39− }
40−
41− xdgDataHome := os.Getenv("XDG_DATA_HOME")
42− if xdgDataHome != "" {
43− return filepath.Join(xdgDataHome, appName)
44− }
45−
46− // return the path to the main data directory
47− // for windows, it should be in `%LOCALAPPDATA%/crush/`
48− // for linux and macOS, it should be in `$HOME/.local/share/crush/`
49− if runtime.GOOS == "windows" {
50− localAppData := os.Getenv("LOCALAPPDATA")
51− if localAppData == "" {
52− localAppData = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local")
53− }
54− return filepath.Join(localAppData, appName)
55− }
56−
57− return filepath.Join(os.Getenv("HOME"), ".local", "share", appName)
58−}
59−
60−func ConfigPath() string {
61− return filepath.Join(baseConfigPath(), fmt.Sprintf("%s.json", appName))
62−}
63−
64−func CrushInitialized() bool {
65− cfgPath := ConfigPath()
66− if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
67− // config file does not exist, so Crush is not initialized
68− return false
69− }
70− return true
71−}
internal/config/init.go+32−12
@@ -5,27 +5,49 @@ import (
55 "os"
66 "path/filepath"
77 "strings"
8+ "sync"
9+ "sync/atomic"
810)
911
1012const (
11− // InitFlagFilename is the name of the file that indicates whether the project has been initialized
1213 InitFlagFilename = "init"
1314)
1415
15−// ProjectInitFlag represents the initialization status for a project directory
1616type ProjectInitFlag struct {
1717 Initialized bool `json:"initialized"`
1818}
1919
20−// ProjectNeedsInitialization checks if the current project needs initialization
20+// TODO: we need to remove the global config instance keeping it now just until everything is migrated
21+var (
22+ instance atomic.Pointer[Config]
23+ cwd string
24+ once sync.Once // Ensures the initialization happens only once
25+)
26+
27+func Init(workingDir string, debug bool) (*Config, error) {
28+ var err error
29+ once.Do(func() {
30+ cwd = workingDir
31+ var cfg *Config
32+ cfg, err = Load(cwd, debug)
33+ instance.Store(cfg)
34+ })
35+
36+ return instance.Load(), err
37+}
38+
39+func Get() *Config {
40+ return instance.Load()
41+}
42+
2143func ProjectNeedsInitialization() (bool, error) {
22− if instance == nil {
44+ cfg := Get()
45+ if cfg == nil {
2346 return false, fmt.Errorf("config not loaded")
2447 }
2548
26− flagFilePath := filepath.Join(instance.Options.DataDirectory, InitFlagFilename)
49+ flagFilePath := filepath.Join(cfg.Options.DataDirectory, InitFlagFilename)
2750
28− // Check if the flag file exists
2951 _, err := os.Stat(flagFilePath)
3052 if err == nil {
3153 return false, nil
@@ -35,8 +57,7 @@ func ProjectNeedsInitialization() (bool, error) {
3557 return false, fmt.Errorf("failed to check init flag file: %w", err)
3658 }
3759
38− // Check if any variation of CRUSH.md already exists in working directory
39− crushExists, err := crushMdExists(WorkingDirectory())
60+ crushExists, err := crushMdExists(cfg.WorkingDir())
4061 if err != nil {
4162 return false, fmt.Errorf("failed to check for CRUSH.md files: %w", err)
4263 }
@@ -47,7 +68,6 @@ func ProjectNeedsInitialization() (bool, error) {
4768 return true, nil
4869}
4970
50−// crushMdExists checks if any case variation of crush.md exists in the directory
5171func crushMdExists(dir string) (bool, error) {
5272 entries, err := os.ReadDir(dir)
5373 if err != nil {
@@ -68,12 +88,12 @@ func crushMdExists(dir string) (bool, error) {
6888 return false, nil
6989}
7090
71−// MarkProjectInitialized marks the current project as initialized
7291func MarkProjectInitialized() error {
73− if instance == nil {
92+ cfg := Get()
93+ if cfg == nil {
7494 return fmt.Errorf("config not loaded")
7595 }
76− flagFilePath := filepath.Join(instance.Options.DataDirectory, InitFlagFilename)
96+ flagFilePath := filepath.Join(cfg.Options.DataDirectory, InitFlagFilename)
7797
7898 file, err := os.Create(flagFilePath)
7999 if err != nil {
internal/config/load.gonew file+539
@@ -0,0 +1,539 @@
1+package config
2+
3+import (
4+ "encoding/json"
5+ "fmt"
6+ "io"
7+ "os"
8+ "path/filepath"
9+ "runtime"
10+ "slices"
11+ "strings"
12+
13+ "github.com/charmbracelet/crush/internal/env"
14+ "github.com/charmbracelet/crush/internal/fur/client"
15+ "github.com/charmbracelet/crush/internal/fur/provider"
16+ "github.com/charmbracelet/crush/internal/log"
17+ "golang.org/x/exp/slog"
18+)
19+
20+// LoadReader config via io.Reader.
21+func LoadReader(fd io.Reader) (*Config, error) {
22+ data, err := io.ReadAll(fd)
23+ if err != nil {
24+ return nil, err
25+ }
26+
27+ var config Config
28+ err = json.Unmarshal(data, &config)
29+ if err != nil {
30+ return nil, err
31+ }
32+ return &config, err
33+}
34+
35+// Load loads the configuration from the default paths.
36+func Load(workingDir string, debug bool) (*Config, error) {
37+ // uses default config paths
38+ configPaths := []string{
39+ globalConfig(),
40+ globalConfigData(),
41+ filepath.Join(workingDir, fmt.Sprintf("%s.json", appName)),
42+ filepath.Join(workingDir, fmt.Sprintf(".%s.json", appName)),
43+ }
44+ cfg, err := loadFromConfigPaths(configPaths)
45+ if err != nil {
46+ return nil, fmt.Errorf("failed to load config from paths %v: %w", configPaths, err)
47+ }
48+
49+ cfg.setDefaults(workingDir)
50+
51+ if debug {
52+ cfg.Options.Debug = true
53+ }
54+
55+ // Init logs
56+ log.Init(
57+ filepath.Join(cfg.Options.DataDirectory, "logs", fmt.Sprintf("%s.log", appName)),
58+ cfg.Options.Debug,
59+ )
60+
61+ if err != nil {
62+ return nil, fmt.Errorf("failed to load config: %w", err)
63+ }
64+
65+ // Load known providers, this loads the config from fur
66+ providers, err := LoadProviders(client.New())
67+ if err != nil || len(providers) == 0 {
68+ return nil, fmt.Errorf("failed to load providers: %w", err)
69+ }
70+
71+ env := env.New()
72+ // Configure providers
73+ valueResolver := NewShellVariableResolver(env)
74+ cfg.resolver = valueResolver
75+ if err := cfg.configureProviders(env, valueResolver, providers); err != nil {
76+ return nil, fmt.Errorf("failed to configure providers: %w", err)
77+ }
78+
79+ if !cfg.IsConfigured() {
80+ slog.Warn("No providers configured")
81+ return cfg, nil
82+ }
83+
84+ if err := cfg.configureSelectedModels(providers); err != nil {
85+ return nil, fmt.Errorf("failed to configure selected models: %w", err)
86+ }
87+
88+ // TODO: remove the agents concept from the config
89+ agents := map[string]Agent{
90+ "coder": {
91+ ID: "coder",
92+ Name: "Coder",
93+ Description: "An agent that helps with executing coding tasks.",
94+ Model: SelectedModelTypeLarge,
95+ ContextPaths: cfg.Options.ContextPaths,
96+ // All tools allowed
97+ },
98+ "task": {
99+ ID: "task",
100+ Name: "Task",
101+ Description: "An agent that helps with searching for context and finding implementation details.",
102+ Model: SelectedModelTypeLarge,
103+ ContextPaths: cfg.Options.ContextPaths,
104+ AllowedTools: []string{
105+ "glob",
106+ "grep",
107+ "ls",
108+ "sourcegraph",
109+ "view",
110+ },
111+ // NO MCPs or LSPs by default
112+ AllowedMCP: map[string][]string{},
113+ AllowedLSP: []string{},
114+ },
115+ }
116+ cfg.Agents = agents
117+
118+ return cfg, nil
119+}
120+
121+func (cfg *Config) configureProviders(env env.Env, resolver VariableResolver, knownProviders []provider.Provider) error {
122+ knownProviderNames := make(map[string]bool)
123+ for _, p := range knownProviders {
124+ knownProviderNames[string(p.ID)] = true
125+ config, configExists := cfg.Providers[string(p.ID)]
126+ // if the user configured a known provider we need to allow it to override a couple of parameters
127+ if configExists {
128+ if config.Disable {
129+ slog.Debug("Skipping provider due to disable flag", "provider", p.ID)
130+ delete(cfg.Providers, string(p.ID))
131+ continue
132+ }
133+ if config.BaseURL != "" {
134+ p.APIEndpoint = config.BaseURL
135+ }
136+ if config.APIKey != "" {
137+ p.APIKey = config.APIKey
138+ }
139+ if len(config.Models) > 0 {
140+ models := []provider.Model{}
141+ seen := make(map[string]bool)
142+
143+ for _, model := range config.Models {
144+ if seen[model.ID] {
145+ continue
146+ }
147+ seen[model.ID] = true
148+ if model.Model == "" {
149+ model.Model = model.ID
150+ }
151+ models = append(models, model)
152+ }
153+ for _, model := range p.Models {
154+ if seen[model.ID] {
155+ continue
156+ }
157+ seen[model.ID] = true
158+ if model.Model == "" {
159+ model.Model = model.ID
160+ }
161+ models = append(models, model)
162+ }
163+
164+ p.Models = models
165+ }
166+ }
167+ prepared := ProviderConfig{
168+ ID: string(p.ID),
169+ BaseURL: p.APIEndpoint,
170+ APIKey: p.APIKey,
171+ Type: p.Type,
172+ Disable: config.Disable,
173+ ExtraHeaders: config.ExtraHeaders,
174+ ExtraParams: make(map[string]string),
175+ Models: p.Models,
176+ }
177+
178+ switch p.ID {
179+ // Handle specific providers that require additional configuration
180+ case provider.InferenceProviderVertexAI:
181+ if !hasVertexCredentials(env) {
182+ if configExists {
183+ slog.Warn("Skipping Vertex AI provider due to missing credentials")
184+ delete(cfg.Providers, string(p.ID))
185+ }
186+ continue
187+ }
188+ prepared.ExtraParams["project"] = env.Get("GOOGLE_CLOUD_PROJECT")
189+ prepared.ExtraParams["location"] = env.Get("GOOGLE_CLOUD_LOCATION")
190+ case provider.InferenceProviderBedrock:
191+ if !hasAWSCredentials(env) {
192+ if configExists {
193+ slog.Warn("Skipping Bedrock provider due to missing AWS credentials")
194+ delete(cfg.Providers, string(p.ID))
195+ }
196+ continue
197+ }
198+ for _, model := range p.Models {
199+ if !strings.HasPrefix(model.ID, "anthropic.") {
200+ return fmt.Errorf("bedrock provider only supports anthropic models for now, found: %s", model.ID)
201+ }
202+ }
203+ default:
204+ // if the provider api or endpoint are missing we skip them
205+ v, err := resolver.ResolveValue(p.APIKey)
206+ if v == "" || err != nil {
207+ if configExists {
208+ slog.Warn("Skipping provider due to missing API key", "provider", p.ID)
209+ delete(cfg.Providers, string(p.ID))
210+ }
211+ continue
212+ }
213+ }
214+ cfg.Providers[string(p.ID)] = prepared
215+ }
216+
217+ // validate the custom providers
218+ for id, providerConfig := range cfg.Providers {
219+ if knownProviderNames[id] {
220+ continue
221+ }
222+
223+ // Make sure the provider ID is set
224+ providerConfig.ID = id
225+ // default to OpenAI if not set
226+ if providerConfig.Type == "" {
227+ providerConfig.Type = provider.TypeOpenAI
228+ }
229+
230+ if providerConfig.Disable {
231+ slog.Debug("Skipping custom provider due to disable flag", "provider", id)
232+ delete(cfg.Providers, id)
233+ continue
234+ }
235+ if providerConfig.APIKey == "" {
236+ slog.Warn("Skipping custom provider due to missing API key", "provider", id)
237+ delete(cfg.Providers, id)
238+ continue
239+ }
240+ if providerConfig.BaseURL == "" {
241+ slog.Warn("Skipping custom provider due to missing API endpoint", "provider", id)
242+ delete(cfg.Providers, id)
243+ continue
244+ }
245+ if len(providerConfig.Models) == 0 {
246+ slog.Warn("Skipping custom provider because the provider has no models", "provider", id)
247+ delete(cfg.Providers, id)
248+ continue
249+ }
250+ if providerConfig.Type != provider.TypeOpenAI {
251+ slog.Warn("Skipping custom provider because the provider type is not supported", "provider", id, "type", providerConfig.Type)
252+ delete(cfg.Providers, id)
253+ continue
254+ }
255+
256+ apiKey, err := resolver.ResolveValue(providerConfig.APIKey)
257+ if apiKey == "" || err != nil {
258+ slog.Warn("Skipping custom provider due to missing API key", "provider", id, "error", err)
259+ delete(cfg.Providers, id)
260+ continue
261+ }
262+ baseURL, err := resolver.ResolveValue(providerConfig.BaseURL)
263+ if baseURL == "" || err != nil {
264+ slog.Warn("Skipping custom provider due to missing API endpoint", "provider", id, "error", err)
265+ delete(cfg.Providers, id)
266+ continue
267+ }
268+
269+ cfg.Providers[id] = providerConfig
270+ }
271+ return nil
272+}
273+
274+func (cfg *Config) setDefaults(workingDir string) {
275+ cfg.workingDir = workingDir
276+ if cfg.Options == nil {
277+ cfg.Options = &Options{}
278+ }
279+ if cfg.Options.TUI == nil {
280+ cfg.Options.TUI = &TUIOptions{}
281+ }
282+ if cfg.Options.ContextPaths == nil {
283+ cfg.Options.ContextPaths = []string{}
284+ }
285+ if cfg.Options.DataDirectory == "" {
286+ cfg.Options.DataDirectory = filepath.Join(workingDir, defaultDataDirectory)
287+ }
288+ if cfg.Providers == nil {
289+ cfg.Providers = make(map[string]ProviderConfig)
290+ }
291+ if cfg.Models == nil {
292+ cfg.Models = make(map[SelectedModelType]SelectedModel)
293+ }
294+ if cfg.MCP == nil {
295+ cfg.MCP = make(map[string]MCPConfig)
296+ }
297+ if cfg.LSP == nil {
298+ cfg.LSP = make(map[string]LSPConfig)
299+ }
300+
301+ // Add the default context paths if they are not already present
302+ cfg.Options.ContextPaths = append(defaultContextPaths, cfg.Options.ContextPaths...)
303+ slices.Sort(cfg.Options.ContextPaths)
304+ cfg.Options.ContextPaths = slices.Compact(cfg.Options.ContextPaths)
305+}
306+
307+func (cfg *Config) defaultModelSelection(knownProviders []provider.Provider) (largeModel SelectedModel, smallModel SelectedModel, err error) {
308+ if len(knownProviders) == 0 && len(cfg.Providers) == 0 {
309+ err = fmt.Errorf("no providers configured, please configure at least one provider")
310+ return
311+ }
312+
313+ // Use the first provider enabled based on the known providers order
314+ // if no provider found that is known use the first provider configured
315+ for _, p := range knownProviders {
316+ providerConfig, ok := cfg.Providers[string(p.ID)]
317+ if !ok || providerConfig.Disable {
318+ continue
319+ }
320+ defaultLargeModel := cfg.GetModel(string(p.ID), p.DefaultLargeModelID)
321+ if defaultLargeModel == nil {
322+ err = fmt.Errorf("default large model %s not found for provider %s", p.DefaultLargeModelID, p.ID)
323+ return
324+ }
325+ largeModel = SelectedModel{
326+ Provider: string(p.ID),
327+ Model: defaultLargeModel.ID,
328+ MaxTokens: defaultLargeModel.DefaultMaxTokens,
329+ ReasoningEffort: defaultLargeModel.DefaultReasoningEffort,
330+ }
331+
332+ defaultSmallModel := cfg.GetModel(string(p.ID), p.DefaultSmallModelID)
333+ if defaultSmallModel == nil {
334+ err = fmt.Errorf("default small model %s not found for provider %s", p.DefaultSmallModelID, p.ID)
335+ }
336+ smallModel = SelectedModel{
337+ Provider: string(p.ID),
338+ Model: defaultSmallModel.ID,
339+ MaxTokens: defaultSmallModel.DefaultMaxTokens,
340+ ReasoningEffort: defaultSmallModel.DefaultReasoningEffort,
341+ }
342+ return
343+ }
344+
345+ enabledProviders := cfg.EnabledProviders()
346+ slices.SortFunc(enabledProviders, func(a, b ProviderConfig) int {
347+ return strings.Compare(a.ID, b.ID)
348+ })
349+
350+ if len(enabledProviders) == 0 {
351+ err = fmt.Errorf("no providers configured, please configure at least one provider")
352+ return
353+ }
354+
355+ providerConfig := enabledProviders[0]
356+ if len(providerConfig.Models) == 0 {
357+ err = fmt.Errorf("provider %s has no models configured", providerConfig.ID)
358+ return
359+ }
360+ defaultLargeModel := cfg.GetModel(providerConfig.ID, providerConfig.Models[0].ID)
361+ largeModel = SelectedModel{
362+ Provider: providerConfig.ID,
363+ Model: defaultLargeModel.ID,
364+ MaxTokens: defaultLargeModel.DefaultMaxTokens,
365+ }
366+ defaultSmallModel := cfg.GetModel(providerConfig.ID, providerConfig.Models[0].ID)
367+ smallModel = SelectedModel{
368+ Provider: providerConfig.ID,
369+ Model: defaultSmallModel.ID,
370+ MaxTokens: defaultSmallModel.DefaultMaxTokens,
371+ }
372+ return
373+}
374+
375+func (cfg *Config) configureSelectedModels(knownProviders []provider.Provider) error {
376+ large, small, err := cfg.defaultModelSelection(knownProviders)
377+ if err != nil {
378+ return fmt.Errorf("failed to select default models: %w", err)
379+ }
380+
381+ largeModelSelected, largeModelConfigured := cfg.Models[SelectedModelTypeLarge]
382+ if largeModelConfigured {
383+ if largeModelSelected.Model != "" {
384+ large.Model = largeModelSelected.Model
385+ }
386+ if largeModelSelected.Provider != "" {
387+ large.Provider = largeModelSelected.Provider
388+ }
389+ model := cfg.GetModel(large.Provider, large.Model)
390+ if model == nil {
391+ return fmt.Errorf("large model %s not found for provider %s", large.Model, large.Provider)
392+ }
393+ if largeModelSelected.MaxTokens > 0 {
394+ large.MaxTokens = largeModelSelected.MaxTokens
395+ } else {
396+ large.MaxTokens = model.DefaultMaxTokens
397+ }
398+ if largeModelSelected.ReasoningEffort != "" {
399+ large.ReasoningEffort = largeModelSelected.ReasoningEffort
400+ }
401+ large.Think = largeModelSelected.Think
402+ }
403+ smallModelSelected, smallModelConfigured := cfg.Models[SelectedModelTypeSmall]
404+ if smallModelConfigured {
405+ if smallModelSelected.Model != "" {
406+ small.Model = smallModelSelected.Model
407+ }
408+ if smallModelSelected.Provider != "" {
409+ small.Provider = smallModelSelected.Provider
410+ }
411+
412+ model := cfg.GetModel(small.Provider, small.Model)
413+ if model == nil {
414+ return fmt.Errorf("large model %s not found for provider %s", large.Model, large.Provider)
415+ }
416+ if smallModelSelected.MaxTokens > 0 {
417+ small.MaxTokens = smallModelSelected.MaxTokens
418+ } else {
419+ small.MaxTokens = model.DefaultMaxTokens
420+ }
421+ small.ReasoningEffort = smallModelSelected.ReasoningEffort
422+ small.Think = smallModelSelected.Think
423+ }
424+
425+ // validate the selected models
426+ largeModel := cfg.GetModel(large.Provider, large.Model)
427+ if largeModel == nil {
428+ return fmt.Errorf("large model %s not found for provider %s", large.Model, large.Provider)
429+ }
430+ smallModel := cfg.GetModel(small.Provider, small.Model)
431+ if smallModel == nil {
432+ return fmt.Errorf("small model %s not found for provider %s", small.Model, small.Provider)
433+ }
434+ cfg.Models[SelectedModelTypeLarge] = large
435+ cfg.Models[SelectedModelTypeSmall] = small
436+ return nil
437+}
438+
439+func loadFromConfigPaths(configPaths []string) (*Config, error) {
440+ var configs []io.Reader
441+
442+ for _, path := range configPaths {
443+ fd, err := os.Open(path)
444+ if err != nil {
445+ if os.IsNotExist(err) {
446+ continue
447+ }
448+ return nil, fmt.Errorf("failed to open config file %s: %w", path, err)
449+ }
450+ defer fd.Close()
451+
452+ configs = append(configs, fd)
453+ }
454+
455+ return loadFromReaders(configs)
456+}
457+
458+func loadFromReaders(readers []io.Reader) (*Config, error) {
459+ if len(readers) == 0 {
460+ return &Config{}, nil
461+ }
462+
463+ merged, err := Merge(readers)
464+ if err != nil {
465+ return nil, fmt.Errorf("failed to merge configuration readers: %w", err)
466+ }
467+
468+ return LoadReader(merged)
469+}
470+
471+func hasVertexCredentials(env env.Env) bool {
472+ useVertex := env.Get("GOOGLE_GENAI_USE_VERTEXAI") == "true"
473+ hasProject := env.Get("GOOGLE_CLOUD_PROJECT") != ""
474+ hasLocation := env.Get("GOOGLE_CLOUD_LOCATION") != ""
475+ return useVertex && hasProject && hasLocation
476+}
477+
478+func hasAWSCredentials(env env.Env) bool {
479+ if env.Get("AWS_ACCESS_KEY_ID") != "" && env.Get("AWS_SECRET_ACCESS_KEY") != "" {
480+ return true
481+ }
482+
483+ if env.Get("AWS_PROFILE") != "" || env.Get("AWS_DEFAULT_PROFILE") != "" {
484+ return true
485+ }
486+
487+ if env.Get("AWS_REGION") != "" || env.Get("AWS_DEFAULT_REGION") != "" {
488+ return true
489+ }
490+
491+ if env.Get("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
492+ env.Get("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
493+ return true
494+ }
495+
496+ return false
497+}
498+
499+func globalConfig() string {
500+ xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
501+ if xdgConfigHome != "" {
502+ return filepath.Join(xdgConfigHome, appName, fmt.Sprintf("%s.json", appName))
503+ }
504+
505+ // return the path to the main config directory
506+ // for windows, it should be in `%LOCALAPPDATA%/crush/`
507+ // for linux and macOS, it should be in `$HOME/.config/crush/`
508+ if runtime.GOOS == "windows" {
509+ localAppData := os.Getenv("LOCALAPPDATA")
510+ if localAppData == "" {
511+ localAppData = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local")
512+ }
513+ return filepath.Join(localAppData, appName, fmt.Sprintf("%s.json", appName))
514+ }
515+
516+ return filepath.Join(os.Getenv("HOME"), ".config", appName, fmt.Sprintf("%s.json", appName))
517+}
518+
519+// globalConfigData returns the path to the main data directory for the application.
520+// this config is used when the app overrides configurations instead of updating the global config.
521+func globalConfigData() string {
522+ xdgDataHome := os.Getenv("XDG_DATA_HOME")
523+ if xdgDataHome != "" {
524+ return filepath.Join(xdgDataHome, appName, fmt.Sprintf("%s.json", appName))
525+ }
526+
527+ // return the path to the main data directory
528+ // for windows, it should be in `%LOCALAPPDATA%/crush/`
529+ // for linux and macOS, it should be in `$HOME/.local/share/crush/`
530+ if runtime.GOOS == "windows" {
531+ localAppData := os.Getenv("LOCALAPPDATA")
532+ if localAppData == "" {
533+ localAppData = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local")
534+ }
535+ return filepath.Join(localAppData, appName, fmt.Sprintf("%s.json", appName))
536+ }
537+
538+ return filepath.Join(os.Getenv("HOME"), ".local", "share", appName, fmt.Sprintf("%s.json", appName))
539+}
internal/config/load_test.gonew file+1150
@@ -0,0 +1,1150 @@
1+package config
2+
3+import (
4+ "io"
5+ "log/slog"
6+ "os"
7+ "path/filepath"
8+ "strings"
9+ "testing"
10+
11+ "github.com/charmbracelet/crush/internal/env"
12+ "github.com/charmbracelet/crush/internal/fur/provider"
13+ "github.com/stretchr/testify/assert"
14+)
15+
16+func TestMain(m *testing.M) {
17+ slog.SetDefault(slog.New(slog.NewTextHandler(io.Discard, nil)))
18+
19+ exitVal := m.Run()
20+ os.Exit(exitVal)
21+}
22+
23+func TestConfig_LoadFromReaders(t *testing.T) {
24+ data1 := strings.NewReader(`{"providers": {"openai": {"api_key": "key1", "base_url": "https://api.openai.com/v1"}}}`)
25+ data2 := strings.NewReader(`{"providers": {"openai": {"api_key": "key2", "base_url": "https://api.openai.com/v2"}}}`)
26+ data3 := strings.NewReader(`{"providers": {"openai": {}}}`)
27+
28+ loadedConfig, err := loadFromReaders([]io.Reader{data1, data2, data3})
29+
30+ assert.NoError(t, err)
31+ assert.NotNil(t, loadedConfig)
32+ assert.Len(t, loadedConfig.Providers, 1)
33+ assert.Equal(t, "key2", loadedConfig.Providers["openai"].APIKey)
34+ assert.Equal(t, "https://api.openai.com/v2", loadedConfig.Providers["openai"].BaseURL)
35+}
36+
37+func TestConfig_setDefaults(t *testing.T) {
38+ cfg := &Config{}
39+
40+ cfg.setDefaults("/tmp")
41+
42+ assert.NotNil(t, cfg.Options)
43+ assert.NotNil(t, cfg.Options.TUI)
44+ assert.NotNil(t, cfg.Options.ContextPaths)
45+ assert.NotNil(t, cfg.Providers)
46+ assert.NotNil(t, cfg.Models)
47+ assert.NotNil(t, cfg.LSP)
48+ assert.NotNil(t, cfg.MCP)
49+ assert.Equal(t, filepath.Join("/tmp", ".crush"), cfg.Options.DataDirectory)
50+ for _, path := range defaultContextPaths {
51+ assert.Contains(t, cfg.Options.ContextPaths, path)
52+ }
53+ assert.Equal(t, "/tmp", cfg.workingDir)
54+}
55+
56+func TestConfig_configureProviders(t *testing.T) {
57+ knownProviders := []provider.Provider{
58+ {
59+ ID: "openai",
60+ APIKey: "$OPENAI_API_KEY",
61+ APIEndpoint: "https://api.openai.com/v1",
62+ Models: []provider.Model{{
63+ ID: "test-model",
64+ }},
65+ },
66+ }
67+
68+ cfg := &Config{}
69+ cfg.setDefaults("/tmp")
70+ env := env.NewFromMap(map[string]string{
71+ "OPENAI_API_KEY": "test-key",
72+ })
73+ resolver := NewEnvironmentVariableResolver(env)
74+ err := cfg.configureProviders(env, resolver, knownProviders)
75+ assert.NoError(t, err)
76+ assert.Len(t, cfg.Providers, 1)
77+
78+ // We want to make sure that we keep the configured API key as a placeholder
79+ assert.Equal(t, "$OPENAI_API_KEY", cfg.Providers["openai"].APIKey)
80+}
81+
82+func TestConfig_configureProvidersWithOverride(t *testing.T) {
83+ knownProviders := []provider.Provider{
84+ {
85+ ID: "openai",
86+ APIKey: "$OPENAI_API_KEY",
87+ APIEndpoint: "https://api.openai.com/v1",
88+ Models: []provider.Model{{
89+ ID: "test-model",
90+ }},
91+ },
92+ }
93+
94+ cfg := &Config{
95+ Providers: map[string]ProviderConfig{
96+ "openai": {
97+ APIKey: "xyz",
98+ BaseURL: "https://api.openai.com/v2",
99+ Models: []provider.Model{
100+ {
101+ ID: "test-model",
102+ Model: "Updated",
103+ },
104+ {
105+ ID: "another-model",
106+ },
107+ },
108+ },
109+ },
110+ }
111+ cfg.setDefaults("/tmp")
112+
113+ env := env.NewFromMap(map[string]string{
114+ "OPENAI_API_KEY": "test-key",
115+ })
116+ resolver := NewEnvironmentVariableResolver(env)
117+ err := cfg.configureProviders(env, resolver, knownProviders)
118+ assert.NoError(t, err)
119+ assert.Len(t, cfg.Providers, 1)
120+
121+ // We want to make sure that we keep the configured API key as a placeholder
122+ assert.Equal(t, "xyz", cfg.Providers["openai"].APIKey)
123+ assert.Equal(t, "https://api.openai.com/v2", cfg.Providers["openai"].BaseURL)
124+ assert.Len(t, cfg.Providers["openai"].Models, 2)
125+ assert.Equal(t, "Updated", cfg.Providers["openai"].Models[0].Model)
126+}
127+
128+func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
129+ knownProviders := []provider.Provider{
130+ {
131+ ID: "openai",
132+ APIKey: "$OPENAI_API_KEY",
133+ APIEndpoint: "https://api.openai.com/v1",
134+ Models: []provider.Model{{
135+ ID: "test-model",
136+ }},
137+ },
138+ }
139+
140+ cfg := &Config{
141+ Providers: map[string]ProviderConfig{
142+ "custom": {
143+ APIKey: "xyz",
144+ BaseURL: "https://api.someendpoint.com/v2",
145+ Models: []provider.Model{
146+ {
147+ ID: "test-model",
148+ },
149+ },
150+ },
151+ },
152+ }
153+ cfg.setDefaults("/tmp")
154+ env := env.NewFromMap(map[string]string{
155+ "OPENAI_API_KEY": "test-key",
156+ })
157+ resolver := NewEnvironmentVariableResolver(env)
158+ err := cfg.configureProviders(env, resolver, knownProviders)
159+ assert.NoError(t, err)
160+ // Should be to because of the env variable
161+ assert.Len(t, cfg.Providers, 2)
162+
163+ // We want to make sure that we keep the configured API key as a placeholder
164+ assert.Equal(t, "xyz", cfg.Providers["custom"].APIKey)
165+ // Make sure we set the ID correctly
166+ assert.Equal(t, "custom", cfg.Providers["custom"].ID)
167+ assert.Equal(t, "https://api.someendpoint.com/v2", cfg.Providers["custom"].BaseURL)
168+ assert.Len(t, cfg.Providers["custom"].Models, 1)
169+
170+ _, ok := cfg.Providers["openai"]
171+ assert.True(t, ok, "OpenAI provider should still be present")
172+}
173+
174+func TestConfig_configureProvidersBedrockWithCredentials(t *testing.T) {
175+ knownProviders := []provider.Provider{
176+ {
177+ ID: provider.InferenceProviderBedrock,
178+ APIKey: "",
179+ APIEndpoint: "",
180+ Models: []provider.Model{{
181+ ID: "anthropic.claude-sonnet-4-20250514-v1:0",
182+ }},
183+ },
184+ }
185+
186+ cfg := &Config{}
187+ cfg.setDefaults("/tmp")
188+ env := env.NewFromMap(map[string]string{
189+ "AWS_ACCESS_KEY_ID": "test-key-id",
190+ "AWS_SECRET_ACCESS_KEY": "test-secret-key",
191+ })
192+ resolver := NewEnvironmentVariableResolver(env)
193+ err := cfg.configureProviders(env, resolver, knownProviders)
194+ assert.NoError(t, err)
195+ assert.Len(t, cfg.Providers, 1)
196+
197+ bedrockProvider, ok := cfg.Providers["bedrock"]
198+ assert.True(t, ok, "Bedrock provider should be present")
199+ assert.Len(t, bedrockProvider.Models, 1)
200+ assert.Equal(t, "anthropic.claude-sonnet-4-20250514-v1:0", bedrockProvider.Models[0].ID)
201+}
202+
203+func TestConfig_configureProvidersBedrockWithoutCredentials(t *testing.T) {
204+ knownProviders := []provider.Provider{
205+ {
206+ ID: provider.InferenceProviderBedrock,
207+ APIKey: "",
208+ APIEndpoint: "",
209+ Models: []provider.Model{{
210+ ID: "anthropic.claude-sonnet-4-20250514-v1:0",
211+ }},
212+ },
213+ }
214+
215+ cfg := &Config{}
216+ cfg.setDefaults("/tmp")
217+ env := env.NewFromMap(map[string]string{})
218+ resolver := NewEnvironmentVariableResolver(env)
219+ err := cfg.configureProviders(env, resolver, knownProviders)
220+ assert.NoError(t, err)
221+ // Provider should not be configured without credentials
222+ assert.Len(t, cfg.Providers, 0)
223+}
224+
225+func TestConfig_configureProvidersBedrockWithoutUnsupportedModel(t *testing.T) {
226+ knownProviders := []provider.Provider{
227+ {
228+ ID: provider.InferenceProviderBedrock,
229+ APIKey: "",
230+ APIEndpoint: "",
231+ Models: []provider.Model{{
232+ ID: "some-random-model",
233+ }},
234+ },
235+ }
236+
237+ cfg := &Config{}
238+ cfg.setDefaults("/tmp")
239+ env := env.NewFromMap(map[string]string{
240+ "AWS_ACCESS_KEY_ID": "test-key-id",
241+ "AWS_SECRET_ACCESS_KEY": "test-secret-key",
242+ })
243+ resolver := NewEnvironmentVariableResolver(env)
244+ err := cfg.configureProviders(env, resolver, knownProviders)
245+ assert.Error(t, err)
246+}
247+
248+func TestConfig_configureProvidersVertexAIWithCredentials(t *testing.T) {
249+ knownProviders := []provider.Provider{
250+ {
251+ ID: provider.InferenceProviderVertexAI,
252+ APIKey: "",
253+ APIEndpoint: "",
254+ Models: []provider.Model{{
255+ ID: "gemini-pro",
256+ }},
257+ },
258+ }
259+
260+ cfg := &Config{}
261+ cfg.setDefaults("/tmp")
262+ env := env.NewFromMap(map[string]string{
263+ "GOOGLE_GENAI_USE_VERTEXAI": "true",
264+ "GOOGLE_CLOUD_PROJECT": "test-project",
265+ "GOOGLE_CLOUD_LOCATION": "us-central1",
266+ })
267+ resolver := NewEnvironmentVariableResolver(env)
268+ err := cfg.configureProviders(env, resolver, knownProviders)
269+ assert.NoError(t, err)
270+ assert.Len(t, cfg.Providers, 1)
271+
272+ vertexProvider, ok := cfg.Providers["vertexai"]
273+ assert.True(t, ok, "VertexAI provider should be present")
274+ assert.Len(t, vertexProvider.Models, 1)
275+ assert.Equal(t, "gemini-pro", vertexProvider.Models[0].ID)
276+ assert.Equal(t, "test-project", vertexProvider.ExtraParams["project"])
277+ assert.Equal(t, "us-central1", vertexProvider.ExtraParams["location"])
278+}
279+
280+func TestConfig_configureProvidersVertexAIWithoutCredentials(t *testing.T) {
281+ knownProviders := []provider.Provider{
282+ {
283+ ID: provider.InferenceProviderVertexAI,
284+ APIKey: "",
285+ APIEndpoint: "",
286+ Models: []provider.Model{{
287+ ID: "gemini-pro",
288+ }},
289+ },
290+ }
291+
292+ cfg := &Config{}
293+ cfg.setDefaults("/tmp")
294+ env := env.NewFromMap(map[string]string{
295+ "GOOGLE_GENAI_USE_VERTEXAI": "false",
296+ "GOOGLE_CLOUD_PROJECT": "test-project",
297+ "GOOGLE_CLOUD_LOCATION": "us-central1",
298+ })
299+ resolver := NewEnvironmentVariableResolver(env)
300+ err := cfg.configureProviders(env, resolver, knownProviders)
301+ assert.NoError(t, err)
302+ // Provider should not be configured without proper credentials
303+ assert.Len(t, cfg.Providers, 0)
304+}
305+
306+func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) {
307+ knownProviders := []provider.Provider{
308+ {
309+ ID: provider.InferenceProviderVertexAI,
310+ APIKey: "",
311+ APIEndpoint: "",
312+ Models: []provider.Model{{
313+ ID: "gemini-pro",
314+ }},
315+ },
316+ }
317+
318+ cfg := &Config{}
319+ cfg.setDefaults("/tmp")
320+ env := env.NewFromMap(map[string]string{
321+ "GOOGLE_GENAI_USE_VERTEXAI": "true",
322+ "GOOGLE_CLOUD_LOCATION": "us-central1",
323+ })
324+ resolver := NewEnvironmentVariableResolver(env)
325+ err := cfg.configureProviders(env, resolver, knownProviders)
326+ assert.NoError(t, err)
327+ // Provider should not be configured without project
328+ assert.Len(t, cfg.Providers, 0)
329+}
330+
331+func TestConfig_configureProvidersSetProviderID(t *testing.T) {
332+ knownProviders := []provider.Provider{
333+ {
334+ ID: "openai",
335+ APIKey: "$OPENAI_API_KEY",
336+ APIEndpoint: "https://api.openai.com/v1",
337+ Models: []provider.Model{{
338+ ID: "test-model",
339+ }},
340+ },
341+ }
342+
343+ cfg := &Config{}
344+ cfg.setDefaults("/tmp")
345+ env := env.NewFromMap(map[string]string{
346+ "OPENAI_API_KEY": "test-key",
347+ })
348+ resolver := NewEnvironmentVariableResolver(env)
349+ err := cfg.configureProviders(env, resolver, knownProviders)
350+ assert.NoError(t, err)
351+ assert.Len(t, cfg.Providers, 1)
352+
353+ // Provider ID should be set
354+ assert.Equal(t, "openai", cfg.Providers["openai"].ID)
355+}
356+
357+func TestConfig_EnabledProviders(t *testing.T) {
358+ t.Run("all providers enabled", func(t *testing.T) {
359+ cfg := &Config{
360+ Providers: map[string]ProviderConfig{
361+ "openai": {
362+ ID: "openai",
363+ APIKey: "key1",
364+ Disable: false,
365+ },
366+ "anthropic": {
367+ ID: "anthropic",
368+ APIKey: "key2",
369+ Disable: false,
370+ },
371+ },
372+ }
373+
374+ enabled := cfg.EnabledProviders()
375+ assert.Len(t, enabled, 2)
376+ })
377+
378+ t.Run("some providers disabled", func(t *testing.T) {
379+ cfg := &Config{
380+ Providers: map[string]ProviderConfig{
381+ "openai": {
382+ ID: "openai",
383+ APIKey: "key1",
384+ Disable: false,
385+ },
386+ "anthropic": {
387+ ID: "anthropic",
388+ APIKey: "key2",
389+ Disable: true,
390+ },
391+ },
392+ }
393+
394+ enabled := cfg.EnabledProviders()
395+ assert.Len(t, enabled, 1)
396+ assert.Equal(t, "openai", enabled[0].ID)
397+ })
398+
399+ t.Run("empty providers map", func(t *testing.T) {
400+ cfg := &Config{
401+ Providers: map[string]ProviderConfig{},
402+ }
403+
404+ enabled := cfg.EnabledProviders()
405+ assert.Len(t, enabled, 0)
406+ })
407+}
408+
409+func TestConfig_IsConfigured(t *testing.T) {
410+ t.Run("returns true when at least one provider is enabled", func(t *testing.T) {
411+ cfg := &Config{
412+ Providers: map[string]ProviderConfig{
413+ "openai": {
414+ ID: "openai",
415+ APIKey: "key1",
416+ Disable: false,
417+ },
418+ },
419+ }
420+
421+ assert.True(t, cfg.IsConfigured())
422+ })
423+
424+ t.Run("returns false when no providers are configured", func(t *testing.T) {
425+ cfg := &Config{
426+ Providers: map[string]ProviderConfig{},
427+ }
428+
429+ assert.False(t, cfg.IsConfigured())
430+ })
431+
432+ t.Run("returns false when all providers are disabled", func(t *testing.T) {
433+ cfg := &Config{
434+ Providers: map[string]ProviderConfig{
435+ "openai": {
436+ ID: "openai",
437+ APIKey: "key1",
438+ Disable: true,
439+ },
440+ "anthropic": {
441+ ID: "anthropic",
442+ APIKey: "key2",
443+ Disable: true,
444+ },
445+ },
446+ }
447+
448+ assert.False(t, cfg.IsConfigured())
449+ })
450+}
451+
452+func TestConfig_configureProvidersWithDisabledProvider(t *testing.T) {
453+ knownProviders := []provider.Provider{
454+ {
455+ ID: "openai",
456+ APIKey: "$OPENAI_API_KEY",
457+ APIEndpoint: "https://api.openai.com/v1",
458+ Models: []provider.Model{{
459+ ID: "test-model",
460+ }},
461+ },
462+ }
463+
464+ cfg := &Config{
465+ Providers: map[string]ProviderConfig{
466+ "openai": {
467+ Disable: true,
468+ },
469+ },
470+ }
471+ cfg.setDefaults("/tmp")
472+
473+ env := env.NewFromMap(map[string]string{
474+ "OPENAI_API_KEY": "test-key",
475+ })
476+ resolver := NewEnvironmentVariableResolver(env)
477+ err := cfg.configureProviders(env, resolver, knownProviders)
478+ assert.NoError(t, err)
479+
480+ // Provider should be removed from config when disabled
481+ assert.Len(t, cfg.Providers, 0)
482+ _, exists := cfg.Providers["openai"]
483+ assert.False(t, exists)
484+}
485+
486+func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
487+ t.Run("custom provider with missing API key is removed", func(t *testing.T) {
488+ cfg := &Config{
489+ Providers: map[string]ProviderConfig{
490+ "custom": {
491+ BaseURL: "https://api.custom.com/v1",
492+ Models: []provider.Model{{
493+ ID: "test-model",
494+ }},
495+ },
496+ },
497+ }
498+ cfg.setDefaults("/tmp")
499+
500+ env := env.NewFromMap(map[string]string{})
501+ resolver := NewEnvironmentVariableResolver(env)
502+ err := cfg.configureProviders(env, resolver, []provider.Provider{})
503+ assert.NoError(t, err)
504+
505+ assert.Len(t, cfg.Providers, 0)
506+ _, exists := cfg.Providers["custom"]
507+ assert.False(t, exists)
508+ })
509+
510+ t.Run("custom provider with missing BaseURL is removed", func(t *testing.T) {
511+ cfg := &Config{
512+ Providers: map[string]ProviderConfig{
513+ "custom": {
514+ APIKey: "test-key",
515+ Models: []provider.Model{{
516+ ID: "test-model",
517+ }},
518+ },
519+ },
520+ }
521+ cfg.setDefaults("/tmp")
522+
523+ env := env.NewFromMap(map[string]string{})
524+ resolver := NewEnvironmentVariableResolver(env)
525+ err := cfg.configureProviders(env, resolver, []provider.Provider{})
526+ assert.NoError(t, err)
527+
528+ assert.Len(t, cfg.Providers, 0)
529+ _, exists := cfg.Providers["custom"]
530+ assert.False(t, exists)
531+ })
532+
533+ t.Run("custom provider with no models is removed", func(t *testing.T) {
534+ cfg := &Config{
535+ Providers: map[string]ProviderConfig{
536+ "custom": {
537+ APIKey: "test-key",
538+ BaseURL: "https://api.custom.com/v1",
539+ Models: []provider.Model{},
540+ },
541+ },
542+ }
543+ cfg.setDefaults("/tmp")
544+
545+ env := env.NewFromMap(map[string]string{})
546+ resolver := NewEnvironmentVariableResolver(env)
547+ err := cfg.configureProviders(env, resolver, []provider.Provider{})
548+ assert.NoError(t, err)
549+
550+ assert.Len(t, cfg.Providers, 0)
551+ _, exists := cfg.Providers["custom"]
552+ assert.False(t, exists)
553+ })
554+
555+ t.Run("custom provider with unsupported type is removed", func(t *testing.T) {
556+ cfg := &Config{
557+ Providers: map[string]ProviderConfig{
558+ "custom": {
559+ APIKey: "test-key",
560+ BaseURL: "https://api.custom.com/v1",
561+ Type: "unsupported",
562+ Models: []provider.Model{{
563+ ID: "test-model",
564+ }},
565+ },
566+ },
567+ }
568+ cfg.setDefaults("/tmp")
569+
570+ env := env.NewFromMap(map[string]string{})
571+ resolver := NewEnvironmentVariableResolver(env)
572+ err := cfg.configureProviders(env, resolver, []provider.Provider{})
573+ assert.NoError(t, err)
574+
575+ assert.Len(t, cfg.Providers, 0)
576+ _, exists := cfg.Providers["custom"]
577+ assert.False(t, exists)
578+ })
579+
580+ t.Run("valid custom provider is kept and ID is set", func(t *testing.T) {
581+ cfg := &Config{
582+ Providers: map[string]ProviderConfig{
583+ "custom": {
584+ APIKey: "test-key",
585+ BaseURL: "https://api.custom.com/v1",
586+ Type: provider.TypeOpenAI,
587+ Models: []provider.Model{{
588+ ID: "test-model",
589+ }},
590+ },
591+ },
592+ }
593+ cfg.setDefaults("/tmp")
594+
595+ env := env.NewFromMap(map[string]string{})
596+ resolver := NewEnvironmentVariableResolver(env)
597+ err := cfg.configureProviders(env, resolver, []provider.Provider{})
598+ assert.NoError(t, err)
599+
600+ assert.Len(t, cfg.Providers, 1)
601+ customProvider, exists := cfg.Providers["custom"]
602+ assert.True(t, exists)
603+ assert.Equal(t, "custom", customProvider.ID)
604+ assert.Equal(t, "test-key", customProvider.APIKey)
605+ assert.Equal(t, "https://api.custom.com/v1", customProvider.BaseURL)
606+ })
607+
608+ t.Run("disabled custom provider is removed", func(t *testing.T) {
609+ cfg := &Config{
610+ Providers: map[string]ProviderConfig{
611+ "custom": {
612+ APIKey: "test-key",
613+ BaseURL: "https://api.custom.com/v1",
614+ Type: provider.TypeOpenAI,
615+ Disable: true,
616+ Models: []provider.Model{{
617+ ID: "test-model",
618+ }},
619+ },
620+ },
621+ }
622+ cfg.setDefaults("/tmp")
623+
624+ env := env.NewFromMap(map[string]string{})
625+ resolver := NewEnvironmentVariableResolver(env)
626+ err := cfg.configureProviders(env, resolver, []provider.Provider{})
627+ assert.NoError(t, err)
628+
629+ assert.Len(t, cfg.Providers, 0)
630+ _, exists := cfg.Providers["custom"]
631+ assert.False(t, exists)
632+ })
633+}
634+
635+func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
636+ t.Run("VertexAI provider removed when credentials missing with existing config", func(t *testing.T) {
637+ knownProviders := []provider.Provider{
638+ {
639+ ID: provider.InferenceProviderVertexAI,
640+ APIKey: "",
641+ APIEndpoint: "",
642+ Models: []provider.Model{{
643+ ID: "gemini-pro",
644+ }},
645+ },
646+ }
647+
648+ cfg := &Config{
649+ Providers: map[string]ProviderConfig{
650+ "vertexai": {
651+ BaseURL: "custom-url",
652+ },
653+ },
654+ }
655+ cfg.setDefaults("/tmp")
656+
657+ env := env.NewFromMap(map[string]string{
658+ "GOOGLE_GENAI_USE_VERTEXAI": "false",
659+ })
660+ resolver := NewEnvironmentVariableResolver(env)
661+ err := cfg.configureProviders(env, resolver, knownProviders)
662+ assert.NoError(t, err)
663+
664+ assert.Len(t, cfg.Providers, 0)
665+ _, exists := cfg.Providers["vertexai"]
666+ assert.False(t, exists)
667+ })
668+
669+ t.Run("Bedrock provider removed when AWS credentials missing with existing config", func(t *testing.T) {
670+ knownProviders := []provider.Provider{
671+ {
672+ ID: provider.InferenceProviderBedrock,
673+ APIKey: "",
674+ APIEndpoint: "",
675+ Models: []provider.Model{{
676+ ID: "anthropic.claude-sonnet-4-20250514-v1:0",
677+ }},
678+ },
679+ }
680+
681+ cfg := &Config{
682+ Providers: map[string]ProviderConfig{
683+ "bedrock": {
684+ BaseURL: "custom-url",
685+ },
686+ },
687+ }
688+ cfg.setDefaults("/tmp")
689+
690+ env := env.NewFromMap(map[string]string{})
691+ resolver := NewEnvironmentVariableResolver(env)
692+ err := cfg.configureProviders(env, resolver, knownProviders)
693+ assert.NoError(t, err)
694+
695+ assert.Len(t, cfg.Providers, 0)
696+ _, exists := cfg.Providers["bedrock"]
697+ assert.False(t, exists)
698+ })
699+
700+ t.Run("provider removed when API key missing with existing config", func(t *testing.T) {
701+ knownProviders := []provider.Provider{
702+ {
703+ ID: "openai",
704+ APIKey: "$MISSING_API_KEY",
705+ APIEndpoint: "https://api.openai.com/v1",
706+ Models: []provider.Model{{
707+ ID: "test-model",
708+ }},
709+ },
710+ }
711+
712+ cfg := &Config{
713+ Providers: map[string]ProviderConfig{
714+ "openai": {
715+ BaseURL: "custom-url",
716+ },
717+ },
718+ }
719+ cfg.setDefaults("/tmp")
720+
721+ env := env.NewFromMap(map[string]string{})
722+ resolver := NewEnvironmentVariableResolver(env)
723+ err := cfg.configureProviders(env, resolver, knownProviders)
724+ assert.NoError(t, err)
725+
726+ assert.Len(t, cfg.Providers, 0)
727+ _, exists := cfg.Providers["openai"]
728+ assert.False(t, exists)
729+ })
730+
731+ t.Run("known provider should still be added if the endpoint is missing the client will use default endpoints", func(t *testing.T) {
732+ knownProviders := []provider.Provider{
733+ {
734+ ID: "openai",
735+ APIKey: "$OPENAI_API_KEY",
736+ APIEndpoint: "$MISSING_ENDPOINT",
737+ Models: []provider.Model{{
738+ ID: "test-model",
739+ }},
740+ },
741+ }
742+
743+ cfg := &Config{
744+ Providers: map[string]ProviderConfig{
745+ "openai": {
746+ APIKey: "test-key",
747+ },
748+ },
749+ }
750+ cfg.setDefaults("/tmp")
751+
752+ env := env.NewFromMap(map[string]string{
753+ "OPENAI_API_KEY": "test-key",
754+ })
755+ resolver := NewEnvironmentVariableResolver(env)
756+ err := cfg.configureProviders(env, resolver, knownProviders)
757+ assert.NoError(t, err)
758+
759+ assert.Len(t, cfg.Providers, 1)
760+ _, exists := cfg.Providers["openai"]
761+ assert.True(t, exists)
762+ })
763+}
764+
765+func TestConfig_defaultModelSelection(t *testing.T) {
766+ t.Run("default behavior uses the default models for given provider", func(t *testing.T) {
767+ knownProviders := []provider.Provider{
768+ {
769+ ID: "openai",
770+ APIKey: "abc",
771+ DefaultLargeModelID: "large-model",
772+ DefaultSmallModelID: "small-model",
773+ Models: []provider.Model{
774+ {
775+ ID: "large-model",
776+ DefaultMaxTokens: 1000,
777+ },
778+ {
779+ ID: "small-model",
780+ DefaultMaxTokens: 500,
781+ },
782+ },
783+ },
784+ }
785+
786+ cfg := &Config{}
787+ cfg.setDefaults("/tmp")
788+ env := env.NewFromMap(map[string]string{})
789+ resolver := NewEnvironmentVariableResolver(env)
790+ err := cfg.configureProviders(env, resolver, knownProviders)
791+ assert.NoError(t, err)
792+
793+ large, small, err := cfg.defaultModelSelection(knownProviders)
794+ assert.NoError(t, err)
795+ assert.Equal(t, "large-model", large.Model)
796+ assert.Equal(t, "openai", large.Provider)
797+ assert.Equal(t, int64(1000), large.MaxTokens)
798+ assert.Equal(t, "small-model", small.Model)
799+ assert.Equal(t, "openai", small.Provider)
800+ assert.Equal(t, int64(500), small.MaxTokens)
801+ })
802+ t.Run("should error if no providers configured", func(t *testing.T) {
803+ knownProviders := []provider.Provider{
804+ {
805+ ID: "openai",
806+ APIKey: "$MISSING_KEY",
807+ DefaultLargeModelID: "large-model",
808+ DefaultSmallModelID: "small-model",
809+ Models: []provider.Model{
810+ {
811+ ID: "large-model",
812+ DefaultMaxTokens: 1000,
813+ },
814+ {
815+ ID: "small-model",
816+ DefaultMaxTokens: 500,
817+ },
818+ },
819+ },
820+ }
821+
822+ cfg := &Config{}
823+ cfg.setDefaults("/tmp")
824+ env := env.NewFromMap(map[string]string{})
825+ resolver := NewEnvironmentVariableResolver(env)
826+ err := cfg.configureProviders(env, resolver, knownProviders)
827+ assert.NoError(t, err)
828+
829+ _, _, err = cfg.defaultModelSelection(knownProviders)
830+ assert.Error(t, err)
831+ })
832+ t.Run("should error if model is missing", func(t *testing.T) {
833+ knownProviders := []provider.Provider{
834+ {
835+ ID: "openai",
836+ APIKey: "abc",
837+ DefaultLargeModelID: "large-model",
838+ DefaultSmallModelID: "small-model",
839+ Models: []provider.Model{
840+ {
841+ ID: "not-large-model",
842+ DefaultMaxTokens: 1000,
843+ },
844+ {
845+ ID: "small-model",
846+ DefaultMaxTokens: 500,
847+ },
848+ },
849+ },
850+ }
851+
852+ cfg := &Config{}
853+ cfg.setDefaults("/tmp")
854+ env := env.NewFromMap(map[string]string{})
855+ resolver := NewEnvironmentVariableResolver(env)
856+ err := cfg.configureProviders(env, resolver, knownProviders)
857+ assert.NoError(t, err)
858+ _, _, err = cfg.defaultModelSelection(knownProviders)
859+ assert.Error(t, err)
860+ })
861+
862+ t.Run("should configure the default models with a custom provider", func(t *testing.T) {
863+ knownProviders := []provider.Provider{
864+ {
865+ ID: "openai",
866+ APIKey: "$MISSING", // will not be included in the config
867+ DefaultLargeModelID: "large-model",
868+ DefaultSmallModelID: "small-model",
869+ Models: []provider.Model{
870+ {
871+ ID: "not-large-model",
872+ DefaultMaxTokens: 1000,
873+ },
874+ {
875+ ID: "small-model",
876+ DefaultMaxTokens: 500,
877+ },
878+ },
879+ },
880+ }
881+
882+ cfg := &Config{
883+ Providers: map[string]ProviderConfig{
884+ "custom": {
885+ APIKey: "test-key",
886+ BaseURL: "https://api.custom.com/v1",
887+ Models: []provider.Model{
888+ {
889+ ID: "model",
890+ DefaultMaxTokens: 600,
891+ },
892+ },
893+ },
894+ },
895+ }
896+ cfg.setDefaults("/tmp")
897+ env := env.NewFromMap(map[string]string{})
898+ resolver := NewEnvironmentVariableResolver(env)
899+ err := cfg.configureProviders(env, resolver, knownProviders)
900+ assert.NoError(t, err)
901+ large, small, err := cfg.defaultModelSelection(knownProviders)
902+ assert.NoError(t, err)
903+ assert.Equal(t, "model", large.Model)
904+ assert.Equal(t, "custom", large.Provider)
905+ assert.Equal(t, int64(600), large.MaxTokens)
906+ assert.Equal(t, "model", small.Model)
907+ assert.Equal(t, "custom", small.Provider)
908+ assert.Equal(t, int64(600), small.MaxTokens)
909+ })
910+
911+ t.Run("should fail if no model configured", func(t *testing.T) {
912+ knownProviders := []provider.Provider{
913+ {
914+ ID: "openai",
915+ APIKey: "$MISSING", // will not be included in the config
916+ DefaultLargeModelID: "large-model",
917+ DefaultSmallModelID: "small-model",
918+ Models: []provider.Model{
919+ {
920+ ID: "not-large-model",
921+ DefaultMaxTokens: 1000,
922+ },
923+ {
924+ ID: "small-model",
925+ DefaultMaxTokens: 500,
926+ },
927+ },
928+ },
929+ }
930+
931+ cfg := &Config{
932+ Providers: map[string]ProviderConfig{
933+ "custom": {
934+ APIKey: "test-key",
935+ BaseURL: "https://api.custom.com/v1",
936+ Models: []provider.Model{},
937+ },
938+ },
939+ }
940+ cfg.setDefaults("/tmp")
941+ env := env.NewFromMap(map[string]string{})
942+ resolver := NewEnvironmentVariableResolver(env)
943+ err := cfg.configureProviders(env, resolver, knownProviders)
944+ assert.NoError(t, err)
945+ _, _, err = cfg.defaultModelSelection(knownProviders)
946+ assert.Error(t, err)
947+ })
948+ t.Run("should use the default provider first", func(t *testing.T) {
949+ knownProviders := []provider.Provider{
950+ {
951+ ID: "openai",
952+ APIKey: "set",
953+ DefaultLargeModelID: "large-model",
954+ DefaultSmallModelID: "small-model",
955+ Models: []provider.Model{
956+ {
957+ ID: "large-model",
958+ DefaultMaxTokens: 1000,
959+ },
960+ {
961+ ID: "small-model",
962+ DefaultMaxTokens: 500,
963+ },
964+ },
965+ },
966+ }
967+
968+ cfg := &Config{
969+ Providers: map[string]ProviderConfig{
970+ "custom": {
971+ APIKey: "test-key",
972+ BaseURL: "https://api.custom.com/v1",
973+ Models: []provider.Model{
974+ {
975+ ID: "large-model",
976+ DefaultMaxTokens: 1000,
977+ },
978+ },
979+ },
980+ },
981+ }
982+ cfg.setDefaults("/tmp")
983+ env := env.NewFromMap(map[string]string{})
984+ resolver := NewEnvironmentVariableResolver(env)
985+ err := cfg.configureProviders(env, resolver, knownProviders)
986+ assert.NoError(t, err)
987+ large, small, err := cfg.defaultModelSelection(knownProviders)
988+ assert.NoError(t, err)
989+ assert.Equal(t, "large-model", large.Model)
990+ assert.Equal(t, "openai", large.Provider)
991+ assert.Equal(t, int64(1000), large.MaxTokens)
992+ assert.Equal(t, "small-model", small.Model)
993+ assert.Equal(t, "openai", small.Provider)
994+ assert.Equal(t, int64(500), small.MaxTokens)
995+ })
996+}
997+
998+func TestConfig_configureSelectedModels(t *testing.T) {
999+ t.Run("should override defaults", func(t *testing.T) {
1000+ knownProviders := []provider.Provider{
1001+ {
1002+ ID: "openai",
1003+ APIKey: "abc",
1004+ DefaultLargeModelID: "large-model",
1005+ DefaultSmallModelID: "small-model",
1006+ Models: []provider.Model{
1007+ {
1008+ ID: "larger-model",
1009+ DefaultMaxTokens: 2000,
1010+ },
1011+ {
1012+ ID: "large-model",
1013+ DefaultMaxTokens: 1000,
1014+ },
1015+ {
1016+ ID: "small-model",
1017+ DefaultMaxTokens: 500,
1018+ },
1019+ },
1020+ },
1021+ }
1022+
1023+ cfg := &Config{
1024+ Models: map[SelectedModelType]SelectedModel{
1025+ "large": {
1026+ Model: "larger-model",
1027+ },
1028+ },
1029+ }
1030+ cfg.setDefaults("/tmp")
1031+ env := env.NewFromMap(map[string]string{})
1032+ resolver := NewEnvironmentVariableResolver(env)
1033+ err := cfg.configureProviders(env, resolver, knownProviders)
1034+ assert.NoError(t, err)
1035+
1036+ err = cfg.configureSelectedModels(knownProviders)
1037+ assert.NoError(t, err)
1038+ large := cfg.Models[SelectedModelTypeLarge]
1039+ small := cfg.Models[SelectedModelTypeSmall]
1040+ assert.Equal(t, "larger-model", large.Model)
1041+ assert.Equal(t, "openai", large.Provider)
1042+ assert.Equal(t, int64(2000), large.MaxTokens)
1043+ assert.Equal(t, "small-model", small.Model)
1044+ assert.Equal(t, "openai", small.Provider)
1045+ assert.Equal(t, int64(500), small.MaxTokens)
1046+ })
1047+ t.Run("should be possible to use multiple providers", func(t *testing.T) {
1048+ knownProviders := []provider.Provider{
1049+ {
1050+ ID: "openai",
1051+ APIKey: "abc",
1052+ DefaultLargeModelID: "large-model",
1053+ DefaultSmallModelID: "small-model",
1054+ Models: []provider.Model{
1055+ {
1056+ ID: "large-model",
1057+ DefaultMaxTokens: 1000,
1058+ },
1059+ {
1060+ ID: "small-model",
1061+ DefaultMaxTokens: 500,
1062+ },
1063+ },
1064+ },
1065+ {
1066+ ID: "anthropic",
1067+ APIKey: "abc",
1068+ DefaultLargeModelID: "a-large-model",
1069+ DefaultSmallModelID: "a-small-model",
1070+ Models: []provider.Model{
1071+ {
1072+ ID: "a-large-model",
1073+ DefaultMaxTokens: 1000,
1074+ },
1075+ {
1076+ ID: "a-small-model",
1077+ DefaultMaxTokens: 200,
1078+ },
1079+ },
1080+ },
1081+ }
1082+
1083+ cfg := &Config{
1084+ Models: map[SelectedModelType]SelectedModel{
1085+ "small": {
1086+ Model: "a-small-model",
1087+ Provider: "anthropic",
1088+ MaxTokens: 300,
1089+ },
1090+ },
1091+ }
1092+ cfg.setDefaults("/tmp")
1093+ env := env.NewFromMap(map[string]string{})
1094+ resolver := NewEnvironmentVariableResolver(env)
1095+ err := cfg.configureProviders(env, resolver, knownProviders)
1096+ assert.NoError(t, err)
1097+
1098+ err = cfg.configureSelectedModels(knownProviders)
1099+ assert.NoError(t, err)
1100+ large := cfg.Models[SelectedModelTypeLarge]
1101+ small := cfg.Models[SelectedModelTypeSmall]
1102+ assert.Equal(t, "large-model", large.Model)
1103+ assert.Equal(t, "openai", large.Provider)
1104+ assert.Equal(t, int64(1000), large.MaxTokens)
1105+ assert.Equal(t, "a-small-model", small.Model)
1106+ assert.Equal(t, "anthropic", small.Provider)
1107+ assert.Equal(t, int64(300), small.MaxTokens)
1108+ })
1109+
1110+ t.Run("should override the max tokens only", func(t *testing.T) {
1111+ knownProviders := []provider.Provider{
1112+ {
1113+ ID: "openai",
1114+ APIKey: "abc",
1115+ DefaultLargeModelID: "large-model",
1116+ DefaultSmallModelID: "small-model",
1117+ Models: []provider.Model{
1118+ {
1119+ ID: "large-model",
1120+ DefaultMaxTokens: 1000,
1121+ },
1122+ {
1123+ ID: "small-model",
1124+ DefaultMaxTokens: 500,
1125+ },
1126+ },
1127+ },
1128+ }
1129+
1130+ cfg := &Config{
1131+ Models: map[SelectedModelType]SelectedModel{
1132+ "large": {
1133+ MaxTokens: 100,
1134+ },
1135+ },
1136+ }
1137+ cfg.setDefaults("/tmp")
1138+ env := env.NewFromMap(map[string]string{})
1139+ resolver := NewEnvironmentVariableResolver(env)
1140+ err := cfg.configureProviders(env, resolver, knownProviders)
1141+ assert.NoError(t, err)
1142+
1143+ err = cfg.configureSelectedModels(knownProviders)
1144+ assert.NoError(t, err)
1145+ large := cfg.Models[SelectedModelTypeLarge]
1146+ assert.Equal(t, "large-model", large.Model)
1147+ assert.Equal(t, "openai", large.Provider)
1148+ assert.Equal(t, int64(100), large.MaxTokens)
1149+ })
1150+}
internal/config/merge.gonew file+16
@@ -0,0 +1,16 @@
1+package config
2+
3+import (
4+ "bytes"
5+ "io"
6+
7+ "github.com/qjebbs/go-jsons"
8+)
9+
10+func Merge(data []io.Reader) (io.Reader, error) {
11+ got, err := jsons.Merge(data)
12+ if err != nil {
13+ return nil, err
14+ }
15+ return bytes.NewReader(got), nil
16+}
internal/config/merge_test.gonew file+27
@@ -0,0 +1,27 @@
1+package config
2+
3+import (
4+ "io"
5+ "strings"
6+ "testing"
7+)
8+
9+func TestMerge(t *testing.T) {
10+ data1 := strings.NewReader(`{"foo": "bar"}`)
11+ data2 := strings.NewReader(`{"baz": "qux"}`)
12+
13+ merged, err := Merge([]io.Reader{data1, data2})
14+ if err != nil {
15+ t.Fatalf("expected no error, got %v", err)
16+ }
17+
18+ expected := `{"baz":"qux","foo":"bar"}`
19+ got, err := io.ReadAll(merged)
20+ if err != nil {
21+ t.Fatalf("expected no error reading merged data, got %v", err)
22+ }
23+
24+ if string(got) != expected {
25+ t.Errorf("expected %s, got %s", expected, string(got))
26+ }
27+}
internal/config/provider.go+51−36
@@ -4,27 +4,44 @@ import (
44 "encoding/json"
55 "os"
66 "path/filepath"
7+ "runtime"
78 "sync"
89
910 "github.com/charmbracelet/crush/internal/fur/client"
1011 "github.com/charmbracelet/crush/internal/fur/provider"
1112)
1213
13−var fur = client.New()
14+type ProviderClient interface {
15+ GetProviders() ([]provider.Provider, error)
16+}
1417
1518var (
16− providerOnc sync.Once // Ensures the initialization happens only once
19+ providerOnce sync.Once
1720 providerList []provider.Provider
18− // UseMockProviders can be set to true in tests to avoid API calls
19− UseMockProviders bool
2021)
2122
22−func providersPath() string {
23− return filepath.Join(baseDataPath(), "providers.json")
23+// file to cache provider data
24+func providerCacheFileData() string {
25+ xdgDataHome := os.Getenv("XDG_DATA_HOME")
26+ if xdgDataHome != "" {
27+ return filepath.Join(xdgDataHome, appName, "providers.json")
28+ }
29+
30+ // return the path to the main data directory
31+ // for windows, it should be in `%LOCALAPPDATA%/crush/`
32+ // for linux and macOS, it should be in `$HOME/.local/share/crush/`
33+ if runtime.GOOS == "windows" {
34+ localAppData := os.Getenv("LOCALAPPDATA")
35+ if localAppData == "" {
36+ localAppData = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local")
37+ }
38+ return filepath.Join(localAppData, appName, "providers.json")
39+ }
40+
41+ return filepath.Join(os.Getenv("HOME"), ".local", "share", appName, "providers.json")
2442}
2543
26−func saveProviders(providers []provider.Provider) error {
27− path := providersPath()
44+func saveProvidersInCache(path string, providers []provider.Provider) error {
2845 dir := filepath.Dir(path)
2946 if err := os.MkdirAll(dir, 0o755); err != nil {
3047 return err
@@ -38,8 +55,7 @@ func saveProviders(providers []provider.Provider) error {
3855 return os.WriteFile(path, data, 0o644)
3956}
4057
41−func loadProviders() ([]provider.Provider, error) {
42− path := providersPath()
58+func loadProvidersFromCache(path string) ([]provider.Provider, error) {
4359 data, err := os.ReadFile(path)
4460 if err != nil {
4561 return nil, err
@@ -50,34 +66,33 @@ func loadProviders() ([]provider.Provider, error) {
5066 return providers, err
5167}
5268
53−func Providers() []provider.Provider {
54− providerOnc.Do(func() {
55− // Use mock providers when testing
56− if UseMockProviders {
57− providerList = MockProviders()
58− return
69+func loadProviders(path string, client ProviderClient) ([]provider.Provider, error) {
70+ providers, err := client.GetProviders()
71+ if err != nil {
72+ fallbackToCache, err := loadProvidersFromCache(path)
73+ if err != nil {
74+ return nil, err
5975 }
60−
61− // Try to get providers from upstream API
62− if providers, err := fur.GetProviders(); err == nil {
63− providerList = providers
64− // Save providers locally for future fallback
65− _ = saveProviders(providers)
66− } else {
67− // If upstream fails, try to load from local cache
68− if localProviders, localErr := loadProviders(); localErr == nil {
69− providerList = localProviders
70− } else {
71− // If both fail, return empty list
72− providerList = []provider.Provider{}
73− }
76+ providers = fallbackToCache
77+ } else {
78+ if err := saveProvidersInCache(path, providerList); err != nil {
79+ return nil, err
7480 }
75− })
76− return providerList
81+ }
82+ return providers, nil
83+}
84+
85+func Providers() ([]provider.Provider, error) {
86+ return LoadProviders(client.New())
7787}
7888
79−// ResetProviders resets the provider cache. Useful for testing.
80−func ResetProviders() {
81− providerOnc = sync.Once{}
82− providerList = nil
89+func LoadProviders(client ProviderClient) ([]provider.Provider, error) {
90+ var err error
91+ providerOnce.Do(func() {
92+ providerList, err = loadProviders(providerCacheFileData(), client)
93+ })
94+ if err != nil {
95+ return nil, err
96+ }
97+ return providerList, nil
8398}
internal/config/provider_mock.godeleted−293
@@ -1,293 +0,0 @@
1−package config
2−
3−import (
4− "github.com/charmbracelet/crush/internal/fur/provider"
5−)
6−
7−// MockProviders returns a mock list of providers for testing.
8−// This avoids making API calls during tests and provides consistent test data.
9−// Simplified version with only default models from each provider.
10−func MockProviders() []provider.Provider {
11− return []provider.Provider{
12− {
13− Name: "Anthropic",
14− ID: provider.InferenceProviderAnthropic,
15− APIKey: "$ANTHROPIC_API_KEY",
16− APIEndpoint: "$ANTHROPIC_API_ENDPOINT",
17− Type: provider.TypeAnthropic,
18− DefaultLargeModelID: "claude-sonnet-4-20250514",
19− DefaultSmallModelID: "claude-3-5-haiku-20241022",
20− Models: []provider.Model{
21− {
22− ID: "claude-sonnet-4-20250514",
23− Name: "Claude Sonnet 4",
24− CostPer1MIn: 3.0,
25− CostPer1MOut: 15.0,
26− CostPer1MInCached: 3.75,
27− CostPer1MOutCached: 0.3,
28− ContextWindow: 200000,
29− DefaultMaxTokens: 50000,
30− CanReason: true,
31− SupportsImages: true,
32− },
33− {
34− ID: "claude-3-5-haiku-20241022",
35− Name: "Claude 3.5 Haiku",
36− CostPer1MIn: 0.8,
37− CostPer1MOut: 4.0,
38− CostPer1MInCached: 1.0,
39− CostPer1MOutCached: 0.08,
40− ContextWindow: 200000,
41− DefaultMaxTokens: 5000,
42− CanReason: false,
43− SupportsImages: true,
44− },
45− },
46− },
47− {
48− Name: "OpenAI",
49− ID: provider.InferenceProviderOpenAI,
50− APIKey: "$OPENAI_API_KEY",
51− APIEndpoint: "$OPENAI_API_ENDPOINT",
52− Type: provider.TypeOpenAI,
53− DefaultLargeModelID: "codex-mini-latest",
54− DefaultSmallModelID: "gpt-4o",
55− Models: []provider.Model{
56− {
57− ID: "codex-mini-latest",
58− Name: "Codex Mini",
59− CostPer1MIn: 1.5,
60− CostPer1MOut: 6.0,
61− CostPer1MInCached: 0.0,
62− CostPer1MOutCached: 0.375,
63− ContextWindow: 200000,
64− DefaultMaxTokens: 50000,
65− CanReason: true,
66− HasReasoningEffort: true,
67− DefaultReasoningEffort: "medium",
68− SupportsImages: true,
69− },
70− {
71− ID: "gpt-4o",
72− Name: "GPT-4o",
73− CostPer1MIn: 2.5,
74− CostPer1MOut: 10.0,
75− CostPer1MInCached: 0.0,
76− CostPer1MOutCached: 1.25,
77− ContextWindow: 128000,
78− DefaultMaxTokens: 20000,
79− CanReason: false,
80− SupportsImages: true,
81− },
82− },
83− },
84− {
85− Name: "Google Gemini",
86− ID: provider.InferenceProviderGemini,
87− APIKey: "$GEMINI_API_KEY",
88− APIEndpoint: "$GEMINI_API_ENDPOINT",
89− Type: provider.TypeGemini,
90− DefaultLargeModelID: "gemini-2.5-pro",
91− DefaultSmallModelID: "gemini-2.5-flash",
92− Models: []provider.Model{
93− {
94− ID: "gemini-2.5-pro",
95− Name: "Gemini 2.5 Pro",
96− CostPer1MIn: 1.25,
97− CostPer1MOut: 10.0,
98− CostPer1MInCached: 1.625,
99− CostPer1MOutCached: 0.31,
100− ContextWindow: 1048576,
101− DefaultMaxTokens: 50000,
102− CanReason: true,
103− SupportsImages: true,
104− },
105− {
106− ID: "gemini-2.5-flash",
107− Name: "Gemini 2.5 Flash",
108− CostPer1MIn: 0.3,
109− CostPer1MOut: 2.5,
110− CostPer1MInCached: 0.3833,
111− CostPer1MOutCached: 0.075,
112− ContextWindow: 1048576,
113− DefaultMaxTokens: 50000,
114− CanReason: true,
115− SupportsImages: true,
116− },
117− },
118− },
119− {
120− Name: "xAI",
121− ID: provider.InferenceProviderXAI,
122− APIKey: "$XAI_API_KEY",
123− APIEndpoint: "https://api.x.ai/v1",
124− Type: provider.TypeXAI,
125− DefaultLargeModelID: "grok-3",
126− DefaultSmallModelID: "grok-3-mini",
127− Models: []provider.Model{
128− {
129− ID: "grok-3",
130− Name: "Grok 3",
131− CostPer1MIn: 3.0,
132− CostPer1MOut: 15.0,
133− CostPer1MInCached: 0.0,
134− CostPer1MOutCached: 0.75,
135− ContextWindow: 131072,
136− DefaultMaxTokens: 20000,
137− CanReason: false,
138− SupportsImages: false,
139− },
140− {
141− ID: "grok-3-mini",
142− Name: "Grok 3 Mini",
143− CostPer1MIn: 0.3,
144− CostPer1MOut: 0.5,
145− CostPer1MInCached: 0.0,
146− CostPer1MOutCached: 0.075,
147− ContextWindow: 131072,
148− DefaultMaxTokens: 20000,
149− CanReason: true,
150− SupportsImages: false,
151− },
152− },
153− },
154− {
155− Name: "Azure OpenAI",
156− ID: provider.InferenceProviderAzure,
157− APIKey: "$AZURE_OPENAI_API_KEY",
158− APIEndpoint: "$AZURE_OPENAI_API_ENDPOINT",
159− Type: provider.TypeAzure,
160− DefaultLargeModelID: "o4-mini",
161− DefaultSmallModelID: "gpt-4o",
162− Models: []provider.Model{
163− {
164− ID: "o4-mini",
165− Name: "o4 Mini",
166− CostPer1MIn: 1.1,
167− CostPer1MOut: 4.4,
168− CostPer1MInCached: 0.0,
169− CostPer1MOutCached: 0.275,
170− ContextWindow: 200000,
171− DefaultMaxTokens: 50000,
172− CanReason: true,
173− HasReasoningEffort: false,
174− DefaultReasoningEffort: "medium",
175− SupportsImages: true,
176− },
177− {
178− ID: "gpt-4o",
179− Name: "GPT-4o",
180− CostPer1MIn: 2.5,
181− CostPer1MOut: 10.0,
182− CostPer1MInCached: 0.0,
183− CostPer1MOutCached: 1.25,
184− ContextWindow: 128000,
185− DefaultMaxTokens: 20000,
186− CanReason: false,
187− SupportsImages: true,
188− },
189− },
190− },
191− {
192− Name: "AWS Bedrock",
193− ID: provider.InferenceProviderBedrock,
194− Type: provider.TypeBedrock,
195− DefaultLargeModelID: "anthropic.claude-sonnet-4-20250514-v1:0",
196− DefaultSmallModelID: "anthropic.claude-3-5-haiku-20241022-v1:0",
197− Models: []provider.Model{
198− {
199− ID: "anthropic.claude-sonnet-4-20250514-v1:0",
200− Name: "AWS Claude Sonnet 4",
201− CostPer1MIn: 3.0,
202− CostPer1MOut: 15.0,
203− CostPer1MInCached: 3.75,
204− CostPer1MOutCached: 0.3,
205− ContextWindow: 200000,
206− DefaultMaxTokens: 50000,
207− CanReason: true,
208− SupportsImages: true,
209− },
210− {
211− ID: "anthropic.claude-3-5-haiku-20241022-v1:0",
212− Name: "AWS Claude 3.5 Haiku",
213− CostPer1MIn: 0.8,
214− CostPer1MOut: 4.0,
215− CostPer1MInCached: 1.0,
216− CostPer1MOutCached: 0.08,
217− ContextWindow: 200000,
218− DefaultMaxTokens: 50000,
219− CanReason: false,
220− SupportsImages: true,
221− },
222− },
223− },
224− {
225− Name: "Google Vertex AI",
226− ID: provider.InferenceProviderVertexAI,
227− Type: provider.TypeVertexAI,
228− DefaultLargeModelID: "gemini-2.5-pro",
229− DefaultSmallModelID: "gemini-2.5-flash",
230− Models: []provider.Model{
231− {
232− ID: "gemini-2.5-pro",
233− Name: "Gemini 2.5 Pro",
234− CostPer1MIn: 1.25,
235− CostPer1MOut: 10.0,
236− CostPer1MInCached: 1.625,
237− CostPer1MOutCached: 0.31,
238− ContextWindow: 1048576,
239− DefaultMaxTokens: 50000,
240− CanReason: true,
241− SupportsImages: true,
242− },
243− {
244− ID: "gemini-2.5-flash",
245− Name: "Gemini 2.5 Flash",
246− CostPer1MIn: 0.3,
247− CostPer1MOut: 2.5,
248− CostPer1MInCached: 0.3833,
249− CostPer1MOutCached: 0.075,
250− ContextWindow: 1048576,
251− DefaultMaxTokens: 50000,
252− CanReason: true,
253− SupportsImages: true,
254− },
255− },
256− },
257− {
258− Name: "OpenRouter",
259− ID: provider.InferenceProviderOpenRouter,
260− APIKey: "$OPENROUTER_API_KEY",
261− APIEndpoint: "https://openrouter.ai/api/v1",
262− Type: provider.TypeOpenAI,
263− DefaultLargeModelID: "anthropic/claude-sonnet-4",
264− DefaultSmallModelID: "anthropic/claude-haiku-3.5",
265− Models: []provider.Model{
266− {
267− ID: "anthropic/claude-sonnet-4",
268− Name: "Anthropic: Claude Sonnet 4",
269− CostPer1MIn: 3.0,
270− CostPer1MOut: 15.0,
271− CostPer1MInCached: 3.75,
272− CostPer1MOutCached: 0.3,
273− ContextWindow: 200000,
274− DefaultMaxTokens: 32000,
275− CanReason: true,
276− SupportsImages: true,
277− },
278− {
279− ID: "anthropic/claude-haiku-3.5",
280− Name: "Anthropic: Claude 3.5 Haiku",
281− CostPer1MIn: 0.8,
282− CostPer1MOut: 4.0,
283− CostPer1MInCached: 1.0,
284− CostPer1MOutCached: 0.08,
285− ContextWindow: 200000,
286− DefaultMaxTokens: 4096,
287− CanReason: false,
288− SupportsImages: true,
289− },
290− },
291− },
292− }
293−}
internal/config/provider_test.go+52−60
@@ -1,81 +1,73 @@
11package config
22
33import (
4+ "encoding/json"
5+ "errors"
6+ "os"
47 "testing"
58
69 "github.com/charmbracelet/crush/internal/fur/provider"
710 "github.com/stretchr/testify/assert"
8− "github.com/stretchr/testify/require"
911)
1012
11−func TestProviders_MockEnabled(t *testing.T) {
12− originalUseMock := UseMockProviders
13− UseMockProviders = true
14− defer func() {
15− UseMockProviders = originalUseMock
16− ResetProviders()
17− }()
18−
19− ResetProviders()
20− providers := Providers()
21− require.NotEmpty(t, providers)
13+type mockProviderClient struct {
14+ shouldFail bool
15+}
2216
23− providerIDs := make(map[provider.InferenceProvider]bool)
24− for _, p := range providers {
25− providerIDs[p.ID] = true
17+func (m *mockProviderClient) GetProviders() ([]provider.Provider, error) {
18+ if m.shouldFail {
19+ return nil, errors.New("failed to load providers")
2620 }
27−
28− assert.True(t, providerIDs[provider.InferenceProviderAnthropic])
29− assert.True(t, providerIDs[provider.InferenceProviderOpenAI])
30− assert.True(t, providerIDs[provider.InferenceProviderGemini])
21+ return []provider.Provider{
22+ {
23+ Name: "Mock",
24+ },
25+ }, nil
3126}
3227
33−func TestProviders_ResetFunctionality(t *testing.T) {
34− UseMockProviders = true
35− defer func() {
36− UseMockProviders = false
37− ResetProviders()
38− }()
39−
40− providers1 := Providers()
41− require.NotEmpty(t, providers1)
42−
43− ResetProviders()
44− providers2 := Providers()
45− require.NotEmpty(t, providers2)
28+func TestProvider_loadProvidersNoIssues(t *testing.T) {
29+ client := &mockProviderClient{shouldFail: false}
30+ tmpPath := t.TempDir() + "/providers.json"
31+ providers, err := loadProviders(tmpPath, client)
32+ assert.NoError(t, err)
33+ assert.NotNil(t, providers)
34+ assert.Len(t, providers, 1)
4635
47− assert.Equal(t, len(providers1), len(providers2))
36+ // check if file got saved
37+ fileInfo, err := os.Stat(tmpPath)
38+ assert.NoError(t, err)
39+ assert.False(t, fileInfo.IsDir(), "Expected a file, not a directory")
4840}
4941
50−func TestProviders_ModelCapabilities(t *testing.T) {
51− originalUseMock := UseMockProviders
52− UseMockProviders = true
53− defer func() {
54− UseMockProviders = originalUseMock
55− ResetProviders()
56− }()
57−
58− ResetProviders()
59− providers := Providers()
60−
61− var openaiProvider provider.Provider
62− for _, p := range providers {
63− if p.ID == provider.InferenceProviderOpenAI {
64− openaiProvider = p
65− break
66− }
42+func TestProvider_loadProvidersWithIssues(t *testing.T) {
43+ client := &mockProviderClient{shouldFail: true}
44+ tmpPath := t.TempDir() + "/providers.json"
45+ // store providers to a temporary file
46+ oldProviders := []provider.Provider{
47+ {
48+ Name: "OldProvider",
49+ },
50+ }
51+ data, err := json.Marshal(oldProviders)
52+ if err != nil {
53+ t.Fatalf("Failed to marshal old providers: %v", err)
6754 }
68− require.NotEmpty(t, openaiProvider.ID)
6955
70− var foundReasoning, foundNonReasoning bool
71− for _, model := range openaiProvider.Models {
72− if model.CanReason && model.HasReasoningEffort {
73− foundReasoning = true
74− } else if !model.CanReason {
75− foundNonReasoning = true
76− }
56+ err = os.WriteFile(tmpPath, data, 0o644)
57+ if err != nil {
58+ t.Fatalf("Failed to write old providers to file: %v", err)
7759 }
60+ providers, err := loadProviders(tmpPath, client)
61+ assert.NoError(t, err)
62+ assert.NotNil(t, providers)
63+ assert.Len(t, providers, 1)
64+ assert.Equal(t, "OldProvider", providers[0].Name, "Expected to keep old provider when loading fails")
65+}
7866
79− assert.True(t, foundReasoning)
80− assert.True(t, foundNonReasoning)
67+func TestProvider_loadProvidersWithIssuesAndNoCache(t *testing.T) {
68+ client := &mockProviderClient{shouldFail: true}
69+ tmpPath := t.TempDir() + "/providers.json"
70+ providers, err := loadProviders(tmpPath, client)
71+ assert.Error(t, err)
72+ assert.Nil(t, providers, "Expected nil providers when loading fails and no cache exists")
8173}
internal/config/resolve.gonew file+90
@@ -0,0 +1,90 @@
1+package config
2+
3+import (
4+ "context"
5+ "fmt"
6+ "strings"
7+ "time"
8+
9+ "github.com/charmbracelet/crush/internal/env"
10+ "github.com/charmbracelet/crush/internal/shell"
11+)
12+
13+type VariableResolver interface {
14+ ResolveValue(value string) (string, error)
15+}
16+
17+type Shell interface {
18+ Exec(ctx context.Context, command string) (stdout, stderr string, err error)
19+}
20+
21+type shellVariableResolver struct {
22+ shell Shell
23+ env env.Env
24+}
25+
26+func NewShellVariableResolver(env env.Env) VariableResolver {
27+ return &shellVariableResolver{
28+ env: env,
29+ shell: shell.NewShell(
30+ &shell.Options{
31+ Env: env.Env(),
32+ },
33+ ),
34+ }
35+}
36+
37+// ResolveValue is a method for resolving values, such as environment variables.
38+// it will expect strings that start with `$` to be resolved as environment variables or shell commands.
39+// if the string does not start with `$`, it will return the string as is.
40+func (r *shellVariableResolver) ResolveValue(value string) (string, error) {
41+ if !strings.HasPrefix(value, "$") {
42+ return value, nil
43+ }
44+
45+ if strings.HasPrefix(value, "$(") && strings.HasSuffix(value, ")") {
46+ command := strings.TrimSuffix(strings.TrimPrefix(value, "$("), ")")
47+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
48+ defer cancel()
49+
50+ stdout, _, err := r.shell.Exec(ctx, command)
51+ if err != nil {
52+ return "", fmt.Errorf("command execution failed: %w", err)
53+ }
54+ return strings.TrimSpace(stdout), nil
55+ }
56+
57+ if strings.HasPrefix(value, "$") {
58+ varName := strings.TrimPrefix(value, "$")
59+ value = r.env.Get(varName)
60+ if value == "" {
61+ return "", fmt.Errorf("environment variable %q not set", varName)
62+ }
63+ return value, nil
64+ }
65+ return "", fmt.Errorf("invalid value format: %s", value)
66+}
67+
68+type environmentVariableResolver struct {
69+ env env.Env
70+}
71+
72+func NewEnvironmentVariableResolver(env env.Env) VariableResolver {
73+ return &environmentVariableResolver{
74+ env: env,
75+ }
76+}
77+
78+// ResolveValue resolves environment variables from the provided env.Env.
79+func (r *environmentVariableResolver) ResolveValue(value string) (string, error) {
80+ if !strings.HasPrefix(value, "$") {
81+ return value, nil
82+ }
83+
84+ varName := strings.TrimPrefix(value, "$")
85+ resolvedValue := r.env.Get(varName)
86+ if resolvedValue == "" {
87+ return "", fmt.Errorf("environment variable %q not set", varName)
88+ }
89+ return resolvedValue, nil
90+}
internal/config/resolve_test.gonew file+177
@@ -0,0 +1,177 @@
1+package config
2+
3+import (
4+ "context"
5+ "errors"
6+ "testing"
7+
8+ "github.com/charmbracelet/crush/internal/env"
9+ "github.com/stretchr/testify/assert"
10+)
11+
12+// mockShell implements the Shell interface for testing
13+type mockShell struct {
14+ execFunc func(ctx context.Context, command string) (stdout, stderr string, err error)
15+}
16+
17+func (m *mockShell) Exec(ctx context.Context, command string) (stdout, stderr string, err error) {
18+ if m.execFunc != nil {
19+ return m.execFunc(ctx, command)
20+ }
21+ return "", "", nil
22+}
23+
24+func TestShellVariableResolver_ResolveValue(t *testing.T) {
25+ tests := []struct {
26+ name string
27+ value string
28+ envVars map[string]string
29+ shellFunc func(ctx context.Context, command string) (stdout, stderr string, err error)
30+ expected string
31+ expectError bool
32+ }{
33+ {
34+ name: "non-variable string returns as-is",
35+ value: "plain-string",
36+ expected: "plain-string",
37+ },
38+ {
39+ name: "environment variable resolution",
40+ value: "$HOME",
41+ envVars: map[string]string{"HOME": "/home/user"},
42+ expected: "/home/user",
43+ },
44+ {
45+ name: "missing environment variable returns error",
46+ value: "$MISSING_VAR",
47+ envVars: map[string]string{},
48+ expectError: true,
49+ },
50+ {
51+ name: "shell command execution",
52+ value: "$(echo hello)",
53+ shellFunc: func(ctx context.Context, command string) (stdout, stderr string, err error) {
54+ if command == "echo hello" {
55+ return "hello\n", "", nil
56+ }
57+ return "", "", errors.New("unexpected command")
58+ },
59+ expected: "hello",
60+ },
61+ {
62+ name: "shell command with whitespace trimming",
63+ value: "$(echo ' spaced ')",
64+ shellFunc: func(ctx context.Context, command string) (stdout, stderr string, err error) {
65+ if command == "echo ' spaced '" {
66+ return " spaced \n", "", nil
67+ }
68+ return "", "", errors.New("unexpected command")
69+ },
70+ expected: "spaced",
71+ },
72+ {
73+ name: "shell command execution error",
74+ value: "$(false)",
75+ shellFunc: func(ctx context.Context, command string) (stdout, stderr string, err error) {
76+ return "", "", errors.New("command failed")
77+ },
78+ expectError: true,
79+ },
80+ {
81+ name: "invalid format returns error",
82+ value: "$",
83+ expectError: true,
84+ },
85+ }
86+
87+ for _, tt := range tests {
88+ t.Run(tt.name, func(t *testing.T) {
89+ testEnv := env.NewFromMap(tt.envVars)
90+ resolver := &shellVariableResolver{
91+ shell: &mockShell{execFunc: tt.shellFunc},
92+ env: testEnv,
93+ }
94+
95+ result, err := resolver.ResolveValue(tt.value)
96+
97+ if tt.expectError {
98+ assert.Error(t, err)
99+ } else {
100+ assert.NoError(t, err)
101+ assert.Equal(t, tt.expected, result)
102+ }
103+ })
104+ }
105+}
106+
107+func TestEnvironmentVariableResolver_ResolveValue(t *testing.T) {
108+ tests := []struct {
109+ name string
110+ value string
111+ envVars map[string]string
112+ expected string
113+ expectError bool
114+ }{
115+ {
116+ name: "non-variable string returns as-is",
117+ value: "plain-string",
118+ expected: "plain-string",
119+ },
120+ {
121+ name: "environment variable resolution",
122+ value: "$HOME",
123+ envVars: map[string]string{"HOME": "/home/user"},
124+ expected: "/home/user",
125+ },
126+ {
127+ name: "environment variable with complex value",
128+ value: "$PATH",
129+ envVars: map[string]string{"PATH": "/usr/bin:/bin:/usr/local/bin"},
130+ expected: "/usr/bin:/bin:/usr/local/bin",
131+ },
132+ {
133+ name: "missing environment variable returns error",
134+ value: "$MISSING_VAR",
135+ envVars: map[string]string{},
136+ expectError: true,
137+ },
138+ {
139+ name: "empty environment variable returns error",
140+ value: "$EMPTY_VAR",
141+ envVars: map[string]string{"EMPTY_VAR": ""},
142+ expectError: true,
143+ },
144+ }
145+
146+ for _, tt := range tests {
147+ t.Run(tt.name, func(t *testing.T) {
148+ testEnv := env.NewFromMap(tt.envVars)
149+ resolver := NewEnvironmentVariableResolver(testEnv)
150+
151+ result, err := resolver.ResolveValue(tt.value)
152+
153+ if tt.expectError {
154+ assert.Error(t, err)
155+ } else {
156+ assert.NoError(t, err)
157+ assert.Equal(t, tt.expected, result)
158+ }
159+ })
160+ }
161+}
162+
163+func TestNewShellVariableResolver(t *testing.T) {
164+ testEnv := env.NewFromMap(map[string]string{"TEST": "value"})
165+ resolver := NewShellVariableResolver(testEnv)
166+
167+ assert.NotNil(t, resolver)
168+ assert.Implements(t, (*VariableResolver)(nil), resolver)
169+}
170+
171+func TestNewEnvironmentVariableResolver(t *testing.T) {
172+ testEnv := env.NewFromMap(map[string]string{"TEST": "value"})
173+ resolver := NewEnvironmentVariableResolver(testEnv)
174+
175+ assert.NotNil(t, resolver)
176+ assert.Implements(t, (*VariableResolver)(nil), resolver)
177+}
internal/config/shell.godeleted−73
@@ -1,73 +0,0 @@
1−package config
2−
3−import (
4− "context"
5− "fmt"
6− "os"
7− "strings"
8− "time"
9−
10− "github.com/charmbracelet/crush/internal/logging"
11− "github.com/charmbracelet/crush/internal/shell"
12−)
13−
14−// ExecuteCommand executes a shell command and returns the output
15−// This is a shared utility that can be used by both provider config and tools
16−func ExecuteCommand(ctx context.Context, command string, workingDir string) (string, error) {
17− if workingDir == "" {
18− workingDir = WorkingDirectory()
19− }
20−
21− persistentShell := shell.NewShell(&shell.Options{WorkingDir: workingDir})
22−
23− stdout, stderr, err := persistentShell.Exec(ctx, command)
24− if err != nil {
25− logging.Debug("Command execution failed", "command", command, "error", err, "stderr", stderr)
26− return "", fmt.Errorf("command execution failed: %w", err)
27− }
28−
29− return strings.TrimSpace(stdout), nil
30−}
31−
32−// ResolveAPIKey resolves an API key that can be either:
33−// - A direct string value
34−// - An environment variable (prefixed with $)
35−// - A shell command (wrapped in $(...))
36−func ResolveAPIKey(apiKey string) (string, error) {
37− if !strings.HasPrefix(apiKey, "$") {
38− return apiKey, nil
39− }
40−
41− if strings.HasPrefix(apiKey, "$(") && strings.HasSuffix(apiKey, ")") {
42− command := strings.TrimSuffix(strings.TrimPrefix(apiKey, "$("), ")")
43− logging.Debug("Resolving API key from command", "command", command)
44− return resolveCommandAPIKey(command)
45− }
46−
47− envVar := strings.TrimPrefix(apiKey, "$")
48− if value := os.Getenv(envVar); value != "" {
49− logging.Debug("Resolved environment variable", "envVar", envVar, "value", value)
50− return value, nil
51− }
52−
53− logging.Debug("Environment variable not found", "envVar", envVar)
54−
55− return "", fmt.Errorf("environment variable %s not found", envVar)
56−}
57−
58−// resolveCommandAPIKey executes a command to get an API key, with caching support
59−func resolveCommandAPIKey(command string) (string, error) {
60− ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
61− defer cancel()
62−
63− logging.Debug("Executing command for API key", "command", command)
64−
65− workingDir := WorkingDirectory()
66−
67− result, err := ExecuteCommand(ctx, command, workingDir)
68− if err != nil {
69− return "", fmt.Errorf("failed to execute API key command: %w", err)
70− }
71− logging.Debug("Command executed successfully", "command", command, "result", result)
72− return result, nil
73−}
internal/config/validation_test.godeleted−462
@@ -1,462 +0,0 @@
1−package config
2−
3−import (
4− "testing"
5−
6− "github.com/charmbracelet/crush/internal/fur/provider"
7− "github.com/stretchr/testify/assert"
8− "github.com/stretchr/testify/require"
9−)
10−
11−func TestConfig_Validate_ValidConfig(t *testing.T) {
12− cfg := &Config{
13− Models: PreferredModels{
14− Large: PreferredModel{
15− ModelID: "gpt-4",
16− Provider: provider.InferenceProviderOpenAI,
17− },
18− Small: PreferredModel{
19− ModelID: "gpt-3.5-turbo",
20− Provider: provider.InferenceProviderOpenAI,
21− },
22− },
23− Providers: map[provider.InferenceProvider]ProviderConfig{
24− provider.InferenceProviderOpenAI: {
25− ID: provider.InferenceProviderOpenAI,
26− APIKey: "test-key",
27− ProviderType: provider.TypeOpenAI,
28− DefaultLargeModel: "gpt-4",
29− DefaultSmallModel: "gpt-3.5-turbo",
30− Models: []Model{
31− {
32− ID: "gpt-4",
33− Name: "GPT-4",
34− ContextWindow: 8192,
35− DefaultMaxTokens: 4096,
36− CostPer1MIn: 30.0,
37− CostPer1MOut: 60.0,
38− },
39− {
40− ID: "gpt-3.5-turbo",
41− Name: "GPT-3.5 Turbo",
42− ContextWindow: 4096,
43− DefaultMaxTokens: 2048,
44− CostPer1MIn: 1.5,
45− CostPer1MOut: 2.0,
46− },
47− },
48− },
49− },
50− Agents: map[AgentID]Agent{
51− AgentCoder: {
52− ID: AgentCoder,
53− Name: "Coder",
54− Description: "An agent that helps with executing coding tasks.",
55− Model: LargeModel,
56− ContextPaths: []string{"CRUSH.md"},
57− },
58− AgentTask: {
59− ID: AgentTask,
60− Name: "Task",
61− Description: "An agent that helps with searching for context and finding implementation details.",
62− Model: LargeModel,
63− ContextPaths: []string{"CRUSH.md"},
64− AllowedTools: []string{"glob", "grep", "ls", "sourcegraph", "view"},
65− AllowedMCP: map[string][]string{},
66− AllowedLSP: []string{},
67− },
68− },
69− MCP: map[string]MCP{},
70− LSP: map[string]LSPConfig{},
71− Options: Options{
72− DataDirectory: ".crush",
73− ContextPaths: []string{"CRUSH.md"},
74− },
75− }
76−
77− err := cfg.Validate()
78− assert.NoError(t, err)
79−}
80−
81−func TestConfig_Validate_MissingAPIKey(t *testing.T) {
82− cfg := &Config{
83− Providers: map[provider.InferenceProvider]ProviderConfig{
84− provider.InferenceProviderOpenAI: {
85− ID: provider.InferenceProviderOpenAI,
86− ProviderType: provider.TypeOpenAI,
87− // Missing APIKey
88− },
89− },
90− Options: Options{
91− DataDirectory: ".crush",
92− ContextPaths: []string{"CRUSH.md"},
93− },
94− }
95−
96− err := cfg.Validate()
97− require.Error(t, err)
98− assert.Contains(t, err.Error(), "API key is required")
99−}
100−
101−func TestConfig_Validate_InvalidProviderType(t *testing.T) {
102− cfg := &Config{
103− Providers: map[provider.InferenceProvider]ProviderConfig{
104− provider.InferenceProviderOpenAI: {
105− ID: provider.InferenceProviderOpenAI,
106− APIKey: "test-key",
107− ProviderType: provider.Type("invalid"),
108− },
109− },
110− Options: Options{
111− DataDirectory: ".crush",
112− ContextPaths: []string{"CRUSH.md"},
113− },
114− }
115−
116− err := cfg.Validate()
117− require.Error(t, err)
118− assert.Contains(t, err.Error(), "invalid provider type")
119−}
120−
121−func TestConfig_Validate_CustomProviderMissingBaseURL(t *testing.T) {
122− customProvider := provider.InferenceProvider("custom-provider")
123− cfg := &Config{
124− Providers: map[provider.InferenceProvider]ProviderConfig{
125− customProvider: {
126− ID: customProvider,
127− APIKey: "test-key",
128− ProviderType: provider.TypeOpenAI,
129− // Missing BaseURL for custom provider
130− },
131− },
132− Options: Options{
133− DataDirectory: ".crush",
134− ContextPaths: []string{"CRUSH.md"},
135− },
136− }
137−
138− err := cfg.Validate()
139− require.Error(t, err)
140− assert.Contains(t, err.Error(), "BaseURL is required for custom providers")
141−}
142−
143−func TestConfig_Validate_DuplicateModelIDs(t *testing.T) {
144− cfg := &Config{
145− Providers: map[provider.InferenceProvider]ProviderConfig{
146− provider.InferenceProviderOpenAI: {
147− ID: provider.InferenceProviderOpenAI,
148− APIKey: "test-key",
149− ProviderType: provider.TypeOpenAI,
150− Models: []Model{
151− {
152− ID: "gpt-4",
153− Name: "GPT-4",
154− ContextWindow: 8192,
155− DefaultMaxTokens: 4096,
156− },
157− {
158− ID: "gpt-4", // Duplicate ID
159− Name: "GPT-4 Duplicate",
160− ContextWindow: 8192,
161− DefaultMaxTokens: 4096,
162− },
163− },
164− },
165− },
166− Options: Options{
167− DataDirectory: ".crush",
168− ContextPaths: []string{"CRUSH.md"},
169− },
170− }
171−
172− err := cfg.Validate()
173− require.Error(t, err)
174− assert.Contains(t, err.Error(), "duplicate model ID")
175−}
176−
177−func TestConfig_Validate_InvalidModelFields(t *testing.T) {
178− cfg := &Config{
179− Providers: map[provider.InferenceProvider]ProviderConfig{
180− provider.InferenceProviderOpenAI: {
181− ID: provider.InferenceProviderOpenAI,
182− APIKey: "test-key",
183− ProviderType: provider.TypeOpenAI,
184− Models: []Model{
185− {
186− ID: "", // Empty ID
187− Name: "GPT-4",
188− ContextWindow: 0, // Invalid context window
189− DefaultMaxTokens: -1, // Invalid max tokens
190− CostPer1MIn: -5.0, // Negative cost
191− },
192− },
193− },
194− },
195− Options: Options{
196− DataDirectory: ".crush",
197− ContextPaths: []string{"CRUSH.md"},
198− },
199− }
200−
201− err := cfg.Validate()
202− require.Error(t, err)
203− validationErr := err.(ValidationErrors)
204− assert.True(t, len(validationErr) >= 4) // Should have multiple validation errors
205−}
206−
207−func TestConfig_Validate_DefaultModelNotFound(t *testing.T) {
208− cfg := &Config{
209− Providers: map[provider.InferenceProvider]ProviderConfig{
210− provider.InferenceProviderOpenAI: {
211− ID: provider.InferenceProviderOpenAI,
212− APIKey: "test-key",
213− ProviderType: provider.TypeOpenAI,
214− DefaultLargeModel: "nonexistent-model",
215− Models: []Model{
216− {
217− ID: "gpt-4",
218− Name: "GPT-4",
219− ContextWindow: 8192,
220− DefaultMaxTokens: 4096,
221− },
222− },
223− },
224− },
225− Options: Options{
226− DataDirectory: ".crush",
227− ContextPaths: []string{"CRUSH.md"},
228− },
229− }
230−
231− err := cfg.Validate()
232− require.Error(t, err)
233− assert.Contains(t, err.Error(), "default large model 'nonexistent-model' not found")
234−}
235−
236−func TestConfig_Validate_AgentIDMismatch(t *testing.T) {
237− cfg := &Config{
238− Agents: map[AgentID]Agent{
239− AgentCoder: {
240− ID: AgentTask, // Wrong ID
241− Name: "Coder",
242− },
243− },
244− Options: Options{
245− DataDirectory: ".crush",
246− ContextPaths: []string{"CRUSH.md"},
247− },
248− }
249−
250− err := cfg.Validate()
251− require.Error(t, err)
252− assert.Contains(t, err.Error(), "agent ID mismatch")
253−}
254−
255−func TestConfig_Validate_InvalidAgentModelType(t *testing.T) {
256− cfg := &Config{
257− Agents: map[AgentID]Agent{
258− AgentCoder: {
259− ID: AgentCoder,
260− Name: "Coder",
261− Model: ModelType("invalid"),
262− },
263− },
264− Options: Options{
265− DataDirectory: ".crush",
266− ContextPaths: []string{"CRUSH.md"},
267− },
268− }
269−
270− err := cfg.Validate()
271− require.Error(t, err)
272− assert.Contains(t, err.Error(), "invalid model type")
273−}
274−
275−func TestConfig_Validate_UnknownTool(t *testing.T) {
276− cfg := &Config{
277− Agents: map[AgentID]Agent{
278− AgentID("custom-agent"): {
279− ID: AgentID("custom-agent"),
280− Name: "Custom Agent",
281− Model: LargeModel,
282− AllowedTools: []string{"unknown-tool"},
283− },
284− },
285− Options: Options{
286− DataDirectory: ".crush",
287− ContextPaths: []string{"CRUSH.md"},
288− },
289− }
290−
291− err := cfg.Validate()
292− require.Error(t, err)
293− assert.Contains(t, err.Error(), "unknown tool")
294−}
295−
296−func TestConfig_Validate_MCPReference(t *testing.T) {
297− cfg := &Config{
298− Agents: map[AgentID]Agent{
299− AgentID("custom-agent"): {
300− ID: AgentID("custom-agent"),
301− Name: "Custom Agent",
302− Model: LargeModel,
303− AllowedMCP: map[string][]string{"nonexistent-mcp": nil},
304− },
305− },
306− MCP: map[string]MCP{}, // Empty MCP map
307− Options: Options{
308− DataDirectory: ".crush",
309− ContextPaths: []string{"CRUSH.md"},
310− },
311− }
312−
313− err := cfg.Validate()
314− require.Error(t, err)
315− assert.Contains(t, err.Error(), "referenced MCP 'nonexistent-mcp' not found")
316−}
317−
318−func TestConfig_Validate_InvalidMCPType(t *testing.T) {
319− cfg := &Config{
320− MCP: map[string]MCP{
321− "test-mcp": {
322− Type: MCPType("invalid"),
323− },
324− },
325− Options: Options{
326− DataDirectory: ".crush",
327− ContextPaths: []string{"CRUSH.md"},
328− },
329− }
330−
331− err := cfg.Validate()
332− require.Error(t, err)
333− assert.Contains(t, err.Error(), "invalid MCP type")
334−}
335−
336−func TestConfig_Validate_MCPMissingCommand(t *testing.T) {
337− cfg := &Config{
338− MCP: map[string]MCP{
339− "test-mcp": {
340− Type: MCPStdio,
341− // Missing Command
342− },
343− },
344− Options: Options{
345− DataDirectory: ".crush",
346− ContextPaths: []string{"CRUSH.md"},
347− },
348− }
349−
350− err := cfg.Validate()
351− require.Error(t, err)
352− assert.Contains(t, err.Error(), "command is required for stdio MCP")
353−}
354−
355−func TestConfig_Validate_LSPMissingCommand(t *testing.T) {
356− cfg := &Config{
357− LSP: map[string]LSPConfig{
358− "test-lsp": {
359− // Missing Command
360− },
361− },
362− Options: Options{
363− DataDirectory: ".crush",
364− ContextPaths: []string{"CRUSH.md"},
365− },
366− }
367−
368− err := cfg.Validate()
369− require.Error(t, err)
370− assert.Contains(t, err.Error(), "command is required for LSP")
371−}
372−
373−func TestConfig_Validate_NoValidProviders(t *testing.T) {
374− cfg := &Config{
375− Providers: map[provider.InferenceProvider]ProviderConfig{
376− provider.InferenceProviderOpenAI: {
377− ID: provider.InferenceProviderOpenAI,
378− APIKey: "test-key",
379− ProviderType: provider.TypeOpenAI,
380− Disabled: true, // Disabled
381− },
382− },
383− Options: Options{
384− DataDirectory: ".crush",
385− ContextPaths: []string{"CRUSH.md"},
386− },
387− }
388−
389− err := cfg.Validate()
390− require.Error(t, err)
391− assert.Contains(t, err.Error(), "at least one non-disabled provider is required")
392−}
393−
394−func TestConfig_Validate_MissingDefaultAgents(t *testing.T) {
395− cfg := &Config{
396− Providers: map[provider.InferenceProvider]ProviderConfig{
397− provider.InferenceProviderOpenAI: {
398− ID: provider.InferenceProviderOpenAI,
399− APIKey: "test-key",
400− ProviderType: provider.TypeOpenAI,
401− },
402− },
403− Agents: map[AgentID]Agent{}, // Missing default agents
404− Options: Options{
405− DataDirectory: ".crush",
406− ContextPaths: []string{"CRUSH.md"},
407− },
408− }
409−
410− err := cfg.Validate()
411− require.Error(t, err)
412− assert.Contains(t, err.Error(), "coder agent is required")
413− assert.Contains(t, err.Error(), "task agent is required")
414−}
415−
416−func TestConfig_Validate_KnownAgentProtection(t *testing.T) {
417− cfg := &Config{
418− Agents: map[AgentID]Agent{
419− AgentCoder: {
420− ID: AgentCoder,
421− Name: "Modified Coder", // Should not be allowed
422− Description: "Modified description", // Should not be allowed
423− Model: LargeModel,
424− },
425− },
426− Options: Options{
427− DataDirectory: ".crush",
428− ContextPaths: []string{"CRUSH.md"},
429− },
430− }
431−
432− err := cfg.Validate()
433− require.Error(t, err)
434− assert.Contains(t, err.Error(), "coder agent name cannot be changed")
435− assert.Contains(t, err.Error(), "coder agent description cannot be changed")
436−}
437−
438−func TestConfig_Validate_EmptyDataDirectory(t *testing.T) {
439− cfg := &Config{
440− Options: Options{
441− DataDirectory: "", // Empty
442− ContextPaths: []string{"CRUSH.md"},
443− },
444− }
445−
446− err := cfg.Validate()
447− require.Error(t, err)
448− assert.Contains(t, err.Error(), "data directory is required")
449−}
450−
451−func TestConfig_Validate_EmptyContextPath(t *testing.T) {
452− cfg := &Config{
453− Options: Options{
454− DataDirectory: ".crush",
455− ContextPaths: []string{""}, // Empty context path
456− },
457− }
458−
459− err := cfg.Validate()
460− require.Error(t, err)
461− assert.Contains(t, err.Error(), "context path cannot be empty")
462−}
internal/db/connect.go+6−9
@@ -4,20 +4,17 @@ import (
44 "context"
55 "database/sql"
66 "fmt"
7+ "log/slog"
78 "os"
89 "path/filepath"
910
1011 _ "github.com/ncruces/go-sqlite3/driver"
1112 _ "github.com/ncruces/go-sqlite3/embed"
1213
13− "github.com/charmbracelet/crush/internal/config"
14− "github.com/charmbracelet/crush/internal/logging"
15−
1614 "github.com/pressly/goose/v3"
1715)
1816
19−func Connect(ctx context.Context) (*sql.DB, error) {
20− dataDir := config.Get().Options.DataDirectory
17+func Connect(ctx context.Context, dataDir string) (*sql.DB, error) {
2118 if dataDir == "" {
2219 return nil, fmt.Errorf("data.dir is not set")
2320 }
@@ -48,21 +45,21 @@ func Connect(ctx context.Context) (*sql.DB, error) {
4845
4946 for _, pragma := range pragmas {
5047 if _, err = db.ExecContext(ctx, pragma); err != nil {
51− logging.Error("Failed to set pragma", pragma, err)
48+ slog.Error("Failed to set pragma", pragma, err)
5249 } else {
53− logging.Debug("Set pragma", "pragma", pragma)
50+ slog.Debug("Set pragma", "pragma", pragma)
5451 }
5552 }
5653
5754 goose.SetBaseFS(FS)
5855
5956 if err := goose.SetDialect("sqlite3"); err != nil {
60− logging.Error("Failed to set dialect", "error", err)
57+ slog.Error("Failed to set dialect", "error", err)
6158 return nil, fmt.Errorf("failed to set dialect: %w", err)
6259 }
6360
6461 if err := goose.Up(db, "migrations"); err != nil {
65− logging.Error("Failed to apply migrations", "error", err)
62+ slog.Error("Failed to apply migrations", "error", err)
6663 return nil, fmt.Errorf("failed to apply migrations: %w", err)
6764 }
6865 return db, nil
internal/diff/diff.go−5
@@ -4,15 +4,10 @@ import (
44 "strings"
55
66 "github.com/aymanbagabas/go-udiff"
7− "github.com/charmbracelet/crush/internal/config"
87)
98
109// GenerateDiff creates a unified diff from two file contents
1110func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, int) {
12− // remove the cwd prefix and ensure consistent path format
13− // this prevents issues with absolute paths in different environments
14− cwd := config.WorkingDirectory()
15− fileName = strings.TrimPrefix(fileName, cwd)
1611 fileName = strings.TrimPrefix(fileName, "/")
1712
1813 var (
internal/env/env.gonew file+58
@@ -0,0 +1,58 @@
1+package env
2+
3+import "os"
4+
5+type Env interface {
6+ Get(key string) string
7+ Env() []string
8+}
9+
10+type osEnv struct{}
11+
12+// Get implements Env.
13+func (o *osEnv) Get(key string) string {
14+ return os.Getenv(key)
15+}
16+
17+func (o *osEnv) Env() []string {
18+ env := os.Environ()
19+ if len(env) == 0 {
20+ return nil
21+ }
22+ return env
23+}
24+
25+func New() Env {
26+ return &osEnv{}
27+}
28+
29+type mapEnv struct {
30+ m map[string]string
31+}
32+
33+// Get implements Env.
34+func (m *mapEnv) Get(key string) string {
35+ if value, ok := m.m[key]; ok {
36+ return value
37+ }
38+ return ""
39+}
40+
41+// Env implements Env.
42+func (m *mapEnv) Env() []string {
43+ if len(m.m) == 0 {
44+ return nil
45+ }
46+ env := make([]string, 0, len(m.m))
47+ for k, v := range m.m {
48+ env = append(env, k+"="+v)
49+ }
50+ return env
51+}
52+
53+func NewFromMap(m map[string]string) Env {
54+ if m == nil {
55+ m = make(map[string]string)
56+ }
57+ return &mapEnv{m: m}
58+}
internal/env/env_test.gonew file+142
@@ -0,0 +1,142 @@
1+package env
2+
3+import (
4+ "os"
5+ "strings"
6+ "testing"
7+
8+ "github.com/stretchr/testify/assert"
9+)
10+
11+func TestOsEnv_Get(t *testing.T) {
12+ env := New()
13+
14+ // Test getting an existing environment variable
15+ os.Setenv("TEST_VAR", "test_value")
16+ defer os.Unsetenv("TEST_VAR")
17+
18+ value := env.Get("TEST_VAR")
19+ assert.Equal(t, "test_value", value)
20+
21+ // Test getting a non-existent environment variable
22+ value = env.Get("NON_EXISTENT_VAR")
23+ assert.Equal(t, "", value)
24+}
25+
26+func TestOsEnv_Env(t *testing.T) {
27+ env := New()
28+
29+ envVars := env.Env()
30+
31+ // Environment should not be empty in normal circumstances
32+ assert.NotNil(t, envVars)
33+ assert.Greater(t, len(envVars), 0)
34+
35+ // Each environment variable should be in key=value format
36+ for _, envVar := range envVars {
37+ assert.Contains(t, envVar, "=")
38+ }
39+}
40+
41+func TestNewFromMap(t *testing.T) {
42+ testMap := map[string]string{
43+ "KEY1": "value1",
44+ "KEY2": "value2",
45+ }
46+
47+ env := NewFromMap(testMap)
48+ assert.NotNil(t, env)
49+ assert.IsType(t, &mapEnv{}, env)
50+}
51+
52+func TestMapEnv_Get(t *testing.T) {
53+ testMap := map[string]string{
54+ "KEY1": "value1",
55+ "KEY2": "value2",
56+ }
57+
58+ env := NewFromMap(testMap)
59+
60+ // Test getting existing keys
61+ assert.Equal(t, "value1", env.Get("KEY1"))
62+ assert.Equal(t, "value2", env.Get("KEY2"))
63+
64+ // Test getting non-existent key
65+ assert.Equal(t, "", env.Get("NON_EXISTENT"))
66+}
67+
68+func TestMapEnv_Env(t *testing.T) {
69+ t.Run("with values", func(t *testing.T) {
70+ testMap := map[string]string{
71+ "KEY1": "value1",
72+ "KEY2": "value2",
73+ }
74+
75+ env := NewFromMap(testMap)
76+ envVars := env.Env()
77+
78+ assert.Len(t, envVars, 2)
79+
80+ // Convert to map for easier testing (order is not guaranteed)
81+ envMap := make(map[string]string)
82+ for _, envVar := range envVars {
83+ parts := strings.SplitN(envVar, "=", 2)
84+ assert.Len(t, parts, 2)
85+ envMap[parts[0]] = parts[1]
86+ }
87+
88+ assert.Equal(t, "value1", envMap["KEY1"])
89+ assert.Equal(t, "value2", envMap["KEY2"])
90+ })
91+
92+ t.Run("empty map", func(t *testing.T) {
93+ env := NewFromMap(map[string]string{})
94+ envVars := env.Env()
95+ assert.Nil(t, envVars)
96+ })
97+
98+ t.Run("nil map", func(t *testing.T) {
99+ env := NewFromMap(nil)
100+ envVars := env.Env()
101+ assert.Nil(t, envVars)
102+ })
103+}
104+
105+func TestMapEnv_GetEmptyValue(t *testing.T) {
106+ testMap := map[string]string{
107+ "EMPTY_KEY": "",
108+ "NORMAL_KEY": "value",
109+ }
110+
111+ env := NewFromMap(testMap)
112+
113+ // Test that empty values are returned correctly
114+ assert.Equal(t, "", env.Get("EMPTY_KEY"))
115+ assert.Equal(t, "value", env.Get("NORMAL_KEY"))
116+}
117+
118+func TestMapEnv_EnvFormat(t *testing.T) {
119+ testMap := map[string]string{
120+ "KEY_WITH_EQUALS": "value=with=equals",
121+ "KEY_WITH_SPACES": "value with spaces",
122+ }
123+
124+ env := NewFromMap(testMap)
125+ envVars := env.Env()
126+
127+ assert.Len(t, envVars, 2)
128+
129+ // Check that the format is correct even with special characters
130+ found := make(map[string]bool)
131+ for _, envVar := range envVars {
132+ if envVar == "KEY_WITH_EQUALS=value=with=equals" {
133+ found["equals"] = true
134+ }
135+ if envVar == "KEY_WITH_SPACES=value with spaces" {
136+ found["spaces"] = true
137+ }
138+ }
139+
140+ assert.True(t, found["equals"], "Should handle values with equals signs")
141+ assert.True(t, found["spaces"], "Should handle values with spaces")
142+}
internal/fsext/fileutil.go+4−3
@@ -2,6 +2,7 @@ package fsext
22
33import (
44 "fmt"
5+ "log/slog"
56 "os"
67 "os/exec"
78 "path/filepath"
@@ -11,7 +12,7 @@ import (
1112
1213 "github.com/bmatcuk/doublestar/v4"
1314 "github.com/charlievieth/fastwalk"
14− "github.com/charmbracelet/crush/internal/logging"
15+
1516 ignore "github.com/sabhiram/go-gitignore"
1617)
1718
@@ -24,11 +25,11 @@ func init() {
2425 var err error
2526 rgPath, err = exec.LookPath("rg")
2627 if err != nil {
27− logging.Warn("Ripgrep (rg) not found in $PATH. Some features might be limited or slower.")
28+ slog.Warn("Ripgrep (rg) not found in $PATH. Some features might be limited or slower.")
2829 }
2930 fzfPath, err = exec.LookPath("fzf")
3031 if err != nil {
31− logging.Warn("FZF not found in $PATH. Some features might be limited or slower.")
32+ slog.Warn("FZF not found in $PATH. Some features might be limited or slower.")
3233 }
3334}
3435
internal/fur/provider/provider.go+1−1
@@ -45,7 +45,7 @@ type Provider struct {
4545// Model represents an AI model configuration.
4646type Model struct {
4747 ID string `json:"id"`
48− Name string `json:"model"`
48+ Model string `json:"model"`
4949 CostPer1MIn float64 `json:"cost_per_1m_in"`
5050 CostPer1MOut float64 `json:"cost_per_1m_out"`
5151 CostPer1MInCached float64 `json:"cost_per_1m_in_cached"`
internal/llm/agent/agent.go+60−78
@@ -4,17 +4,19 @@ import (
44 "context"
55 "errors"
66 "fmt"
7+ "log/slog"
78 "slices"
89 "strings"
910 "sync"
1011 "time"
1112
1213 "github.com/charmbracelet/crush/internal/config"
14+ fur "github.com/charmbracelet/crush/internal/fur/provider"
1315 "github.com/charmbracelet/crush/internal/history"
1416 "github.com/charmbracelet/crush/internal/llm/prompt"
1517 "github.com/charmbracelet/crush/internal/llm/provider"
1618 "github.com/charmbracelet/crush/internal/llm/tools"
17− "github.com/charmbracelet/crush/internal/logging"
19+ "github.com/charmbracelet/crush/internal/log"
1820 "github.com/charmbracelet/crush/internal/lsp"
1921 "github.com/charmbracelet/crush/internal/message"
2022 "github.com/charmbracelet/crush/internal/permission"
@@ -49,7 +51,7 @@ type AgentEvent struct {
4951
5052type Service interface {
5153 pubsub.Suscriber[AgentEvent]
52− Model() config.Model
54+ Model() fur.Model
5355 Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error)
5456 Cancel(sessionID string)
5557 CancelAll()
@@ -76,9 +78,9 @@ type agent struct {
7678 activeRequests sync.Map
7779}
7880
79−var agentPromptMap = map[config.AgentID]prompt.PromptID{
80− config.AgentCoder: prompt.PromptCoder,
81− config.AgentTask: prompt.PromptTask,
81+var agentPromptMap = map[string]prompt.PromptID{
82+ "coder": prompt.PromptCoder,
83+ "task": prompt.PromptTask,
8284}
8385
8486func NewAgent(
@@ -92,25 +94,26 @@ func NewAgent(
9294) (Service, error) {
9395 ctx := context.Background()
9496 cfg := config.Get()
95− otherTools := GetMcpTools(ctx, permissions)
97+ otherTools := GetMcpTools(ctx, permissions, cfg)
9698 if len(lspClients) > 0 {
9799 otherTools = append(otherTools, tools.NewDiagnosticsTool(lspClients))
98100 }
99101
102+ cwd := cfg.WorkingDir()
100103 allTools := []tools.BaseTool{
101− tools.NewBashTool(permissions),
102− tools.NewEditTool(lspClients, permissions, history),
103− tools.NewFetchTool(permissions),
104− tools.NewGlobTool(),
105− tools.NewGrepTool(),
106− tools.NewLsTool(),
104+ tools.NewBashTool(permissions, cwd),
105+ tools.NewEditTool(lspClients, permissions, history, cwd),
106+ tools.NewFetchTool(permissions, cwd),
107+ tools.NewGlobTool(cwd),
108+ tools.NewGrepTool(cwd),
109+ tools.NewLsTool(cwd),
107110 tools.NewSourcegraphTool(),
108− tools.NewViewTool(lspClients),
109− tools.NewWriteTool(lspClients, permissions, history),
111+ tools.NewViewTool(lspClients, cwd),
112+ tools.NewWriteTool(lspClients, permissions, history, cwd),
110113 }
111114
112− if agentCfg.ID == config.AgentCoder {
113− taskAgentCfg := config.Get().Agents[config.AgentTask]
115+ if agentCfg.ID == "coder" {
116+ taskAgentCfg := config.Get().Agents["task"]
114117 if taskAgentCfg.ID == "" {
115118 return nil, fmt.Errorf("task agent not found in config")
116119 }
@@ -130,13 +133,13 @@ func NewAgent(
130133 }
131134
132135 allTools = append(allTools, otherTools...)
133− providerCfg := config.GetAgentProvider(agentCfg.ID)
134− if providerCfg.ID == "" {
136+ providerCfg := config.Get().GetProviderForModel(agentCfg.Model)
137+ if providerCfg == nil {
135138 return nil, fmt.Errorf("provider for agent %s not found in config", agentCfg.Name)
136139 }
137− model := config.GetAgentModel(agentCfg.ID)
140+ model := config.Get().GetModelByType(agentCfg.Model)
138141
139− if model.ID == "" {
142+ if model == nil {
140143 return nil, fmt.Errorf("model not found for agent %s", agentCfg.Name)
141144 }
142145
@@ -148,51 +151,40 @@ func NewAgent(
148151 provider.WithModel(agentCfg.Model),
149152 provider.WithSystemMessage(prompt.GetPrompt(promptID, providerCfg.ID)),
150153 }
151− agentProvider, err := provider.NewProvider(providerCfg, opts...)
154+ agentProvider, err := provider.NewProvider(*providerCfg, opts...)
152155 if err != nil {
153156 return nil, err
154157 }
155158
156− smallModelCfg := cfg.Models.Small
157− var smallModel config.Model
158−
159− var smallModelProviderCfg config.ProviderConfig
159+ smallModelCfg := cfg.Models[config.SelectedModelTypeSmall]
160+ var smallModelProviderCfg *config.ProviderConfig
160161 if smallModelCfg.Provider == providerCfg.ID {
161162 smallModelProviderCfg = providerCfg
162163 } else {
163− for _, p := range cfg.Providers {
164− if p.ID == smallModelCfg.Provider {
165− smallModelProviderCfg = p
166− break
167− }
168− }
164+ smallModelProviderCfg = cfg.GetProviderForModel(config.SelectedModelTypeSmall)
165+
169166 if smallModelProviderCfg.ID == "" {
170167 return nil, fmt.Errorf("provider %s not found in config", smallModelCfg.Provider)
171168 }
172169 }
173− for _, m := range smallModelProviderCfg.Models {
174− if m.ID == smallModelCfg.ModelID {
175− smallModel = m
176− break
177− }
178− }
170+ smallModel := cfg.GetModelByType(config.SelectedModelTypeSmall)
179171 if smallModel.ID == "" {
180− return nil, fmt.Errorf("model %s not found in provider %s", smallModelCfg.ModelID, smallModelProviderCfg.ID)
172+ return nil, fmt.Errorf("model %s not found in provider %s", smallModelCfg.Model, smallModelProviderCfg.ID)
181173 }
182174
183175 titleOpts := []provider.ProviderClientOption{
184− provider.WithModel(config.SmallModel),
176+ provider.WithModel(config.SelectedModelTypeSmall),
185177 provider.WithSystemMessage(prompt.GetPrompt(prompt.PromptTitle, smallModelProviderCfg.ID)),
186178 }
187− titleProvider, err := provider.NewProvider(smallModelProviderCfg, titleOpts...)
179+ titleProvider, err := provider.NewProvider(*smallModelProviderCfg, titleOpts...)
188180 if err != nil {
189181 return nil, err
190182 }
191183 summarizeOpts := []provider.ProviderClientOption{
192− provider.WithModel(config.SmallModel),
184+ provider.WithModel(config.SelectedModelTypeSmall),
193185 provider.WithSystemMessage(prompt.GetPrompt(prompt.PromptSummarizer, smallModelProviderCfg.ID)),
194186 }
195− summarizeProvider, err := provider.NewProvider(smallModelProviderCfg, summarizeOpts...)
187+ summarizeProvider, err := provider.NewProvider(*smallModelProviderCfg, summarizeOpts...)
196188 if err != nil {
197189 return nil, err
198190 }
@@ -225,15 +217,15 @@ func NewAgent(
225217 return agent, nil
226218}
227219
228−func (a *agent) Model() config.Model {
229− return config.GetAgentModel(a.agentCfg.ID)
220+func (a *agent) Model() fur.Model {
221+ return *config.Get().GetModelByType(a.agentCfg.Model)
230222}
231223
232224func (a *agent) Cancel(sessionID string) {
233225 // Cancel regular requests
234226 if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID); exists {
235227 if cancel, ok := cancelFunc.(context.CancelFunc); ok {
236− logging.InfoPersist(fmt.Sprintf("Request cancellation initiated for session: %s", sessionID))
228+ slog.Info(fmt.Sprintf("Request cancellation initiated for session: %s", sessionID))
237229 cancel()
238230 }
239231 }
@@ -241,7 +233,7 @@ func (a *agent) Cancel(sessionID string) {
241233 // Also check for summarize requests
242234 if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID + "-summarize"); exists {
243235 if cancel, ok := cancelFunc.(context.CancelFunc); ok {
244− logging.InfoPersist(fmt.Sprintf("Summarize cancellation initiated for session: %s", sessionID))
236+ slog.Info(fmt.Sprintf("Summarize cancellation initiated for session: %s", sessionID))
245237 cancel()
246238 }
247239 }
@@ -335,8 +327,8 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac
335327
336328 a.activeRequests.Store(sessionID, cancel)
337329 go func() {
338− logging.Debug("Request started", "sessionID", sessionID)
339− defer logging.RecoverPanic("agent.Run", func() {
330+ slog.Debug("Request started", "sessionID", sessionID)
331+ defer log.RecoverPanic("agent.Run", func() {
340332 events <- a.err(fmt.Errorf("panic while running the agent"))
341333 })
342334 var attachmentParts []message.ContentPart
@@ -345,9 +337,9 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac
345337 }
346338 result := a.processGeneration(genCtx, sessionID, content, attachmentParts)
347339 if result.Error != nil && !errors.Is(result.Error, ErrRequestCancelled) && !errors.Is(result.Error, context.Canceled) {
348− logging.ErrorPersist(result.Error.Error())
340+ slog.Error(result.Error.Error())
349341 }
350− logging.Debug("Request completed", "sessionID", sessionID)
342+ slog.Debug("Request completed", "sessionID", sessionID)
351343 a.activeRequests.Delete(sessionID)
352344 cancel()
353345 a.Publish(pubsub.CreatedEvent, result)
@@ -366,12 +358,12 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string
366358 }
367359 if len(msgs) == 0 {
368360 go func() {
369− defer logging.RecoverPanic("agent.Run", func() {
370− logging.ErrorPersist("panic while generating title")
361+ defer log.RecoverPanic("agent.Run", func() {
362+ slog.Error("panic while generating title")
371363 })
372364 titleErr := a.generateTitle(context.Background(), sessionID, content)
373365 if titleErr != nil && !errors.Is(titleErr, context.Canceled) && !errors.Is(titleErr, context.DeadlineExceeded) {
374− logging.ErrorPersist(fmt.Sprintf("failed to generate title: %v", titleErr))
366+ slog.Error(fmt.Sprintf("failed to generate title: %v", titleErr))
375367 }
376368 }()
377369 }
@@ -418,11 +410,7 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string
418410 return a.err(fmt.Errorf("failed to process events: %w", err))
419411 }
420412 if cfg.Options.Debug {
421− seqId := (len(msgHistory) + 1) / 2
422− toolResultFilepath := logging.WriteToolResultsJson(sessionID, seqId, toolResults)
423− logging.Info("Result", "message", agentMessage.FinishReason(), "toolResults", "{}", "filepath", toolResultFilepath)
424− } else {
425− logging.Info("Result", "message", agentMessage.FinishReason(), "toolResults", toolResults)
413+ slog.Info("Result", "message", agentMessage.FinishReason(), "toolResults", toolResults)
426414 }
427415 if (agentMessage.FinishReason() == message.FinishReasonToolUse) && toolResults != nil {
428416 // We are not done, we need to respond with the tool response
@@ -581,22 +569,22 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg
581569 assistantMsg.AppendContent(event.Content)
582570 return a.messages.Update(ctx, *assistantMsg)
583571 case provider.EventToolUseStart:
584− logging.Info("Tool call started", "toolCall", event.ToolCall)
572+ slog.Info("Tool call started", "toolCall", event.ToolCall)
585573 assistantMsg.AddToolCall(*event.ToolCall)
586574 return a.messages.Update(ctx, *assistantMsg)
587575 case provider.EventToolUseDelta:
588576 assistantMsg.AppendToolCallInput(event.ToolCall.ID, event.ToolCall.Input)
589577 return a.messages.Update(ctx, *assistantMsg)
590578 case provider.EventToolUseStop:
591− logging.Info("Finished tool call", "toolCall", event.ToolCall)
579+ slog.Info("Finished tool call", "toolCall", event.ToolCall)
592580 assistantMsg.FinishToolCall(event.ToolCall.ID)
593581 return a.messages.Update(ctx, *assistantMsg)
594582 case provider.EventError:
595583 if errors.Is(event.Error, context.Canceled) {
596− logging.InfoPersist(fmt.Sprintf("Event processing canceled for session: %s", sessionID))
584+ slog.Info(fmt.Sprintf("Event processing canceled for session: %s", sessionID))
597585 return context.Canceled
598586 }
599− logging.ErrorPersist(event.Error.Error())
587+ slog.Error(event.Error.Error())
600588 return event.Error
601589 case provider.EventComplete:
602590 assistantMsg.SetToolCalls(event.Response.ToolCalls)
@@ -610,7 +598,7 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg
610598 return nil
611599}
612600
613−func (a *agent) TrackUsage(ctx context.Context, sessionID string, model config.Model, usage provider.TokenUsage) error {
601+func (a *agent) TrackUsage(ctx context.Context, sessionID string, model fur.Model, usage provider.TokenUsage) error {
614602 sess, err := a.sessions.Get(ctx, sessionID)
615603 if err != nil {
616604 return fmt.Errorf("failed to get session: %w", err)
@@ -819,7 +807,7 @@ func (a *agent) UpdateModel() error {
819807 cfg := config.Get()
820808
821809 // Get current provider configuration
822− currentProviderCfg := config.GetAgentProvider(a.agentCfg.ID)
810+ currentProviderCfg := cfg.GetProviderForModel(a.agentCfg.Model)
823811 if currentProviderCfg.ID == "" {
824812 return fmt.Errorf("provider for agent %s not found in config", a.agentCfg.Name)
825813 }
@@ -827,7 +815,7 @@ func (a *agent) UpdateModel() error {
827815 // Check if provider has changed
828816 if string(currentProviderCfg.ID) != a.providerID {
829817 // Provider changed, need to recreate the main provider
830− model := config.GetAgentModel(a.agentCfg.ID)
818+ model := cfg.GetModelByType(a.agentCfg.Model)
831819 if model.ID == "" {
832820 return fmt.Errorf("model not found for agent %s", a.agentCfg.Name)
833821 }
@@ -842,7 +830,7 @@ func (a *agent) UpdateModel() error {
842830 provider.WithSystemMessage(prompt.GetPrompt(promptID, currentProviderCfg.ID)),
843831 }
844832
845− newProvider, err := provider.NewProvider(currentProviderCfg, opts...)
833+ newProvider, err := provider.NewProvider(*currentProviderCfg, opts...)
846834 if err != nil {
847835 return fmt.Errorf("failed to create new provider: %w", err)
848836 }
@@ -853,7 +841,7 @@ func (a *agent) UpdateModel() error {
853841 }
854842
855843 // Check if small model provider has changed (affects title and summarize providers)
856− smallModelCfg := cfg.Models.Small
844+ smallModelCfg := cfg.Models[config.SelectedModelTypeSmall]
857845 var smallModelProviderCfg config.ProviderConfig
858846
859847 for _, p := range cfg.Providers {
@@ -869,20 +857,14 @@ func (a *agent) UpdateModel() error {
869857
870858 // Check if summarize provider has changed
871859 if string(smallModelProviderCfg.ID) != a.summarizeProviderID {
872− var smallModel config.Model
873− for _, m := range smallModelProviderCfg.Models {
874− if m.ID == smallModelCfg.ModelID {
875− smallModel = m
876− break
877− }
878− }
879− if smallModel.ID == "" {
880− return fmt.Errorf("model %s not found in provider %s", smallModelCfg.ModelID, smallModelProviderCfg.ID)
860+ smallModel := cfg.GetModelByType(config.SelectedModelTypeSmall)
861+ if smallModel == nil {
862+ return fmt.Errorf("model %s not found in provider %s", smallModelCfg.Model, smallModelProviderCfg.ID)
881863 }
882864
883865 // Recreate title provider
884866 titleOpts := []provider.ProviderClientOption{
885− provider.WithModel(config.SmallModel),
867+ provider.WithModel(config.SelectedModelTypeSmall),
886868 provider.WithSystemMessage(prompt.GetPrompt(prompt.PromptTitle, smallModelProviderCfg.ID)),
887869 // We want the title to be short, so we limit the max tokens
888870 provider.WithMaxTokens(40),
@@ -894,7 +876,7 @@ func (a *agent) UpdateModel() error {
894876
895877 // Recreate summarize provider
896878 summarizeOpts := []provider.ProviderClientOption{
897− provider.WithModel(config.SmallModel),
879+ provider.WithModel(config.SelectedModelTypeSmall),
898880 provider.WithSystemMessage(prompt.GetPrompt(prompt.PromptSummarizer, smallModelProviderCfg.ID)),
899881 }
900882 newSummarizeProvider, err := provider.NewProvider(smallModelProviderCfg, summarizeOpts...)
internal/llm/agent/mcp-tools.go+19−16
@@ -4,10 +4,11 @@ import (
44 "context"
55 "encoding/json"
66 "fmt"
7+ "log/slog"
78
89 "github.com/charmbracelet/crush/internal/config"
910 "github.com/charmbracelet/crush/internal/llm/tools"
10− "github.com/charmbracelet/crush/internal/logging"
11+
1112 "github.com/charmbracelet/crush/internal/permission"
1213 "github.com/charmbracelet/crush/internal/version"
1314
@@ -19,8 +20,9 @@ import (
1920type mcpTool struct {
2021 mcpName string
2122 tool mcp.Tool
22− mcpConfig config.MCP
23+ mcpConfig config.MCPConfig
2324 permissions permission.Service
25+ workingDir string
2426}
2527
2628type MCPClient interface {
@@ -97,7 +99,7 @@ func (b *mcpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolRes
9799 p := b.permissions.Request(
98100 permission.CreatePermissionRequest{
99101 SessionID: sessionID,
100− Path: config.WorkingDirectory(),
102+ Path: b.workingDir,
101103 ToolName: b.Info().Name,
102104 Action: "execute",
103105 Description: permissionDescription,
@@ -142,18 +144,19 @@ func (b *mcpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolRes
142144 return tools.NewTextErrorResponse("invalid mcp type"), nil
143145}
144146
145−func NewMcpTool(name string, tool mcp.Tool, permissions permission.Service, mcpConfig config.MCP) tools.BaseTool {
147+func NewMcpTool(name string, tool mcp.Tool, permissions permission.Service, mcpConfig config.MCPConfig, workingDir string) tools.BaseTool {
146148 return &mcpTool{
147149 mcpName: name,
148150 tool: tool,
149151 mcpConfig: mcpConfig,
150152 permissions: permissions,
153+ workingDir: workingDir,
151154 }
152155}
153156
154157var mcpTools []tools.BaseTool
155158
156−func getTools(ctx context.Context, name string, m config.MCP, permissions permission.Service, c MCPClient) []tools.BaseTool {
159+func getTools(ctx context.Context, name string, m config.MCPConfig, permissions permission.Service, c MCPClient, workingDir string) []tools.BaseTool {
157160 var stdioTools []tools.BaseTool
158161 initRequest := mcp.InitializeRequest{}
159162 initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
@@ -164,27 +167,27 @@ func getTools(ctx context.Context, name string, m config.MCP, permissions permis
164167
165168 _, err := c.Initialize(ctx, initRequest)
166169 if err != nil {
167− logging.Error("error initializing mcp client", "error", err)
170+ slog.Error("error initializing mcp client", "error", err)
168171 return stdioTools
169172 }
170173 toolsRequest := mcp.ListToolsRequest{}
171174 tools, err := c.ListTools(ctx, toolsRequest)
172175 if err != nil {
173− logging.Error("error listing tools", "error", err)
176+ slog.Error("error listing tools", "error", err)
174177 return stdioTools
175178 }
176179 for _, t := range tools.Tools {
177− stdioTools = append(stdioTools, NewMcpTool(name, t, permissions, m))
180+ stdioTools = append(stdioTools, NewMcpTool(name, t, permissions, m, workingDir))
178181 }
179182 defer c.Close()
180183 return stdioTools
181184}
182185
183−func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.BaseTool {
186+func GetMcpTools(ctx context.Context, permissions permission.Service, cfg *config.Config) []tools.BaseTool {
184187 if len(mcpTools) > 0 {
185188 return mcpTools
186189 }
187− for name, m := range config.Get().MCP {
190+ for name, m := range cfg.MCP {
188191 switch m.Type {
189192 case config.MCPStdio:
190193 c, err := client.NewStdioMCPClient(
@@ -193,31 +196,31 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
193196 m.Args...,
194197 )
195198 if err != nil {
196− logging.Error("error creating mcp client", "error", err)
199+ slog.Error("error creating mcp client", "error", err)
197200 continue
198201 }
199202
200− mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
203+ mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c, cfg.WorkingDir())...)
201204 case config.MCPHttp:
202205 c, err := client.NewStreamableHttpClient(
203206 m.URL,
204207 transport.WithHTTPHeaders(m.Headers),
205208 )
206209 if err != nil {
207− logging.Error("error creating mcp client", "error", err)
210+ slog.Error("error creating mcp client", "error", err)
208211 continue
209212 }
210− mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
213+ mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c, cfg.WorkingDir())...)
211214 case config.MCPSse:
212215 c, err := client.NewSSEMCPClient(
213216 m.URL,
214217 client.WithHeaders(m.Headers),
215218 )
216219 if err != nil {
217− logging.Error("error creating mcp client", "error", err)
220+ slog.Error("error creating mcp client", "error", err)
218221 continue
219222 }
220− mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
223+ mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c, cfg.WorkingDir())...)
221224 }
222225 }
223226
internal/llm/prompt/coder.go+8−8
@@ -3,6 +3,7 @@ package prompt
33import (
44 "context"
55 "fmt"
6+ "log/slog"
67 "os"
78 "path/filepath"
89 "runtime"
@@ -11,15 +12,14 @@ import (
1112 "github.com/charmbracelet/crush/internal/config"
1213 "github.com/charmbracelet/crush/internal/fur/provider"
1314 "github.com/charmbracelet/crush/internal/llm/tools"
14− "github.com/charmbracelet/crush/internal/logging"
1515)
1616
17−func CoderPrompt(p provider.InferenceProvider, contextFiles ...string) string {
17+func CoderPrompt(p string, contextFiles ...string) string {
1818 var basePrompt string
1919 switch p {
20− case provider.InferenceProviderOpenAI:
20+ case string(provider.InferenceProviderOpenAI):
2121 basePrompt = baseOpenAICoderPrompt
22− case provider.InferenceProviderGemini, provider.InferenceProviderVertexAI:
22+ case string(provider.InferenceProviderGemini), string(provider.InferenceProviderVertexAI):
2323 basePrompt = baseGeminiCoderPrompt
2424 default:
2525 basePrompt = baseAnthropicCoderPrompt
@@ -28,8 +28,8 @@ func CoderPrompt(p provider.InferenceProvider, contextFiles ...string) string {
2828
2929 basePrompt = fmt.Sprintf("%s\n\n%s\n%s", basePrompt, envInfo, lspInformation())
3030
31− contextContent := getContextFromPaths(contextFiles)
32− logging.Debug("Context content", "Context", contextContent)
31+ contextContent := getContextFromPaths(config.Get().WorkingDir(), contextFiles)
32+ slog.Debug("Context content", "Context", contextContent)
3333 if contextContent != "" {
3434 return fmt.Sprintf("%s\n\n# Project-Specific Context\n Make sure to follow the instructions in the context below\n%s", basePrompt, contextContent)
3535 }
@@ -380,11 +380,11 @@ Your core function is efficient and safe assistance. Balance extreme conciseness
380380`
381381
382382func getEnvironmentInfo() string {
383− cwd := config.WorkingDirectory()
383+ cwd := config.Get().WorkingDir()
384384 isGit := isGitRepo(cwd)
385385 platform := runtime.GOOS
386386 date := time.Now().Format("1/2/2006")
387− ls := tools.NewLsTool()
387+ ls := tools.NewLsTool(cwd)
388388 r, _ := ls.Run(context.Background(), tools.ToolCall{
389389 Input: `{"path":"."}`,
390390 })
internal/llm/prompt/prompt.go+6−9
@@ -5,9 +5,6 @@ import (
55 "path/filepath"
66 "strings"
77 "sync"
8−
9− "github.com/charmbracelet/crush/internal/config"
10− "github.com/charmbracelet/crush/internal/fur/provider"
118)
129
1310type PromptID string
@@ -20,25 +17,25 @@ const (
2017 PromptDefault PromptID = "default"
2118)
2219
23−func GetPrompt(promptID PromptID, provider provider.InferenceProvider, contextPaths ...string) string {
20+func GetPrompt(promptID PromptID, provider string, contextPaths ...string) string {
2421 basePrompt := ""
2522 switch promptID {
2623 case PromptCoder:
2724 basePrompt = CoderPrompt(provider)
2825 case PromptTitle:
29− basePrompt = TitlePrompt(provider)
26+ basePrompt = TitlePrompt()
3027 case PromptTask:
31− basePrompt = TaskPrompt(provider)
28+ basePrompt = TaskPrompt()
3229 case PromptSummarizer:
33− basePrompt = SummarizerPrompt(provider)
30+ basePrompt = SummarizerPrompt()
3431 default:
3532 basePrompt = "You are a helpful assistant"
3633 }
3734 return basePrompt
3835}
3936
40−func getContextFromPaths(contextPaths []string) string {
41− return processContextPaths(config.WorkingDirectory(), contextPaths)
37+func getContextFromPaths(workingDir string, contextPaths []string) string {
38+ return processContextPaths(workingDir, contextPaths)
4239}
4340
4441func processContextPaths(workDir string, paths []string) string {
internal/llm/prompt/prompt_test.godeleted−56
@@ -1,56 +0,0 @@
1−package prompt
2−
3−import (
4− "fmt"
5− "os"
6− "path/filepath"
7− "testing"
8−
9− "github.com/charmbracelet/crush/internal/config"
10− "github.com/stretchr/testify/assert"
11− "github.com/stretchr/testify/require"
12−)
13−
14−func TestGetContextFromPaths(t *testing.T) {
15− t.Parallel()
16−
17− tmpDir := t.TempDir()
18− _, err := config.Init(tmpDir, false)
19− if err != nil {
20− t.Fatalf("Failed to load config: %v", err)
21− }
22− testFiles := []string{
23− "file.txt",
24− "directory/file_a.txt",
25− "directory/file_b.txt",
26− "directory/file_c.txt",
27− }
28−
29− createTestFiles(t, tmpDir, testFiles)
30−
31− context := getContextFromPaths(
32− []string{
33− "file.txt",
34− "directory/",
35− },
36− )
37− expectedContext := fmt.Sprintf("# From:%s/file.txt\nfile.txt: test content\n# From:%s/directory/file_a.txt\ndirectory/file_a.txt: test content\n# From:%s/directory/file_b.txt\ndirectory/file_b.txt: test content\n# From:%s/directory/file_c.txt\ndirectory/file_c.txt: test content", tmpDir, tmpDir, tmpDir, tmpDir)
38− assert.Equal(t, expectedContext, context)
39−}
40−
41−func createTestFiles(t *testing.T, tmpDir string, testFiles []string) {
42− t.Helper()
43− for _, path := range testFiles {
44− fullPath := filepath.Join(tmpDir, path)
45− if path[len(path)-1] == '/' {
46− err := os.MkdirAll(fullPath, 0o755)
47− require.NoError(t, err)
48− } else {
49− dir := filepath.Dir(fullPath)
50− err := os.MkdirAll(dir, 0o755)
51− require.NoError(t, err)
52− err = os.WriteFile(fullPath, []byte(path+": test content"), 0o644)
53− require.NoError(t, err)
54− }
55− }
56−}
internal/llm/prompt/summarizer.go+1−5
@@ -1,10 +1,6 @@
11package prompt
22
3−import (
4− "github.com/charmbracelet/crush/internal/fur/provider"
5−)
6−
7−func SummarizerPrompt(_ provider.InferenceProvider) string {
3+func SummarizerPrompt() string {
84 return `You are a helpful AI assistant tasked with summarizing conversations.
95
106When asked to summarize, provide a detailed but concise summary of the conversation.
internal/llm/prompt/task.go+1−3
@@ -2,11 +2,9 @@ package prompt
22
33import (
44 "fmt"
5−
6− "github.com/charmbracelet/crush/internal/fur/provider"
75)
86
9−func TaskPrompt(_ provider.InferenceProvider) string {
7+func TaskPrompt() string {
108 agentPrompt := `You are an agent for Crush. Given the user's prompt, you should use the tools available to you to answer the user's question.
119Notes:
12101. IMPORTANT: You should be concise, direct, and to the point, since your responses will be displayed on a command line interface. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...".
internal/llm/prompt/title.go+1−5
@@ -1,10 +1,6 @@
11package prompt
22
3−import (
4− "github.com/charmbracelet/crush/internal/fur/provider"
5−)
6−
7−func TitlePrompt(_ provider.InferenceProvider) string {
3+func TitlePrompt() string {
84 return `you will generate a short title based on the first message a user begins a conversation with
95- ensure it is not more than 50 characters long
106- the title should be a summary of the user's message
internal/llm/provider/anthropic.go+16−15
@@ -6,6 +6,7 @@ import (
66 "errors"
77 "fmt"
88 "io"
9+ "log/slog"
910 "regexp"
1011 "strconv"
1112 "time"
@@ -16,7 +17,6 @@ import (
1617 "github.com/charmbracelet/crush/internal/config"
1718 "github.com/charmbracelet/crush/internal/fur/provider"
1819 "github.com/charmbracelet/crush/internal/llm/tools"
19− "github.com/charmbracelet/crush/internal/logging"
2020 "github.com/charmbracelet/crush/internal/message"
2121)
2222
@@ -92,7 +92,7 @@ func (a *anthropicClient) convertMessages(messages []message.Message) (anthropic
9292 }
9393
9494 if len(blocks) == 0 {
95− logging.Warn("There is a message without content, investigate, this should not happen")
95+ slog.Warn("There is a message without content, investigate, this should not happen")
9696 continue
9797 }
9898 anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(blocks...))
@@ -153,9 +153,9 @@ func (a *anthropicClient) preparedMessages(messages []anthropic.MessageParam, to
153153 model := a.providerOptions.model(a.providerOptions.modelType)
154154 var thinkingParam anthropic.ThinkingConfigParamUnion
155155 cfg := config.Get()
156− modelConfig := cfg.Models.Large
157− if a.providerOptions.modelType == config.SmallModel {
158− modelConfig = cfg.Models.Small
156+ modelConfig := cfg.Models[config.SelectedModelTypeLarge]
157+ if a.providerOptions.modelType == config.SelectedModelTypeSmall {
158+ modelConfig = cfg.Models[config.SelectedModelTypeSmall]
159159 }
160160 temperature := anthropic.Float(0)
161161
@@ -207,7 +207,7 @@ func (a *anthropicClient) send(ctx context.Context, messages []message.Message,
207207 preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools))
208208 if cfg.Options.Debug {
209209 jsonData, _ := json.Marshal(preparedMessages)
210− logging.Debug("Prepared messages", "messages", string(jsonData))
210+ slog.Debug("Prepared messages", "messages", string(jsonData))
211211 }
212212
213213 anthropicResponse, err := a.client.Messages.New(
@@ -216,13 +216,13 @@ func (a *anthropicClient) send(ctx context.Context, messages []message.Message,
216216 )
217217 // If there is an error we are going to see if we can retry the call
218218 if err != nil {
219− logging.Error("Error in Anthropic API call", "error", err)
219+ slog.Error("Error in Anthropic API call", "error", err)
220220 retry, after, retryErr := a.shouldRetry(attempts, err)
221221 if retryErr != nil {
222222 return nil, retryErr
223223 }
224224 if retry {
225− logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
225+ slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries))
226226 select {
227227 case <-ctx.Done():
228228 return nil, ctx.Err()
@@ -259,7 +259,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
259259 preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools))
260260 if cfg.Options.Debug {
261261 jsonData, _ := json.Marshal(preparedMessages)
262− logging.Debug("Prepared messages", "messages", string(jsonData))
262+ slog.Debug("Prepared messages", "messages", string(jsonData))
263263 }
264264
265265 anthropicStream := a.client.Messages.NewStreaming(
@@ -273,7 +273,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
273273 event := anthropicStream.Current()
274274 err := accumulatedMessage.Accumulate(event)
275275 if err != nil {
276− logging.Warn("Error accumulating message", "error", err)
276+ slog.Warn("Error accumulating message", "error", err)
277277 continue
278278 }
279279
@@ -364,7 +364,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
364364 return
365365 }
366366 if retry {
367− logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
367+ slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries))
368368 select {
369369 case <-ctx.Done():
370370 // context cancelled
@@ -399,7 +399,7 @@ func (a *anthropicClient) shouldRetry(attempts int, err error) (bool, int64, err
399399 }
400400
401401 if apiErr.StatusCode == 401 {
402− a.providerOptions.apiKey, err = config.ResolveAPIKey(a.providerOptions.config.APIKey)
402+ a.providerOptions.apiKey, err = config.Get().Resolve(a.providerOptions.config.APIKey)
403403 if err != nil {
404404 return false, 0, fmt.Errorf("failed to resolve API key: %w", err)
405405 }
@@ -411,7 +411,7 @@ func (a *anthropicClient) shouldRetry(attempts int, err error) (bool, int64, err
411411 if apiErr.StatusCode == 400 {
412412 if adjusted, ok := a.handleContextLimitError(apiErr); ok {
413413 a.adjustedMaxTokens = adjusted
414− logging.Debug("Adjusted max_tokens due to context limit", "new_max_tokens", adjusted)
414+ slog.Debug("Adjusted max_tokens due to context limit", "new_max_tokens", adjusted)
415415 return true, 0, nil
416416 }
417417 }
@@ -438,7 +438,8 @@ func (a *anthropicClient) shouldRetry(attempts int, err error) (bool, int64, err
438438func (a *anthropicClient) handleContextLimitError(apiErr *anthropic.Error) (int, bool) {
439439 // Parse error message like: "input length and max_tokens exceed context limit: 154978 + 50000 > 200000"
440440 errorMsg := apiErr.Error()
441− re := regexp.MustCompile(`input length and max_tokens exceed context limit: (\d+) \+ (\d+) > (\d+)`)
441+
442+ re := regexp.MustCompile("input length and `max_tokens` exceed context limit: (\\d+) \\+ (\\d+) > (\\d+)")
442443 matches := re.FindStringSubmatch(errorMsg)
443444
444445 if len(matches) != 4 {
@@ -490,6 +491,6 @@ func (a *anthropicClient) usage(msg anthropic.Message) TokenUsage {
490491 }
491492}
492493
493−func (a *anthropicClient) Model() config.Model {
494+func (a *anthropicClient) Model() provider.Model {
494495 return a.providerOptions.model(a.providerOptions.modelType)
495496}
internal/llm/provider/bedrock.go+5−4
@@ -7,6 +7,7 @@ import (
77 "strings"
88
99 "github.com/charmbracelet/crush/internal/config"
10+ "github.com/charmbracelet/crush/internal/fur/provider"
1011 "github.com/charmbracelet/crush/internal/llm/tools"
1112 "github.com/charmbracelet/crush/internal/message"
1213)
@@ -31,14 +32,14 @@ func newBedrockClient(opts providerClientOptions) BedrockClient {
3132 }
3233 }
3334
34− opts.model = func(modelType config.ModelType) config.Model {
35− model := config.GetModel(modelType)
35+ opts.model = func(modelType config.SelectedModelType) provider.Model {
36+ model := config.Get().GetModelByType(modelType)
3637
3738 // Prefix the model name with region
3839 regionPrefix := region[:2]
3940 modelName := model.ID
4041 model.ID = fmt.Sprintf("%s.%s", regionPrefix, modelName)
41− return model
42+ return *model
4243 }
4344
4445 model := opts.model(opts.modelType)
@@ -87,6 +88,6 @@ func (b *bedrockClient) stream(ctx context.Context, messages []message.Message,
8788 return b.childProvider.stream(ctx, messages, tools)
8889}
8990
90−func (b *bedrockClient) Model() config.Model {
91+func (b *bedrockClient) Model() provider.Model {
9192 return b.providerOptions.model(b.providerOptions.modelType)
9293}
internal/llm/provider/gemini.go+15−14
@@ -6,12 +6,13 @@ import (
66 "errors"
77 "fmt"
88 "io"
9+ "log/slog"
910 "strings"
1011 "time"
1112
1213 "github.com/charmbracelet/crush/internal/config"
14+ "github.com/charmbracelet/crush/internal/fur/provider"
1315 "github.com/charmbracelet/crush/internal/llm/tools"
14− "github.com/charmbracelet/crush/internal/logging"
1516 "github.com/charmbracelet/crush/internal/message"
1617 "github.com/google/uuid"
1718 "google.golang.org/genai"
@@ -27,7 +28,7 @@ type GeminiClient ProviderClient
2728func newGeminiClient(opts providerClientOptions) GeminiClient {
2829 client, err := createGeminiClient(opts)
2930 if err != nil {
30− logging.Error("Failed to create Gemini client", "error", err)
31+ slog.Error("Failed to create Gemini client", "error", err)
3132 return nil
3233 }
3334
@@ -167,12 +168,12 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
167168 cfg := config.Get()
168169 if cfg.Options.Debug {
169170 jsonData, _ := json.Marshal(geminiMessages)
170− logging.Debug("Prepared messages", "messages", string(jsonData))
171+ slog.Debug("Prepared messages", "messages", string(jsonData))
171172 }
172173
173− modelConfig := cfg.Models.Large
174− if g.providerOptions.modelType == config.SmallModel {
175− modelConfig = cfg.Models.Small
174+ modelConfig := cfg.Models[config.SelectedModelTypeLarge]
175+ if g.providerOptions.modelType == config.SelectedModelTypeSmall {
176+ modelConfig = cfg.Models[config.SelectedModelTypeSmall]
176177 }
177178
178179 maxTokens := model.DefaultMaxTokens
@@ -209,7 +210,7 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
209210 return nil, retryErr
210211 }
211212 if retry {
212− logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
213+ slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries))
213214 select {
214215 case <-ctx.Done():
215216 return nil, ctx.Err()
@@ -265,12 +266,12 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
265266 cfg := config.Get()
266267 if cfg.Options.Debug {
267268 jsonData, _ := json.Marshal(geminiMessages)
268− logging.Debug("Prepared messages", "messages", string(jsonData))
269+ slog.Debug("Prepared messages", "messages", string(jsonData))
269270 }
270271
271− modelConfig := cfg.Models.Large
272− if g.providerOptions.modelType == config.SmallModel {
273− modelConfig = cfg.Models.Small
272+ modelConfig := cfg.Models[config.SelectedModelTypeLarge]
273+ if g.providerOptions.modelType == config.SelectedModelTypeSmall {
274+ modelConfig = cfg.Models[config.SelectedModelTypeSmall]
274275 }
275276 maxTokens := model.DefaultMaxTokens
276277 if modelConfig.MaxTokens > 0 {
@@ -322,7 +323,7 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
322323 return
323324 }
324325 if retry {
325− logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
326+ slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries))
326327 select {
327328 case <-ctx.Done():
328329 if ctx.Err() != nil {
@@ -424,7 +425,7 @@ func (g *geminiClient) shouldRetry(attempts int, err error) (bool, int64, error)
424425
425426 // Check for token expiration (401 Unauthorized)
426427 if contains(errMsg, "unauthorized", "invalid api key", "api key expired") {
427− g.providerOptions.apiKey, err = config.ResolveAPIKey(g.providerOptions.config.APIKey)
428+ g.providerOptions.apiKey, err = config.Get().Resolve(g.providerOptions.config.APIKey)
428429 if err != nil {
429430 return false, 0, fmt.Errorf("failed to resolve API key: %w", err)
430431 }
@@ -462,7 +463,7 @@ func (g *geminiClient) usage(resp *genai.GenerateContentResponse) TokenUsage {
462463 }
463464}
464465
465−func (g *geminiClient) Model() config.Model {
466+func (g *geminiClient) Model() provider.Model {
466467 return g.providerOptions.model(g.providerOptions.modelType)
467468}
468469
internal/llm/provider/openai.go+12−15
@@ -6,12 +6,12 @@ import (
66 "errors"
77 "fmt"
88 "io"
9+ "log/slog"
910 "time"
1011
1112 "github.com/charmbracelet/crush/internal/config"
1213 "github.com/charmbracelet/crush/internal/fur/provider"
1314 "github.com/charmbracelet/crush/internal/llm/tools"
14− "github.com/charmbracelet/crush/internal/logging"
1515 "github.com/charmbracelet/crush/internal/message"
1616 "github.com/openai/openai-go"
1717 "github.com/openai/openai-go/option"
@@ -148,15 +148,12 @@ func (o *openaiClient) preparedParams(messages []openai.ChatCompletionMessagePar
148148 model := o.providerOptions.model(o.providerOptions.modelType)
149149 cfg := config.Get()
150150
151− modelConfig := cfg.Models.Large
152− if o.providerOptions.modelType == config.SmallModel {
153− modelConfig = cfg.Models.Small
151+ modelConfig := cfg.Models[config.SelectedModelTypeLarge]
152+ if o.providerOptions.modelType == config.SelectedModelTypeSmall {
153+ modelConfig = cfg.Models[config.SelectedModelTypeSmall]
154154 }
155155
156− reasoningEffort := model.ReasoningEffort
157− if modelConfig.ReasoningEffort != "" {
158− reasoningEffort = modelConfig.ReasoningEffort
159− }
156+ reasoningEffort := modelConfig.ReasoningEffort
160157
161158 params := openai.ChatCompletionNewParams{
162159 Model: openai.ChatModel(model.ID),
@@ -197,7 +194,7 @@ func (o *openaiClient) send(ctx context.Context, messages []message.Message, too
197194 cfg := config.Get()
198195 if cfg.Options.Debug {
199196 jsonData, _ := json.Marshal(params)
200− logging.Debug("Prepared messages", "messages", string(jsonData))
197+ slog.Debug("Prepared messages", "messages", string(jsonData))
201198 }
202199 attempts := 0
203200 for {
@@ -213,7 +210,7 @@ func (o *openaiClient) send(ctx context.Context, messages []message.Message, too
213210 return nil, retryErr
214211 }
215212 if retry {
216− logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
213+ slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries))
217214 select {
218215 case <-ctx.Done():
219216 return nil, ctx.Err()
@@ -254,7 +251,7 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
254251 cfg := config.Get()
255252 if cfg.Options.Debug {
256253 jsonData, _ := json.Marshal(params)
257− logging.Debug("Prepared messages", "messages", string(jsonData))
254+ slog.Debug("Prepared messages", "messages", string(jsonData))
258255 }
259256
260257 attempts := 0
@@ -291,7 +288,7 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
291288 if err == nil || errors.Is(err, io.EOF) {
292289 if cfg.Options.Debug {
293290 jsonData, _ := json.Marshal(acc.ChatCompletion)
294− logging.Debug("Response", "messages", string(jsonData))
291+ slog.Debug("Response", "messages", string(jsonData))
295292 }
296293 resultFinishReason := acc.ChatCompletion.Choices[0].FinishReason
297294 if resultFinishReason == "" {
@@ -329,7 +326,7 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
329326 return
330327 }
331328 if retry {
332− logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
329+ slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries))
333330 select {
334331 case <-ctx.Done():
335332 // context cancelled
@@ -363,7 +360,7 @@ func (o *openaiClient) shouldRetry(attempts int, err error) (bool, int64, error)
363360
364361 // Check for token expiration (401 Unauthorized)
365362 if apiErr.StatusCode == 401 {
366− o.providerOptions.apiKey, err = config.ResolveAPIKey(o.providerOptions.config.APIKey)
363+ o.providerOptions.apiKey, err = config.Get().Resolve(o.providerOptions.config.APIKey)
367364 if err != nil {
368365 return false, 0, fmt.Errorf("failed to resolve API key: %w", err)
369366 }
@@ -420,6 +417,6 @@ func (o *openaiClient) usage(completion openai.ChatCompletion) TokenUsage {
420417 }
421418}
422419
423−func (a *openaiClient) Model() config.Model {
420+func (a *openaiClient) Model() provider.Model {
424421 return a.providerOptions.model(a.providerOptions.modelType)
425422}
internal/llm/provider/provider.go+11−11
@@ -55,15 +55,15 @@ type Provider interface {
5555
5656 StreamResponse(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent
5757
58− Model() config.Model
58+ Model() provider.Model
5959}
6060
6161type providerClientOptions struct {
6262 baseURL string
6363 config config.ProviderConfig
6464 apiKey string
65− modelType config.ModelType
66− model func(config.ModelType) config.Model
65+ modelType config.SelectedModelType
66+ model func(config.SelectedModelType) provider.Model
6767 disableCache bool
6868 systemMessage string
6969 maxTokens int64
@@ -77,7 +77,7 @@ type ProviderClient interface {
7777 send(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error)
7878 stream(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent
7979
80− Model() config.Model
80+ Model() provider.Model
8181}
8282
8383type baseProvider[C ProviderClient] struct {
@@ -106,11 +106,11 @@ func (p *baseProvider[C]) StreamResponse(ctx context.Context, messages []message
106106 return p.client.stream(ctx, messages, tools)
107107}
108108
109−func (p *baseProvider[C]) Model() config.Model {
109+func (p *baseProvider[C]) Model() provider.Model {
110110 return p.client.Model()
111111}
112112
113−func WithModel(model config.ModelType) ProviderClientOption {
113+func WithModel(model config.SelectedModelType) ProviderClientOption {
114114 return func(options *providerClientOptions) {
115115 options.modelType = model
116116 }
@@ -135,7 +135,7 @@ func WithMaxTokens(maxTokens int64) ProviderClientOption {
135135}
136136
137137func NewProvider(cfg config.ProviderConfig, opts ...ProviderClientOption) (Provider, error) {
138− resolvedAPIKey, err := config.ResolveAPIKey(cfg.APIKey)
138+ resolvedAPIKey, err := config.Get().Resolve(cfg.APIKey)
139139 if err != nil {
140140 return nil, fmt.Errorf("failed to resolve API key for provider %s: %w", cfg.ID, err)
141141 }
@@ -145,14 +145,14 @@ func NewProvider(cfg config.ProviderConfig, opts ...ProviderClientOption) (Provi
145145 config: cfg,
146146 apiKey: resolvedAPIKey,
147147 extraHeaders: cfg.ExtraHeaders,
148− model: func(tp config.ModelType) config.Model {
149− return config.GetModel(tp)
148+ model: func(tp config.SelectedModelType) provider.Model {
149+ return *config.Get().GetModelByType(tp)
150150 },
151151 }
152152 for _, o := range opts {
153153 o(&clientOptions)
154154 }
155− switch cfg.ProviderType {
155+ switch cfg.Type {
156156 case provider.TypeAnthropic:
157157 return &baseProvider[AnthropicClient]{
158158 options: clientOptions,
@@ -190,5 +190,5 @@ func NewProvider(cfg config.ProviderConfig, opts ...ProviderClientOption) (Provi
190190 client: newOpenAIClient(clientOptions),
191191 }, nil
192192 }
193− return nil, fmt.Errorf("provider not supported: %s", cfg.ProviderType)
193+ return nil, fmt.Errorf("provider not supported: %s", cfg.Type)
194194}
internal/llm/provider/vertexai.go+2−2
@@ -2,8 +2,8 @@ package provider
22
33import (
44 "context"
5+ "log/slog"
56
6− "github.com/charmbracelet/crush/internal/logging"
77 "google.golang.org/genai"
88)
99
@@ -18,7 +18,7 @@ func newVertexAIClient(opts providerClientOptions) VertexAIClient {
1818 Backend: genai.BackendVertexAI,
1919 })
2020 if err != nil {
21− logging.Error("Failed to create VertexAI client", "error", err)
21+ slog.Error("Failed to create VertexAI client", "error", err)
2222 return nil
2323 }
2424
internal/llm/tools/bash.go+5−4
@@ -8,7 +8,6 @@ import (
88 "strings"
99 "time"
1010
11− "github.com/charmbracelet/crush/internal/config"
1211 "github.com/charmbracelet/crush/internal/permission"
1312 "github.com/charmbracelet/crush/internal/shell"
1413)
@@ -29,6 +28,7 @@ type BashResponseMetadata struct {
2928}
3029type bashTool struct {
3130 permissions permission.Service
31+ workingDir string
3232}
3333
3434const (
@@ -244,9 +244,10 @@ Important:
244244- Never update git config`, bannedCommandsStr, MaxOutputLength)
245245}
246246
247−func NewBashTool(permission permission.Service) BaseTool {
247+func NewBashTool(permission permission.Service, workingDir string) BaseTool {
248248 return &bashTool{
249249 permissions: permission,
250+ workingDir: workingDir,
250251 }
251252}
252253
@@ -317,7 +318,7 @@ func (b *bashTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
317318 p := b.permissions.Request(
318319 permission.CreatePermissionRequest{
319320 SessionID: sessionID,
320− Path: config.WorkingDirectory(),
321+ Path: b.workingDir,
321322 ToolName: BashToolName,
322323 Action: "execute",
323324 Description: fmt.Sprintf("Execute command: %s", params.Command),
@@ -337,7 +338,7 @@ func (b *bashTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
337338 defer cancel()
338339 }
339340 stdout, stderr, err := shell.
340− GetPersistentShell(config.WorkingDirectory()).
341+ GetPersistentShell(b.workingDir).
341342 Exec(ctx, params.Command)
342343 interrupted := shell.IsInterrupt(err)
343344 exitCode := shell.ExitCode(err)
internal/llm/tools/edit.go+17−16
@@ -4,15 +4,15 @@ import (
44 "context"
55 "encoding/json"
66 "fmt"
7+ "log/slog"
78 "os"
89 "path/filepath"
910 "strings"
1011 "time"
1112
12− "github.com/charmbracelet/crush/internal/config"
1313 "github.com/charmbracelet/crush/internal/diff"
1414 "github.com/charmbracelet/crush/internal/history"
15− "github.com/charmbracelet/crush/internal/logging"
15+
1616 "github.com/charmbracelet/crush/internal/lsp"
1717 "github.com/charmbracelet/crush/internal/permission"
1818)
@@ -40,6 +40,7 @@ type editTool struct {
4040 lspClients map[string]*lsp.Client
4141 permissions permission.Service
4242 files history.Service
43+ workingDir string
4344}
4445
4546const (
@@ -98,11 +99,12 @@ WINDOWS NOTES:
9899Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.`
99100)
100101
101−func NewEditTool(lspClients map[string]*lsp.Client, permissions permission.Service, files history.Service) BaseTool {
102+func NewEditTool(lspClients map[string]*lsp.Client, permissions permission.Service, files history.Service, workingDir string) BaseTool {
102103 return &editTool{
103104 lspClients: lspClients,
104105 permissions: permissions,
105106 files: files,
107+ workingDir: workingDir,
106108 }
107109}
108110
@@ -143,8 +145,7 @@ func (e *editTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
143145 }
144146
145147 if !filepath.IsAbs(params.FilePath) {
146− wd := config.WorkingDirectory()
147− params.FilePath = filepath.Join(wd, params.FilePath)
148+ params.FilePath = filepath.Join(e.workingDir, params.FilePath)
148149 }
149150
150151 var response ToolResponse
@@ -205,9 +206,9 @@ func (e *editTool) createNewFile(ctx context.Context, filePath, content string)
205206 _, additions, removals := diff.GenerateDiff(
206207 "",
207208 content,
208− filePath,
209+ strings.TrimPrefix(filePath, e.workingDir),
209210 )
210− rootDir := config.WorkingDirectory()
211+ rootDir := e.workingDir
211212 permissionPath := filepath.Dir(filePath)
212213 if strings.HasPrefix(filePath, rootDir) {
213214 permissionPath = rootDir
@@ -246,7 +247,7 @@ func (e *editTool) createNewFile(ctx context.Context, filePath, content string)
246247 _, err = e.files.CreateVersion(ctx, sessionID, filePath, content)
247248 if err != nil {
248249 // Log error but don't fail the operation
249− logging.Debug("Error creating file history version", "error", err)
250+ slog.Debug("Error creating file history version", "error", err)
250251 }
251252
252253 recordFileWrite(filePath)
@@ -317,10 +318,10 @@ func (e *editTool) deleteContent(ctx context.Context, filePath, oldString string
317318 _, additions, removals := diff.GenerateDiff(
318319 oldContent,
319320 newContent,
320− filePath,
321+ strings.TrimPrefix(filePath, e.workingDir),
321322 )
322323
323− rootDir := config.WorkingDirectory()
324+ rootDir := e.workingDir
324325 permissionPath := filepath.Dir(filePath)
325326 if strings.HasPrefix(filePath, rootDir) {
326327 permissionPath = rootDir
@@ -361,13 +362,13 @@ func (e *editTool) deleteContent(ctx context.Context, filePath, oldString string
361362 // User Manually changed the content store an intermediate version
362363 _, err = e.files.CreateVersion(ctx, sessionID, filePath, oldContent)
363364 if err != nil {
364− logging.Debug("Error creating file history version", "error", err)
365+ slog.Debug("Error creating file history version", "error", err)
365366 }
366367 }
367368 // Store the new version
368369 _, err = e.files.CreateVersion(ctx, sessionID, filePath, "")
369370 if err != nil {
370− logging.Debug("Error creating file history version", "error", err)
371+ slog.Debug("Error creating file history version", "error", err)
371372 }
372373
373374 recordFileWrite(filePath)
@@ -440,9 +441,9 @@ func (e *editTool) replaceContent(ctx context.Context, filePath, oldString, newS
440441 _, additions, removals := diff.GenerateDiff(
441442 oldContent,
442443 newContent,
443− filePath,
444+ strings.TrimPrefix(filePath, e.workingDir),
444445 )
445− rootDir := config.WorkingDirectory()
446+ rootDir := e.workingDir
446447 permissionPath := filepath.Dir(filePath)
447448 if strings.HasPrefix(filePath, rootDir) {
448449 permissionPath = rootDir
@@ -483,13 +484,13 @@ func (e *editTool) replaceContent(ctx context.Context, filePath, oldString, newS
483484 // User Manually changed the content store an intermediate version
484485 _, err = e.files.CreateVersion(ctx, sessionID, filePath, oldContent)
485486 if err != nil {
486− logging.Debug("Error creating file history version", "error", err)
487+ slog.Debug("Error creating file history version", "error", err)
487488 }
488489 }
489490 // Store the new version
490491 _, err = e.files.CreateVersion(ctx, sessionID, filePath, newContent)
491492 if err != nil {
492− logging.Debug("Error creating file history version", "error", err)
493+ slog.Debug("Error creating file history version", "error", err)
493494 }
494495
495496 recordFileWrite(filePath)
internal/llm/tools/fetch.go+4−3
@@ -11,7 +11,6 @@ import (
1111
1212 md "github.com/JohannesKaufmann/html-to-markdown"
1313 "github.com/PuerkitoBio/goquery"
14− "github.com/charmbracelet/crush/internal/config"
1514 "github.com/charmbracelet/crush/internal/permission"
1615)
1716
@@ -30,6 +29,7 @@ type FetchPermissionsParams struct {
3029type fetchTool struct {
3130 client *http.Client
3231 permissions permission.Service
32+ workingDir string
3333}
3434
3535const (
@@ -65,7 +65,7 @@ TIPS:
6565- Set appropriate timeouts for potentially slow websites`
6666)
6767
68−func NewFetchTool(permissions permission.Service) BaseTool {
68+func NewFetchTool(permissions permission.Service, workingDir string) BaseTool {
6969 return &fetchTool{
7070 client: &http.Client{
7171 Timeout: 30 * time.Second,
@@ -76,6 +76,7 @@ func NewFetchTool(permissions permission.Service) BaseTool {
7676 },
7777 },
7878 permissions: permissions,
79+ workingDir: workingDir,
7980 }
8081}
8182
@@ -133,7 +134,7 @@ func (t *fetchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
133134 p := t.permissions.Request(
134135 permission.CreatePermissionRequest{
135136 SessionID: sessionID,
136− Path: config.WorkingDirectory(),
137+ Path: t.workingDir,
137138 ToolName: FetchToolName,
138139 Action: "fetch",
139140 Description: fmt.Sprintf("Fetch content from URL: %s", params.URL),
internal/llm/tools/glob.go+10−7
@@ -5,14 +5,13 @@ import (
55 "context"
66 "encoding/json"
77 "fmt"
8+ "log/slog"
89 "os/exec"
910 "path/filepath"
1011 "sort"
1112 "strings"
1213
13− "github.com/charmbracelet/crush/internal/config"
1414 "github.com/charmbracelet/crush/internal/fsext"
15− "github.com/charmbracelet/crush/internal/logging"
1615)
1716
1817const (
@@ -68,10 +67,14 @@ type GlobResponseMetadata struct {
6867 Truncated bool `json:"truncated"`
6968}
7069
71−type globTool struct{}
70+type globTool struct {
71+ workingDir string
72+}
7273
73−func NewGlobTool() BaseTool {
74− return &globTool{}
74+func NewGlobTool(workingDir string) BaseTool {
75+ return &globTool{
76+ workingDir: workingDir,
77+ }
7578}
7679
7780func (g *globTool) Name() string {
@@ -108,7 +111,7 @@ func (g *globTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
108111
109112 searchPath := params.Path
110113 if searchPath == "" {
111− searchPath = config.WorkingDirectory()
114+ searchPath = g.workingDir
112115 }
113116
114117 files, truncated, err := globFiles(params.Pattern, searchPath, 100)
@@ -143,7 +146,7 @@ func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
143146 if err == nil {
144147 return matches, len(matches) >= limit && limit > 0, nil
145148 }
146− logging.Warn(fmt.Sprintf("Ripgrep execution failed: %v. Falling back to doublestar.", err))
149+ slog.Warn(fmt.Sprintf("Ripgrep execution failed: %v. Falling back to doublestar.", err))
147150 }
148151
149152 return fsext.GlobWithDoubleStar(pattern, searchPath, limit)
internal/llm/tools/grep.go+8−5
@@ -16,7 +16,6 @@ import (
1616 "sync"
1717 "time"
1818
19− "github.com/charmbracelet/crush/internal/config"
2019 "github.com/charmbracelet/crush/internal/fsext"
2120)
2221
@@ -89,7 +88,9 @@ type GrepResponseMetadata struct {
8988 Truncated bool `json:"truncated"`
9089}
9190
92−type grepTool struct{}
91+type grepTool struct {
92+ workingDir string
93+}
9394
9495const (
9596 GrepToolName = "grep"
@@ -136,8 +137,10 @@ TIPS:
136137- Use literal_text=true when searching for exact text containing special characters like dots, parentheses, etc.`
137138)
138139
139−func NewGrepTool() BaseTool {
140− return &grepTool{}
140+func NewGrepTool(workingDir string) BaseTool {
141+ return &grepTool{
142+ workingDir: workingDir,
143+ }
141144}
142145
143146func (g *grepTool) Name() string {
@@ -200,7 +203,7 @@ func (g *grepTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
200203
201204 searchPath := params.Path
202205 if searchPath == "" {
203− searchPath = config.WorkingDirectory()
206+ searchPath = g.workingDir
204207 }
205208
206209 matches, truncated, err := searchFiles(searchPattern, searchPath, params.Include, 100)
internal/llm/tools/ls.go+9−6
@@ -8,7 +8,6 @@ import (
88 "path/filepath"
99 "strings"
1010
11− "github.com/charmbracelet/crush/internal/config"
1211 "github.com/charmbracelet/crush/internal/fsext"
1312)
1413
@@ -29,7 +28,9 @@ type LSResponseMetadata struct {
2928 Truncated bool `json:"truncated"`
3029}
3130
32−type lsTool struct{}
31+type lsTool struct {
32+ workingDir string
33+}
3334
3435const (
3536 LSToolName = "ls"
@@ -70,8 +71,10 @@ TIPS:
7071- Combine with other tools for more effective exploration`
7172)
7273
73−func NewLsTool() BaseTool {
74− return &lsTool{}
74+func NewLsTool(workingDir string) BaseTool {
75+ return &lsTool{
76+ workingDir: workingDir,
77+ }
7578}
7679
7780func (l *lsTool) Name() string {
@@ -107,11 +110,11 @@ func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
107110
108111 searchPath := params.Path
109112 if searchPath == "" {
110− searchPath = config.WorkingDirectory()
113+ searchPath = l.workingDir
111114 }
112115
113116 if !filepath.IsAbs(searchPath) {
114− searchPath = filepath.Join(config.WorkingDirectory(), searchPath)
117+ searchPath = filepath.Join(l.workingDir, searchPath)
115118 }
116119
117120 if _, err := os.Stat(searchPath); os.IsNotExist(err) {
internal/llm/tools/view.go+5−4
@@ -10,7 +10,6 @@ import (
1010 "path/filepath"
1111 "strings"
1212
13− "github.com/charmbracelet/crush/internal/config"
1413 "github.com/charmbracelet/crush/internal/lsp"
1514)
1615
@@ -22,6 +21,7 @@ type ViewParams struct {
2221
2322type viewTool struct {
2423 lspClients map[string]*lsp.Client
24+ workingDir string
2525}
2626
2727type ViewResponseMetadata struct {
@@ -71,9 +71,10 @@ TIPS:
7171- When viewing large files, use the offset parameter to read specific sections`
7272)
7373
74−func NewViewTool(lspClients map[string]*lsp.Client) BaseTool {
74+func NewViewTool(lspClients map[string]*lsp.Client, workingDir string) BaseTool {
7575 return &viewTool{
76− lspClients,
76+ lspClients: lspClients,
77+ workingDir: workingDir,
7778 }
7879}
7980
@@ -117,7 +118,7 @@ func (v *viewTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
117118 // Handle relative paths
118119 filePath := params.FilePath
119120 if !filepath.IsAbs(filePath) {
120− filePath = filepath.Join(config.WorkingDirectory(), filePath)
121+ filePath = filepath.Join(v.workingDir, filePath)
121122 }
122123
123124 // Check if file exists
internal/llm/tools/write.go+10−8
@@ -4,15 +4,15 @@ import (
44 "context"
55 "encoding/json"
66 "fmt"
7+ "log/slog"
78 "os"
89 "path/filepath"
910 "strings"
1011 "time"
1112
12− "github.com/charmbracelet/crush/internal/config"
1313 "github.com/charmbracelet/crush/internal/diff"
1414 "github.com/charmbracelet/crush/internal/history"
15− "github.com/charmbracelet/crush/internal/logging"
15+
1616 "github.com/charmbracelet/crush/internal/lsp"
1717 "github.com/charmbracelet/crush/internal/permission"
1818)
@@ -32,6 +32,7 @@ type writeTool struct {
3232 lspClients map[string]*lsp.Client
3333 permissions permission.Service
3434 files history.Service
35+ workingDir string
3536}
3637
3738type WriteResponseMetadata struct {
@@ -76,11 +77,12 @@ TIPS:
7677- Always include descriptive comments when making changes to existing code`
7778)
7879
79−func NewWriteTool(lspClients map[string]*lsp.Client, permissions permission.Service, files history.Service) BaseTool {
80+func NewWriteTool(lspClients map[string]*lsp.Client, permissions permission.Service, files history.Service, workingDir string) BaseTool {
8081 return &writeTool{
8182 lspClients: lspClients,
8283 permissions: permissions,
8384 files: files,
85+ workingDir: workingDir,
8486 }
8587}
8688
@@ -122,7 +124,7 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
122124
123125 filePath := params.FilePath
124126 if !filepath.IsAbs(filePath) {
125− filePath = filepath.Join(config.WorkingDirectory(), filePath)
127+ filePath = filepath.Join(w.workingDir, filePath)
126128 }
127129
128130 fileInfo, err := os.Stat(filePath)
@@ -167,10 +169,10 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
167169 diff, additions, removals := diff.GenerateDiff(
168170 oldContent,
169171 params.Content,
170− filePath,
172+ strings.TrimPrefix(filePath, w.workingDir),
171173 )
172174
173− rootDir := config.WorkingDirectory()
175+ rootDir := w.workingDir
174176 permissionPath := filepath.Dir(filePath)
175177 if strings.HasPrefix(filePath, rootDir) {
176178 permissionPath = rootDir
@@ -211,13 +213,13 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
211213 // User Manually changed the content store an intermediate version
212214 _, err = w.files.CreateVersion(ctx, sessionID, filePath, oldContent)
213215 if err != nil {
214− logging.Debug("Error creating file history version", "error", err)
216+ slog.Debug("Error creating file history version", "error", err)
215217 }
216218 }
217219 // Store the new version
218220 _, err = w.files.CreateVersion(ctx, sessionID, filePath, params.Content)
219221 if err != nil {
220− logging.Debug("Error creating file history version", "error", err)
222+ slog.Debug("Error creating file history version", "error", err)
221223 }
222224
223225 recordFileWrite(filePath)
internal/log/log.gonew file+61
@@ -0,0 +1,61 @@
1+package log
2+
3+import (
4+ "fmt"
5+ "log/slog"
6+ "os"
7+ "runtime/debug"
8+ "sync"
9+ "time"
10+
11+ "gopkg.in/natefinch/lumberjack.v2"
12+)
13+
14+var initOnce sync.Once
15+
16+func Init(logFile string, debug bool) {
17+ initOnce.Do(func() {
18+ logRotator := &lumberjack.Logger{
19+ Filename: logFile,
20+ MaxSize: 10, // Max size in MB
21+ MaxBackups: 0, // Number of backups
22+ MaxAge: 30, // Days
23+ Compress: false, // Enable compression
24+ }
25+
26+ level := slog.LevelInfo
27+ if debug {
28+ level = slog.LevelDebug
29+ }
30+
31+ logger := slog.NewJSONHandler(logRotator, &slog.HandlerOptions{
32+ Level: level,
33+ AddSource: true,
34+ })
35+
36+ slog.SetDefault(slog.New(logger))
37+ })
38+}
39+
40+func RecoverPanic(name string, cleanup func()) {
41+ if r := recover(); r != nil {
42+ // Create a timestamped panic log file
43+ timestamp := time.Now().Format("20060102-150405")
44+ filename := fmt.Sprintf("crush-panic-%s-%s.log", name, timestamp)
45+
46+ file, err := os.Create(filename)
47+ if err == nil {
48+ defer file.Close()
49+
50+ // Write panic information and stack trace
51+ fmt.Fprintf(file, "Panic in %s: %v\n\n", name, r)
52+ fmt.Fprintf(file, "Time: %s\n\n", time.Now().Format(time.RFC3339))
53+ fmt.Fprintf(file, "Stack Trace:\n%s\n", debug.Stack())
54+
55+ // Execute cleanup function if provided
56+ if cleanup != nil {
57+ cleanup()
58+ }
59+ }
60+ }
61+}
internal/logging/logger.godeleted−209
@@ -1,209 +0,0 @@
1−package logging
2−
3−import (
4− "fmt"
5− "log/slog"
6− "os"
7−
8− // "path/filepath"
9− "encoding/json"
10− "runtime"
11− "runtime/debug"
12− "sync"
13− "time"
14−)
15−
16−func getCaller() string {
17− var caller string
18− if _, file, line, ok := runtime.Caller(2); ok {
19− // caller = fmt.Sprintf("%s:%d", filepath.Base(file), line)
20− caller = fmt.Sprintf("%s:%d", file, line)
21− } else {
22− caller = "unknown"
23− }
24− return caller
25−}
26−
27−func Info(msg string, args ...any) {
28− source := getCaller()
29− slog.Info(msg, append([]any{"source", source}, args...)...)
30−}
31−
32−func Debug(msg string, args ...any) {
33− // slog.Debug(msg, args...)
34− source := getCaller()
35− slog.Debug(msg, append([]any{"source", source}, args...)...)
36−}
37−
38−func Warn(msg string, args ...any) {
39− slog.Warn(msg, args...)
40−}
41−
42−func Error(msg string, args ...any) {
43− slog.Error(msg, args...)
44−}
45−
46−func InfoPersist(msg string, args ...any) {
47− args = append(args, persistKeyArg, true)
48− slog.Info(msg, args...)
49−}
50−
51−func DebugPersist(msg string, args ...any) {
52− args = append(args, persistKeyArg, true)
53− slog.Debug(msg, args...)
54−}
55−
56−func WarnPersist(msg string, args ...any) {
57− args = append(args, persistKeyArg, true)
58− slog.Warn(msg, args...)
59−}
60−
61−func ErrorPersist(msg string, args ...any) {
62− args = append(args, persistKeyArg, true)
63− slog.Error(msg, args...)
64−}
65−
66−// RecoverPanic is a common function to handle panics gracefully.
67−// It logs the error, creates a panic log file with stack trace,
68−// and executes an optional cleanup function before returning.
69−func RecoverPanic(name string, cleanup func()) {
70− if r := recover(); r != nil {
71− // Log the panic
72− ErrorPersist(fmt.Sprintf("Panic in %s: %v", name, r))
73−
74− // Create a timestamped panic log file
75− timestamp := time.Now().Format("20060102-150405")
76− filename := fmt.Sprintf("crush-panic-%s-%s.log", name, timestamp)
77−
78− file, err := os.Create(filename)
79− if err != nil {
80− ErrorPersist(fmt.Sprintf("Failed to create panic log: %v", err))
81− } else {
82− defer file.Close()
83−
84− // Write panic information and stack trace
85− fmt.Fprintf(file, "Panic in %s: %v\n\n", name, r)
86− fmt.Fprintf(file, "Time: %s\n\n", time.Now().Format(time.RFC3339))
87− fmt.Fprintf(file, "Stack Trace:\n%s\n", debug.Stack())
88−
89− InfoPersist(fmt.Sprintf("Panic details written to %s", filename))
90− }
91−
92− // Execute cleanup function if provided
93− if cleanup != nil {
94− cleanup()
95− }
96− }
97−}
98−
99−// Message Logging for Debug
100−var MessageDir string
101−
102−func GetSessionPrefix(sessionId string) string {
103− return sessionId[:8]
104−}
105−
106−var sessionLogMutex sync.Mutex
107−
108−func AppendToSessionLogFile(sessionId string, filename string, content string) string {
109− if MessageDir == "" || sessionId == "" {
110− return ""
111− }
112− sessionPrefix := GetSessionPrefix(sessionId)
113−
114− sessionLogMutex.Lock()
115− defer sessionLogMutex.Unlock()
116−
117− sessionPath := fmt.Sprintf("%s/%s", MessageDir, sessionPrefix)
118− if _, err := os.Stat(sessionPath); os.IsNotExist(err) {
119− if err := os.MkdirAll(sessionPath, 0o766); err != nil {
120− Error("Failed to create session directory", "dirpath", sessionPath, "error", err)
121− return ""
122− }
123− }
124−
125− filePath := fmt.Sprintf("%s/%s", sessionPath, filename)
126−
127− f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
128− if err != nil {
129− Error("Failed to open session log file", "filepath", filePath, "error", err)
130− return ""
131− }
132− defer f.Close()
133−
134− // Append chunk to file
135− _, err = f.WriteString(content)
136− if err != nil {
137− Error("Failed to write chunk to session log file", "filepath", filePath, "error", err)
138− return ""
139− }
140− return filePath
141−}
142−
143−func WriteRequestMessageJson(sessionId string, requestSeqId int, message any) string {
144− if MessageDir == "" || sessionId == "" || requestSeqId <= 0 {
145− return ""
146− }
147− msgJson, err := json.Marshal(message)
148− if err != nil {
149− Error("Failed to marshal message", "session_id", sessionId, "request_seq_id", requestSeqId, "error", err)
150− return ""
151− }
152− return WriteRequestMessage(sessionId, requestSeqId, string(msgJson))
153−}
154−
155−func WriteRequestMessage(sessionId string, requestSeqId int, message string) string {
156− if MessageDir == "" || sessionId == "" || requestSeqId <= 0 {
157− return ""
158− }
159− filename := fmt.Sprintf("%d_request.json", requestSeqId)
160−
161− return AppendToSessionLogFile(sessionId, filename, message)
162−}
163−
164−func AppendToStreamSessionLogJson(sessionId string, requestSeqId int, jsonableChunk any) string {
165− if MessageDir == "" || sessionId == "" || requestSeqId <= 0 {
166− return ""
167− }
168− chunkJson, err := json.Marshal(jsonableChunk)
169− if err != nil {
170− Error("Failed to marshal message", "session_id", sessionId, "request_seq_id", requestSeqId, "error", err)
171− return ""
172− }
173− return AppendToStreamSessionLog(sessionId, requestSeqId, string(chunkJson))
174−}
175−
176−func AppendToStreamSessionLog(sessionId string, requestSeqId int, chunk string) string {
177− if MessageDir == "" || sessionId == "" || requestSeqId <= 0 {
178− return ""
179− }
180− filename := fmt.Sprintf("%d_response_stream.log", requestSeqId)
181− return AppendToSessionLogFile(sessionId, filename, chunk)
182−}
183−
184−func WriteChatResponseJson(sessionId string, requestSeqId int, response any) string {
185− if MessageDir == "" || sessionId == "" || requestSeqId <= 0 {
186− return ""
187− }
188− responseJson, err := json.Marshal(response)
189− if err != nil {
190− Error("Failed to marshal response", "session_id", sessionId, "request_seq_id", requestSeqId, "error", err)
191− return ""
192− }
193− filename := fmt.Sprintf("%d_response.json", requestSeqId)
194−
195− return AppendToSessionLogFile(sessionId, filename, string(responseJson))
196−}
197−
198−func WriteToolResultsJson(sessionId string, requestSeqId int, toolResults any) string {
199− if MessageDir == "" || sessionId == "" || requestSeqId <= 0 {
200− return ""
201− }
202− toolResultsJson, err := json.Marshal(toolResults)
203− if err != nil {
204− Error("Failed to marshal tool results", "session_id", sessionId, "request_seq_id", requestSeqId, "error", err)
205− return ""
206− }
207− filename := fmt.Sprintf("%d_tool_results.json", requestSeqId)
208− return AppendToSessionLogFile(sessionId, filename, string(toolResultsJson))
209−}
internal/logging/message.godeleted−21
@@ -1,21 +0,0 @@
1−package logging
2−
3−import (
4− "time"
5−)
6−
7−// LogMessage is the event payload for a log message
8−type LogMessage struct {
9− ID string
10− Time time.Time
11− Level string
12− Persist bool // used when we want to show the mesage in the status bar
13− PersistTime time.Duration // used when we want to show the mesage in the status bar
14− Message string `json:"msg"`
15− Attributes []Attr
16−}
17−
18−type Attr struct {
19− Key string
20− Value string
21−}
internal/logging/writer.godeleted−102
@@ -1,102 +0,0 @@
1−package logging
2−
3−import (
4− "bytes"
5− "context"
6− "fmt"
7− "strings"
8− "sync"
9− "time"
10−
11− "github.com/charmbracelet/crush/internal/pubsub"
12− "github.com/go-logfmt/logfmt"
13−)
14−
15−const (
16− persistKeyArg = "$_persist"
17− PersistTimeArg = "$_persist_time"
18−)
19−
20−type LogData struct {
21− messages []LogMessage
22− *pubsub.Broker[LogMessage]
23− lock sync.Mutex
24−}
25−
26−func (l *LogData) Add(msg LogMessage) {
27− l.lock.Lock()
28− defer l.lock.Unlock()
29− l.messages = append(l.messages, msg)
30− l.Publish(pubsub.CreatedEvent, msg)
31−}
32−
33−func (l *LogData) List() []LogMessage {
34− l.lock.Lock()
35− defer l.lock.Unlock()
36− return l.messages
37−}
38−
39−var defaultLogData = &LogData{
40− messages: make([]LogMessage, 0),
41− Broker: pubsub.NewBroker[LogMessage](),
42−}
43−
44−type writer struct{}
45−
46−func (w *writer) Write(p []byte) (int, error) {
47− d := logfmt.NewDecoder(bytes.NewReader(p))
48−
49− for d.ScanRecord() {
50− msg := LogMessage{
51− ID: fmt.Sprintf("%d", time.Now().UnixNano()),
52− Time: time.Now(),
53− }
54− for d.ScanKeyval() {
55− switch string(d.Key()) {
56− case "time":
57− parsed, err := time.Parse(time.RFC3339, string(d.Value()))
58− if err != nil {
59− return 0, fmt.Errorf("parsing time: %w", err)
60− }
61− msg.Time = parsed
62− case "level":
63− msg.Level = strings.ToLower(string(d.Value()))
64− case "msg":
65− msg.Message = string(d.Value())
66− default:
67− if string(d.Key()) == persistKeyArg {
68− msg.Persist = true
69− } else if string(d.Key()) == PersistTimeArg {
70− parsed, err := time.ParseDuration(string(d.Value()))
71− if err != nil {
72− continue
73− }
74− msg.PersistTime = parsed
75− } else {
76− msg.Attributes = append(msg.Attributes, Attr{
77− Key: string(d.Key()),
78− Value: string(d.Value()),
79− })
80− }
81− }
82− }
83− defaultLogData.Add(msg)
84− }
85− if d.Err() != nil {
86− return 0, d.Err()
87− }
88− return len(p), nil
89−}
90−
91−func NewWriter() *writer {
92− w := &writer{}
93− return w
94−}
95−
96−func Subscribe(ctx context.Context) <-chan pubsub.Event[LogMessage] {
97− return defaultLogData.Subscribe(ctx)
98−}
99−
100−func List() []LogMessage {
101− return defaultLogData.List()
102−}
internal/lsp/client.go+22−21
@@ -6,6 +6,7 @@ import (
66 "encoding/json"
77 "fmt"
88 "io"
9+ "log/slog"
910 "os"
1011 "os/exec"
1112 "path/filepath"
@@ -15,7 +16,7 @@ import (
1516 "time"
1617
1718 "github.com/charmbracelet/crush/internal/config"
18− "github.com/charmbracelet/crush/internal/logging"
19+ "github.com/charmbracelet/crush/internal/log"
1920 "github.com/charmbracelet/crush/internal/lsp/protocol"
2021)
2122
@@ -96,17 +97,17 @@ func NewClient(ctx context.Context, command string, args ...string) (*Client, er
9697 go func() {
9798 scanner := bufio.NewScanner(stderr)
9899 for scanner.Scan() {
99− logging.Error("LSP Server", "err", scanner.Text())
100+ slog.Error("LSP Server", "err", scanner.Text())
100101 }
101102 if err := scanner.Err(); err != nil {
102− logging.Error("Error reading", "err", err)
103+ slog.Error("Error reading", "err", err)
103104 }
104105 }()
105106
106107 // Start message handling loop
107108 go func() {
108− defer logging.RecoverPanic("LSP-message-handler", func() {
109− logging.ErrorPersist("LSP message handler crashed, LSP functionality may be impaired")
109+ defer log.RecoverPanic("LSP-message-handler", func() {
110+ slog.Error("LSP message handler crashed, LSP functionality may be impaired")
110111 })
111112 client.handleMessages()
112113 }()
@@ -300,7 +301,7 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
300301 defer ticker.Stop()
301302
302303 if cfg.Options.DebugLSP {
303− logging.Debug("Waiting for LSP server to be ready...")
304+ slog.Debug("Waiting for LSP server to be ready...")
304305 }
305306
306307 // Determine server type for specialized initialization
@@ -309,7 +310,7 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
309310 // For TypeScript-like servers, we need to open some key files first
310311 if serverType == ServerTypeTypeScript {
311312 if cfg.Options.DebugLSP {
312− logging.Debug("TypeScript-like server detected, opening key configuration files")
313+ slog.Debug("TypeScript-like server detected, opening key configuration files")
313314 }
314315 c.openKeyConfigFiles(ctx)
315316 }
@@ -326,15 +327,15 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
326327 // Server responded successfully
327328 c.SetServerState(StateReady)
328329 if cfg.Options.DebugLSP {
329− logging.Debug("LSP server is ready")
330+ slog.Debug("LSP server is ready")
330331 }
331332 return nil
332333 } else {
333− logging.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
334+ slog.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
334335 }
335336
336337 if cfg.Options.DebugLSP {
337− logging.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
338+ slog.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
338339 }
339340 }
340341 }
@@ -376,7 +377,7 @@ func (c *Client) detectServerType() ServerType {
376377
377378// openKeyConfigFiles opens important configuration files that help initialize the server
378379func (c *Client) openKeyConfigFiles(ctx context.Context) {
379− workDir := config.WorkingDirectory()
380+ workDir := config.Get().WorkingDir()
380381 serverType := c.detectServerType()
381382
382383 var filesToOpen []string
@@ -409,9 +410,9 @@ func (c *Client) openKeyConfigFiles(ctx context.Context) {
409410 if _, err := os.Stat(file); err == nil {
410411 // File exists, try to open it
411412 if err := c.OpenFile(ctx, file); err != nil {
412− logging.Debug("Failed to open key config file", "file", file, "error", err)
413+ slog.Debug("Failed to open key config file", "file", file, "error", err)
413414 } else {
414− logging.Debug("Opened key config file for initialization", "file", file)
415+ slog.Debug("Opened key config file for initialization", "file", file)
415416 }
416417 }
417418 }
@@ -464,7 +465,7 @@ func (c *Client) pingTypeScriptServer(ctx context.Context) error {
464465 }
465466
466467 // If we have no open TypeScript files, try to find and open one
467− workDir := config.WorkingDirectory()
468+ workDir := config.Get().WorkingDir()
468469 err := filepath.WalkDir(workDir, func(path string, d os.DirEntry, err error) error {
469470 if err != nil {
470471 return err
@@ -487,7 +488,7 @@ func (c *Client) pingTypeScriptServer(ctx context.Context) error {
487488 return nil
488489 })
489490 if err != nil {
490− logging.Debug("Error walking directory for TypeScript files", "error", err)
491+ slog.Debug("Error walking directory for TypeScript files", "error", err)
491492 }
492493
493494 // Final fallback - just try a generic capability
@@ -527,7 +528,7 @@ func (c *Client) openTypeScriptFiles(ctx context.Context, workDir string) {
527528 if err := c.OpenFile(ctx, path); err == nil {
528529 filesOpened++
529530 if cfg.Options.DebugLSP {
530− logging.Debug("Opened TypeScript file for initialization", "file", path)
531+ slog.Debug("Opened TypeScript file for initialization", "file", path)
531532 }
532533 }
533534 }
@@ -536,11 +537,11 @@ func (c *Client) openTypeScriptFiles(ctx context.Context, workDir string) {
536537 })
537538
538539 if err != nil && cfg.Options.DebugLSP {
539− logging.Debug("Error walking directory for TypeScript files", "error", err)
540+ slog.Debug("Error walking directory for TypeScript files", "error", err)
540541 }
541542
542543 if cfg.Options.DebugLSP {
543− logging.Debug("Opened TypeScript files for initialization", "count", filesOpened)
544+ slog.Debug("Opened TypeScript files for initialization", "count", filesOpened)
544545 }
545546}
546547
@@ -681,7 +682,7 @@ func (c *Client) CloseFile(ctx context.Context, filepath string) error {
681682 }
682683
683684 if cfg.Options.DebugLSP {
684− logging.Debug("Closing file", "file", filepath)
685+ slog.Debug("Closing file", "file", filepath)
685686 }
686687 if err := c.Notify(ctx, "textDocument/didClose", params); err != nil {
687688 return err
@@ -720,12 +721,12 @@ func (c *Client) CloseAllFiles(ctx context.Context) {
720721 for _, filePath := range filesToClose {
721722 err := c.CloseFile(ctx, filePath)
722723 if err != nil && cfg.Options.DebugLSP {
723− logging.Warn("Error closing file", "file", filePath, "error", err)
724+ slog.Warn("Error closing file", "file", filePath, "error", err)
724725 }
725726 }
726727
727728 if cfg.Options.DebugLSP {
728− logging.Debug("Closed all files", "files", filesToClose)
729+ slog.Debug("Closed all files", "files", filesToClose)
729730 }
730731}
731732
internal/lsp/handlers.go+8−7
@@ -2,9 +2,10 @@ package lsp
22
33import (
44 "encoding/json"
5+ "log/slog"
56
67 "github.com/charmbracelet/crush/internal/config"
7− "github.com/charmbracelet/crush/internal/logging"
8+
89 "github.com/charmbracelet/crush/internal/lsp/protocol"
910 "github.com/charmbracelet/crush/internal/lsp/util"
1011)
@@ -18,7 +19,7 @@ func HandleWorkspaceConfiguration(params json.RawMessage) (any, error) {
1819func HandleRegisterCapability(params json.RawMessage) (any, error) {
1920 var registerParams protocol.RegistrationParams
2021 if err := json.Unmarshal(params, ®isterParams); err != nil {
21− logging.Error("Error unmarshaling registration params", "error", err)
22+ slog.Error("Error unmarshaling registration params", "error", err)
2223 return nil, err
2324 }
2425
@@ -28,13 +29,13 @@ func HandleRegisterCapability(params json.RawMessage) (any, error) {
2829 // Parse the registration options
2930 optionsJSON, err := json.Marshal(reg.RegisterOptions)
3031 if err != nil {
31− logging.Error("Error marshaling registration options", "error", err)
32+ slog.Error("Error marshaling registration options", "error", err)
3233 continue
3334 }
3435
3536 var options protocol.DidChangeWatchedFilesRegistrationOptions
3637 if err := json.Unmarshal(optionsJSON, &options); err != nil {
37− logging.Error("Error unmarshaling registration options", "error", err)
38+ slog.Error("Error unmarshaling registration options", "error", err)
3839 continue
3940 }
4041
@@ -54,7 +55,7 @@ func HandleApplyEdit(params json.RawMessage) (any, error) {
5455
5556 err := util.ApplyWorkspaceEdit(edit.Edit)
5657 if err != nil {
57− logging.Error("Error applying workspace edit", "error", err)
58+ slog.Error("Error applying workspace edit", "error", err)
5859 return protocol.ApplyWorkspaceEditResult{Applied: false, FailureReason: err.Error()}, nil
5960 }
6061
@@ -89,7 +90,7 @@ func HandleServerMessage(params json.RawMessage) {
8990 }
9091 if err := json.Unmarshal(params, &msg); err == nil {
9192 if cfg.Options.DebugLSP {
92− logging.Debug("Server message", "type", msg.Type, "message", msg.Message)
93+ slog.Debug("Server message", "type", msg.Type, "message", msg.Message)
9394 }
9495 }
9596}
@@ -97,7 +98,7 @@ func HandleServerMessage(params json.RawMessage) {
9798func HandleDiagnostics(client *Client, params json.RawMessage) {
9899 var diagParams protocol.PublishDiagnosticsParams
99100 if err := json.Unmarshal(params, &diagParams); err != nil {
100− logging.Error("Error unmarshaling diagnostics params", "error", err)
101+ slog.Error("Error unmarshaling diagnostics params", "error", err)
101102 return
102103 }
103104
internal/lsp/protocol/tsprotocol.go+1−1
@@ -55,7 +55,7 @@ type ApplyWorkspaceEditResult struct {
5555 // Indicates whether the edit was applied or not.
5656 Applied bool `json:"applied"`
5757 // An optional textual description for why the edit was not applied.
58− // This may be used by the server for diagnostic logging or to provide
58+ // This may be used by the server for diagnostic slog.or to provide
5959 // a suitable error for a request that triggered the edit.
6060 FailureReason string `json:"failureReason,omitempty"`
6161 // Depending on the client's failure handling strategy `failedChange` might
internal/lsp/transport.go+16−16
@@ -6,10 +6,10 @@ import (
66 "encoding/json"
77 "fmt"
88 "io"
9+ "log/slog"
910 "strings"
1011
1112 "github.com/charmbracelet/crush/internal/config"
12− "github.com/charmbracelet/crush/internal/logging"
1313)
1414
1515// Write writes an LSP message to the given writer
@@ -21,7 +21,7 @@ func WriteMessage(w io.Writer, msg *Message) error {
2121 cfg := config.Get()
2222
2323 if cfg.Options.DebugLSP {
24− logging.Debug("Sending message to server", "method", msg.Method, "id", msg.ID)
24+ slog.Debug("Sending message to server", "method", msg.Method, "id", msg.ID)
2525 }
2626
2727 _, err = fmt.Fprintf(w, "Content-Length: %d\r\n\r\n", len(data))
@@ -50,7 +50,7 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
5050 line = strings.TrimSpace(line)
5151
5252 if cfg.Options.DebugLSP {
53− logging.Debug("Received header", "line", line)
53+ slog.Debug("Received header", "line", line)
5454 }
5555
5656 if line == "" {
@@ -66,7 +66,7 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
6666 }
6767
6868 if cfg.Options.DebugLSP {
69− logging.Debug("Content-Length", "length", contentLength)
69+ slog.Debug("Content-Length", "length", contentLength)
7070 }
7171
7272 // Read content
@@ -77,7 +77,7 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
7777 }
7878
7979 if cfg.Options.DebugLSP {
80− logging.Debug("Received content", "content", string(content))
80+ slog.Debug("Received content", "content", string(content))
8181 }
8282
8383 // Parse message
@@ -96,7 +96,7 @@ func (c *Client) handleMessages() {
9696 msg, err := ReadMessage(c.stdout)
9797 if err != nil {
9898 if cfg.Options.DebugLSP {
99− logging.Error("Error reading message", "error", err)
99+ slog.Error("Error reading message", "error", err)
100100 }
101101 return
102102 }
@@ -104,7 +104,7 @@ func (c *Client) handleMessages() {
104104 // Handle server->client request (has both Method and ID)
105105 if msg.Method != "" && msg.ID != 0 {
106106 if cfg.Options.DebugLSP {
107− logging.Debug("Received request from server", "method", msg.Method, "id", msg.ID)
107+ slog.Debug("Received request from server", "method", msg.Method, "id", msg.ID)
108108 }
109109
110110 response := &Message{
@@ -144,7 +144,7 @@ func (c *Client) handleMessages() {
144144
145145 // Send response back to server
146146 if err := WriteMessage(c.stdin, response); err != nil {
147− logging.Error("Error sending response to server", "error", err)
147+ slog.Error("Error sending response to server", "error", err)
148148 }
149149
150150 continue
@@ -158,11 +158,11 @@ func (c *Client) handleMessages() {
158158
159159 if ok {
160160 if cfg.Options.DebugLSP {
161− logging.Debug("Handling notification", "method", msg.Method)
161+ slog.Debug("Handling notification", "method", msg.Method)
162162 }
163163 go handler(msg.Params)
164164 } else if cfg.Options.DebugLSP {
165− logging.Debug("No handler for notification", "method", msg.Method)
165+ slog.Debug("No handler for notification", "method", msg.Method)
166166 }
167167 continue
168168 }
@@ -175,12 +175,12 @@ func (c *Client) handleMessages() {
175175
176176 if ok {
177177 if cfg.Options.DebugLSP {
178− logging.Debug("Received response for request", "id", msg.ID)
178+ slog.Debug("Received response for request", "id", msg.ID)
179179 }
180180 ch <- msg
181181 close(ch)
182182 } else if cfg.Options.DebugLSP {
183− logging.Debug("No handler for response", "id", msg.ID)
183+ slog.Debug("No handler for response", "id", msg.ID)
184184 }
185185 }
186186 }
@@ -192,7 +192,7 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
192192 id := c.nextID.Add(1)
193193
194194 if cfg.Options.DebugLSP {
195− logging.Debug("Making call", "method", method, "id", id)
195+ slog.Debug("Making call", "method", method, "id", id)
196196 }
197197
198198 msg, err := NewRequest(id, method, params)
@@ -218,14 +218,14 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
218218 }
219219
220220 if cfg.Options.DebugLSP {
221− logging.Debug("Request sent", "method", method, "id", id)
221+ slog.Debug("Request sent", "method", method, "id", id)
222222 }
223223
224224 // Wait for response
225225 resp := <-ch
226226
227227 if cfg.Options.DebugLSP {
228− logging.Debug("Received response", "id", id)
228+ slog.Debug("Received response", "id", id)
229229 }
230230
231231 if resp.Error != nil {
@@ -251,7 +251,7 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
251251func (c *Client) Notify(ctx context.Context, method string, params any) error {
252252 cfg := config.Get()
253253 if cfg.Options.DebugLSP {
254− logging.Debug("Sending notification", "method", method)
254+ slog.Debug("Sending notification", "method", method)
255255 }
256256
257257 msg, err := NewNotification(method, params)
internal/lsp/watcher/watcher.go+42−41
@@ -3,6 +3,7 @@ package watcher
33import (
44 "context"
55 "fmt"
6+ "log/slog"
67 "os"
78 "path/filepath"
89 "strings"
@@ -11,7 +12,7 @@ import (
1112
1213 "github.com/bmatcuk/doublestar/v4"
1314 "github.com/charmbracelet/crush/internal/config"
14− "github.com/charmbracelet/crush/internal/logging"
15+
1516 "github.com/charmbracelet/crush/internal/lsp"
1617 "github.com/charmbracelet/crush/internal/lsp/protocol"
1718 "github.com/fsnotify/fsnotify"
@@ -45,7 +46,7 @@ func NewWorkspaceWatcher(client *lsp.Client) *WorkspaceWatcher {
4546func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watchers []protocol.FileSystemWatcher) {
4647 cfg := config.Get()
4748
48− logging.Debug("Adding file watcher registrations")
49+ slog.Debug("Adding file watcher registrations")
4950 w.registrationMu.Lock()
5051 defer w.registrationMu.Unlock()
5152
@@ -54,33 +55,33 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
5455
5556 // Print detailed registration information for debugging
5657 if cfg.Options.DebugLSP {
57− logging.Debug("Adding file watcher registrations",
58+ slog.Debug("Adding file watcher registrations",
5859 "id", id,
5960 "watchers", len(watchers),
6061 "total", len(w.registrations),
6162 )
6263
6364 for i, watcher := range watchers {
64− logging.Debug("Registration", "index", i+1)
65+ slog.Debug("Registration", "index", i+1)
6566
6667 // Log the GlobPattern
6768 switch v := watcher.GlobPattern.Value.(type) {
6869 case string:
69− logging.Debug("GlobPattern", "pattern", v)
70+ slog.Debug("GlobPattern", "pattern", v)
7071 case protocol.RelativePattern:
71− logging.Debug("GlobPattern", "pattern", v.Pattern)
72+ slog.Debug("GlobPattern", "pattern", v.Pattern)
7273
7374 // Log BaseURI details
7475 switch u := v.BaseURI.Value.(type) {
7576 case string:
76− logging.Debug("BaseURI", "baseURI", u)
77+ slog.Debug("BaseURI", "baseURI", u)
7778 case protocol.DocumentUri:
78− logging.Debug("BaseURI", "baseURI", u)
79+ slog.Debug("BaseURI", "baseURI", u)
7980 default:
80− logging.Debug("BaseURI", "baseURI", u)
81+ slog.Debug("BaseURI", "baseURI", u)
8182 }
8283 default:
83− logging.Debug("GlobPattern", "unknown type", fmt.Sprintf("%T", v))
84+ slog.Debug("GlobPattern", "unknown type", fmt.Sprintf("%T", v))
8485 }
8586
8687 // Log WatchKind
@@ -89,13 +90,13 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
8990 watchKind = *watcher.Kind
9091 }
9192
92− logging.Debug("WatchKind", "kind", watchKind)
93+ slog.Debug("WatchKind", "kind", watchKind)
9394 }
9495 }
9596
9697 // Determine server type for specialized handling
9798 serverName := getServerNameFromContext(ctx)
98− logging.Debug("Server type detected", "serverName", serverName)
99+ slog.Debug("Server type detected", "serverName", serverName)
99100
100101 // Check if this server has sent file watchers
101102 hasFileWatchers := len(watchers) > 0
@@ -123,7 +124,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
123124 filesOpened += highPriorityFilesOpened
124125
125126 if cfg.Options.DebugLSP {
126− logging.Debug("Opened high-priority files",
127+ slog.Debug("Opened high-priority files",
127128 "count", highPriorityFilesOpened,
128129 "serverName", serverName)
129130 }
@@ -131,7 +132,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
131132 // If we've already opened enough high-priority files, we might not need more
132133 if filesOpened >= maxFilesToOpen {
133134 if cfg.Options.DebugLSP {
134− logging.Debug("Reached file limit with high-priority files",
135+ slog.Debug("Reached file limit with high-priority files",
135136 "filesOpened", filesOpened,
136137 "maxFiles", maxFilesToOpen)
137138 }
@@ -149,7 +150,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
149150 if d.IsDir() {
150151 if path != w.workspacePath && shouldExcludeDir(path) {
151152 if cfg.Options.DebugLSP {
152− logging.Debug("Skipping excluded directory", "path", path)
153+ slog.Debug("Skipping excluded directory", "path", path)
153154 }
154155 return filepath.SkipDir
155156 }
@@ -177,7 +178,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
177178
178179 elapsedTime := time.Since(startTime)
179180 if cfg.Options.DebugLSP {
180− logging.Debug("Limited workspace scan complete",
181+ slog.Debug("Limited workspace scan complete",
181182 "filesOpened", filesOpened,
182183 "maxFiles", maxFilesToOpen,
183184 "elapsedTime", elapsedTime.Seconds(),
@@ -186,11 +187,11 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
186187 }
187188
188189 if err != nil && cfg.Options.DebugLSP {
189− logging.Debug("Error scanning workspace for files to open", "error", err)
190+ slog.Debug("Error scanning workspace for files to open", "error", err)
190191 }
191192 }()
192193 } else if cfg.Options.DebugLSP {
193− logging.Debug("Using on-demand file loading for server", "server", serverName)
194+ slog.Debug("Using on-demand file loading for server", "server", serverName)
194195 }
195196}
196197
@@ -266,7 +267,7 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName
266267 matches, err := doublestar.Glob(os.DirFS(w.workspacePath), pattern)
267268 if err != nil {
268269 if cfg.Options.DebugLSP {
269− logging.Debug("Error finding high-priority files", "pattern", pattern, "error", err)
270+ slog.Debug("Error finding high-priority files", "pattern", pattern, "error", err)
270271 }
271272 continue
272273 }
@@ -300,12 +301,12 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName
300301 fullPath := filesToOpen[j]
301302 if err := w.client.OpenFile(ctx, fullPath); err != nil {
302303 if cfg.Options.DebugLSP {
303− logging.Debug("Error opening high-priority file", "path", fullPath, "error", err)
304+ slog.Debug("Error opening high-priority file", "path", fullPath, "error", err)
304305 }
305306 } else {
306307 filesOpened++
307308 if cfg.Options.DebugLSP {
308− logging.Debug("Opened high-priority file", "path", fullPath)
309+ slog.Debug("Opened high-priority file", "path", fullPath)
309310 }
310311 }
311312 }
@@ -334,7 +335,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
334335 }
335336
336337 serverName := getServerNameFromContext(ctx)
337− logging.Debug("Starting workspace watcher", "workspacePath", workspacePath, "serverName", serverName)
338+ slog.Debug("Starting workspace watcher", "workspacePath", workspacePath, "serverName", serverName)
338339
339340 // Register handler for file watcher registrations from the server
340341 lsp.RegisterFileWatchHandler(func(id string, watchers []protocol.FileSystemWatcher) {
@@ -343,7 +344,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
343344
344345 watcher, err := fsnotify.NewWatcher()
345346 if err != nil {
346− logging.Error("Error creating watcher", "error", err)
347+ slog.Error("Error creating watcher", "error", err)
347348 }
348349 defer watcher.Close()
349350
@@ -357,7 +358,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
357358 if d.IsDir() && path != workspacePath {
358359 if shouldExcludeDir(path) {
359360 if cfg.Options.DebugLSP {
360− logging.Debug("Skipping excluded directory", "path", path)
361+ slog.Debug("Skipping excluded directory", "path", path)
361362 }
362363 return filepath.SkipDir
363364 }
@@ -367,14 +368,14 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
367368 if d.IsDir() {
368369 err = watcher.Add(path)
369370 if err != nil {
370− logging.Error("Error watching path", "path", path, "error", err)
371+ slog.Error("Error watching path", "path", path, "error", err)
371372 }
372373 }
373374
374375 return nil
375376 })
376377 if err != nil {
377− logging.Error("Error walking workspace", "error", err)
378+ slog.Error("Error walking workspace", "error", err)
378379 }
379380
380381 // Event loop
@@ -396,7 +397,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
396397 // Skip excluded directories
397398 if !shouldExcludeDir(event.Name) {
398399 if err := watcher.Add(event.Name); err != nil {
399− logging.Error("Error adding directory to watcher", "path", event.Name, "error", err)
400+ slog.Error("Error adding directory to watcher", "path", event.Name, "error", err)
400401 }
401402 }
402403 } else {
@@ -411,7 +412,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
411412 // Debug logging
412413 if cfg.Options.DebugLSP {
413414 matched, kind := w.isPathWatched(event.Name)
414− logging.Debug("File event",
415+ slog.Debug("File event",
415416 "path", event.Name,
416417 "operation", event.Op.String(),
417418 "watched", matched,
@@ -431,7 +432,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
431432 // Just send the notification if needed
432433 info, err := os.Stat(event.Name)
433434 if err != nil {
434− logging.Error("Error getting file info", "path", event.Name, "error", err)
435+ slog.Error("Error getting file info", "path", event.Name, "error", err)
435436 return
436437 }
437438 if !info.IsDir() && watchKind&protocol.WatchCreate != 0 {
@@ -459,7 +460,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
459460 if !ok {
460461 return
461462 }
462− logging.Error("Error watching file", "error", err)
463+ slog.Error("Error watching file", "error", err)
463464 }
464465 }
465466}
@@ -584,7 +585,7 @@ func matchesSimpleGlob(pattern, path string) bool {
584585 // Fall back to simple matching for simpler patterns
585586 matched, err := filepath.Match(pattern, path)
586587 if err != nil {
587− logging.Error("Error matching pattern", "pattern", pattern, "path", path, "error", err)
588+ slog.Error("Error matching pattern", "pattern", pattern, "path", path, "error", err)
588589 return false
589590 }
590591
@@ -595,7 +596,7 @@ func matchesSimpleGlob(pattern, path string) bool {
595596func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPattern) bool {
596597 patternInfo, err := pattern.AsPattern()
597598 if err != nil {
598− logging.Error("Error parsing pattern", "pattern", pattern, "error", err)
599+ slog.Error("Error parsing pattern", "pattern", pattern, "error", err)
599600 return false
600601 }
601602
@@ -620,7 +621,7 @@ func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPatt
620621 // Make path relative to basePath for matching
621622 relPath, err := filepath.Rel(basePath, path)
622623 if err != nil {
623− logging.Error("Error getting relative path", "path", path, "basePath", basePath, "error", err)
624+ slog.Error("Error getting relative path", "path", path, "basePath", basePath, "error", err)
624625 return false
625626 }
626627 relPath = filepath.ToSlash(relPath)
@@ -663,14 +664,14 @@ func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, chan
663664 } else if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {
664665 err := w.client.NotifyChange(ctx, filePath)
665666 if err != nil {
666− logging.Error("Error notifying change", "error", err)
667+ slog.Error("Error notifying change", "error", err)
667668 }
668669 return
669670 }
670671
671672 // Notify LSP server about the file event using didChangeWatchedFiles
672673 if err := w.notifyFileEvent(ctx, uri, changeType); err != nil {
673− logging.Error("Error notifying LSP server about file event", "error", err)
674+ slog.Error("Error notifying LSP server about file event", "error", err)
674675 }
675676}
676677
@@ -678,7 +679,7 @@ func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, chan
678679func (w *WorkspaceWatcher) notifyFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) error {
679680 cfg := config.Get()
680681 if cfg.Options.DebugLSP {
681− logging.Debug("Notifying file event",
682+ slog.Debug("Notifying file event",
682683 "uri", uri,
683684 "changeType", changeType,
684685 )
@@ -853,7 +854,7 @@ func shouldExcludeFile(filePath string) bool {
853854 // Skip large files
854855 if info.Size() > maxFileSize {
855856 if cfg.Options.DebugLSP {
856− logging.Debug("Skipping large file",
857+ slog.Debug("Skipping large file",
857858 "path", filePath,
858859 "size", info.Size(),
859860 "maxSize", maxFileSize,
@@ -891,10 +892,10 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
891892 // This helps with project initialization for certain language servers
892893 if isHighPriorityFile(path, serverName) {
893894 if cfg.Options.DebugLSP {
894− logging.Debug("Opening high-priority file", "path", path, "serverName", serverName)
895+ slog.Debug("Opening high-priority file", "path", path, "serverName", serverName)
895896 }
896897 if err := w.client.OpenFile(ctx, path); err != nil && cfg.Options.DebugLSP {
897− logging.Error("Error opening high-priority file", "path", path, "error", err)
898+ slog.Error("Error opening high-priority file", "path", path, "error", err)
898899 }
899900 return
900901 }
@@ -906,7 +907,7 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
906907 // Check file size - for preloading we're more conservative
907908 if info.Size() > (1 * 1024 * 1024) { // 1MB limit for preloaded files
908909 if cfg.Options.DebugLSP {
909− logging.Debug("Skipping large file for preloading", "path", path, "size", info.Size())
910+ slog.Debug("Skipping large file for preloading", "path", path, "size", info.Size())
910911 }
911912 return
912913 }
@@ -938,7 +939,7 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
938939 if shouldOpen {
939940 // Don't need to check if it's already open - the client.OpenFile handles that
940941 if err := w.client.OpenFile(ctx, path); err != nil && cfg.Options.DebugLSP {
941− logging.Error("Error opening file", "path", path, "error", err)
942+ slog.Error("Error opening file", "path", path, "error", err)
942943 }
943944 }
944945 }
internal/permission/permission.go+4−3
@@ -6,7 +6,6 @@ import (
66 "slices"
77 "sync"
88
9− "github.com/charmbracelet/crush/internal/config"
109 "github.com/charmbracelet/crush/internal/pubsub"
1110 "github.com/google/uuid"
1211)
@@ -44,6 +43,7 @@ type Service interface {
4443type permissionService struct {
4544 *pubsub.Broker[PermissionRequest]
4645
46+ workingDir string
4747 sessionPermissions []PermissionRequest
4848 sessionPermissionsMu sync.RWMutex
4949 pendingRequests sync.Map
@@ -87,7 +87,7 @@ func (s *permissionService) Request(opts CreatePermissionRequest) bool {
8787
8888 dir := filepath.Dir(opts.Path)
8989 if dir == "." {
90− dir = config.WorkingDirectory()
90+ dir = s.workingDir
9191 }
9292 permission := PermissionRequest{
9393 ID: uuid.New().String(),
@@ -125,9 +125,10 @@ func (s *permissionService) AutoApproveSession(sessionID string) {
125125 s.autoApproveSessionsMu.Unlock()
126126}
127127
128−func NewPermissionService() Service {
128+func NewPermissionService(workingDir string) Service {
129129 return &permissionService{
130130 Broker: pubsub.NewBroker[PermissionRequest](),
131+ workingDir: workingDir,
131132 sessionPermissions: make([]PermissionRequest, 0),
132133 }
133134}
internal/shell/persistent.go+3−4
@@ -1,9 +1,8 @@
11package shell
22
33import (
4+ "log/slog"
45 "sync"
5−
6− "github.com/charmbracelet/crush/internal/logging"
76)
87
98// PersistentShell is a singleton shell instance that maintains state across the application
@@ -30,9 +29,9 @@ func GetPersistentShell(cwd string) *PersistentShell {
3029 return shellInstance
3130}
3231
33−// loggingAdapter adapts the internal logging package to the Logger interface
32+// slog.dapter adapts the internal slog.package to the Logger interface
3433type loggingAdapter struct{}
3534
3635func (l *loggingAdapter) InfoPersist(msg string, keysAndValues ...interface{}) {
37− logging.InfoPersist(msg, keysAndValues...)
36+ slog.Info(msg, keysAndValues...)
3837}
internal/tui/components/anim/anim.go+3−2
@@ -261,7 +261,7 @@ func (a Anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
261261}
262262
263263// View renders the current state of the animation.
264−func (a Anim) View() tea.View {
264+func (a Anim) View() string {
265265 var b strings.Builder
266266 for i := range a.width {
267267 switch {
@@ -284,7 +284,8 @@ func (a Anim) View() tea.View {
284284 if a.initialized && a.labelWidth > 0 {
285285 b.WriteString(a.ellipsisFrames[a.ellipsisStep/ellipsisAnimSpeed])
286286 }
287− return tea.NewView(b.String())
287+
288+ return b.String()
288289}
289290
290291// Step is a command that triggers the next step in the animation.
internal/tui/components/anim/example/main.go+3−3
@@ -50,20 +50,20 @@ func (m model) View() tea.View {
5050 }
5151
5252 v := tea.NewView("")
53− v.SetBackgroundColor(m.bgColor)
53+ v.BackgroundColor = m.bgColor
5454
5555 if m.quitting {
5656 return v
5757 }
5858
5959 if a, ok := m.anim.(anim.Anim); ok {
60− l := lipgloss.NewLayer(a.View().String()).
60+ l := lipgloss.NewLayer(a.View()).
6161 Width(a.Width()).
6262 X(m.w/2 - a.Width()/2).
6363 Y(m.h / 2)
6464
6565 v = tea.NewView(lipgloss.NewCanvas(l))
66− v.SetBackgroundColor(m.bgColor)
66+ v.BackgroundColor = m.bgColor
6767 return v
6868 }
6969 return v
internal/tui/components/chat/chat.go+4−6
@@ -101,12 +101,10 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
101101}
102102
103103// View renders the message list or an initial screen if empty.
104−func (m *messageListCmp) View() tea.View {
105− return tea.NewView(
106− lipgloss.JoinVertical(
107− lipgloss.Left,
108− m.listCmp.View().String(),
109− ),
104+func (m *messageListCmp) View() string {
105+ return lipgloss.JoinVertical(
106+ lipgloss.Left,
107+ m.listCmp.View(),
110108 )
111109}
112110
internal/tui/components/chat/editor/editor.go+9−11
@@ -14,7 +14,6 @@ import (
1414 tea "github.com/charmbracelet/bubbletea/v2"
1515 "github.com/charmbracelet/crush/internal/app"
1616 "github.com/charmbracelet/crush/internal/fsext"
17− "github.com/charmbracelet/crush/internal/logging"
1817 "github.com/charmbracelet/crush/internal/message"
1918 "github.com/charmbracelet/crush/internal/session"
2019 "github.com/charmbracelet/crush/internal/tui/components/chat"
@@ -153,8 +152,7 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
153152 return m, nil
154153 case filepicker.FilePickedMsg:
155154 if len(m.attachments) >= maxAttachments {
156− logging.ErrorPersist(fmt.Sprintf("cannot add more than %d images", maxAttachments))
157− return m, cmd
155+ return m, util.ReportError(fmt.Errorf("cannot add more than %d images", maxAttachments))
158156 }
159157 m.attachments = append(m.attachments, msg.Attachment)
160158 return m, nil
@@ -265,20 +263,22 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
265263 return m, tea.Batch(cmds...)
266264}
267265
268−func (m *editorCmp) View() tea.View {
269− t := styles.CurrentTheme()
266+func (m *editorCmp) Cursor() *tea.Cursor {
270267 cursor := m.textarea.Cursor()
271268 if cursor != nil {
272269 cursor.X = cursor.X + m.x + 1
273270 cursor.Y = cursor.Y + m.y + 1 // adjust for padding
274271 }
272+ return cursor
273+}
274+
275+func (m *editorCmp) View() string {
276+ t := styles.CurrentTheme()
275277 if len(m.attachments) == 0 {
276278 content := t.S().Base.Padding(1).Render(
277279 m.textarea.View(),
278280 )
279− view := tea.NewView(content)
280− view.SetCursor(cursor)
281− return view
281+ return content
282282 }
283283 content := t.S().Base.Padding(0, 1, 1, 1).Render(
284284 lipgloss.JoinVertical(lipgloss.Top,
@@ -286,9 +286,7 @@ func (m *editorCmp) View() tea.View {
286286 m.textarea.View(),
287287 ),
288288 )
289− view := tea.NewView(content)
290− view.SetCursor(cursor)
291− return view
289+ return content
292290}
293291
294292func (m *editorCmp) SetSize(width, height int) tea.Cmd {
internal/tui/components/chat/header/header.go+6−5
@@ -57,9 +57,9 @@ func (p *header) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5757 return p, nil
5858}
5959
60−func (p *header) View() tea.View {
60+func (p *header) View() string {
6161 if p.session.ID == "" {
62− return tea.NewView("")
62+ return ""
6363 }
6464
6565 t := styles.CurrentTheme()
@@ -86,12 +86,12 @@ func (p *header) View() tea.View {
8686 parts...,
8787 ),
8888 )
89− return tea.NewView(content)
89+ return content
9090}
9191
9292func (h *header) details() string {
9393 t := styles.CurrentTheme()
94− cwd := fsext.DirTrim(fsext.PrettyPath(config.WorkingDirectory()), 4)
94+ cwd := fsext.DirTrim(fsext.PrettyPath(config.Get().WorkingDir()), 4)
9595 parts := []string{
9696 t.S().Muted.Render(cwd),
9797 }
@@ -111,7 +111,8 @@ func (h *header) details() string {
111111 parts = append(parts, t.S().Error.Render(fmt.Sprintf("%s%d", styles.ErrorIcon, errorCount)))
112112 }
113113
114− model := config.GetAgentModel(config.AgentCoder)
114+ agentCfg := config.Get().Agents["coder"]
115+ model := config.Get().GetModelByType(agentCfg.Model)
115116 percentage := (float64(h.session.CompletionTokens+h.session.PromptTokens) / float64(model.ContextWindow)) * 100
116117 formattedPercentage := t.S().Muted.Render(fmt.Sprintf("%d%%", int(percentage)))
117118 parts = append(parts, formattedPercentage)
internal/tui/components/chat/messages/messages.go+10−13
@@ -10,7 +10,6 @@ import (
1010 "github.com/charmbracelet/lipgloss/v2"
1111
1212 "github.com/charmbracelet/crush/internal/config"
13− "github.com/charmbracelet/crush/internal/fur/provider"
1413 "github.com/charmbracelet/crush/internal/message"
1514 "github.com/charmbracelet/crush/internal/tui/components/anim"
1615 "github.com/charmbracelet/crush/internal/tui/components/core"
@@ -89,20 +88,20 @@ func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8988
9089// View renders the message component based on its current state.
9190// Returns different views for spinning, user, and assistant messages.
92−func (m *messageCmp) View() tea.View {
91+func (m *messageCmp) View() string {
9392 if m.spinning {
94− return tea.NewView(m.style().PaddingLeft(1).Render(m.anim.View().String()))
93+ return m.style().PaddingLeft(1).Render(m.anim.View())
9594 }
9695 if m.message.ID != "" {
9796 // this is a user or assistant message
9897 switch m.message.Role {
9998 case message.User:
100− return tea.NewView(m.renderUserMessage())
99+ return m.renderUserMessage()
101100 default:
102− return tea.NewView(m.renderAssistantMessage())
101+ return m.renderAssistantMessage()
103102 }
104103 }
105− return tea.NewView(m.style().Render("No message content"))
104+ return m.style().Render("No message content")
106105}
107106
108107// GetMessage returns the underlying message data
@@ -289,20 +288,18 @@ func (m *assistantSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) {
289288 return m, nil
290289}
291290
292−func (m *assistantSectionModel) View() tea.View {
291+func (m *assistantSectionModel) View() string {
293292 t := styles.CurrentTheme()
294293 finishData := m.message.FinishPart()
295294 finishTime := time.Unix(finishData.Time, 0)
296295 duration := finishTime.Sub(m.lastUserMessageTime)
297296 infoMsg := t.S().Subtle.Render(duration.String())
298297 icon := t.S().Subtle.Render(styles.ModelIcon)
299− model := config.GetProviderModel(provider.InferenceProvider(m.message.Provider), m.message.Model)
300− modelFormatted := t.S().Muted.Render(model.Name)
298+ model := config.Get().GetModel(m.message.Provider, m.message.Model)
299+ modelFormatted := t.S().Muted.Render(model.Model)
301300 assistant := fmt.Sprintf("%s %s %s", icon, modelFormatted, infoMsg)
302− return tea.NewView(
303− t.S().Base.PaddingLeft(2).Render(
304− core.Section(assistant, m.width-2),
305− ),
301+ return t.S().Base.PaddingLeft(2).Render(
302+ core.Section(assistant, m.width-2),
306303 )
307304}
308305
internal/tui/components/chat/messages/renderer.go+2−2
@@ -7,10 +7,10 @@ import (
77 "time"
88
99 "github.com/charmbracelet/crush/internal/fsext"
10− "github.com/charmbracelet/crush/internal/highlight"
1110 "github.com/charmbracelet/crush/internal/llm/agent"
1211 "github.com/charmbracelet/crush/internal/llm/tools"
1312 "github.com/charmbracelet/crush/internal/tui/components/core"
13+ "github.com/charmbracelet/crush/internal/tui/highlight"
1414 "github.com/charmbracelet/crush/internal/tui/styles"
1515 "github.com/charmbracelet/lipgloss/v2"
1616 "github.com/charmbracelet/lipgloss/v2/tree"
@@ -542,7 +542,7 @@ func (tr agentRenderer) Render(v *toolCallCmp) string {
542542
543543 if v.result.ToolCallID == "" {
544544 v.spinning = true
545− parts = append(parts, v.anim.View().String())
545+ parts = append(parts, v.anim.View())
546546 } else {
547547 v.spinning = false
548548 }
internal/tui/components/chat/messages/tool.go+4−4
@@ -145,19 +145,19 @@ func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
145145
146146// View renders the tool call component based on its current state.
147147// Shows either a pending animation or the tool-specific rendered result.
148−func (m *toolCallCmp) View() tea.View {
148+func (m *toolCallCmp) View() string {
149149 box := m.style()
150150
151151 if !m.call.Finished && !m.cancelled {
152− return tea.NewView(box.Render(m.renderPending()))
152+ return box.Render(m.renderPending())
153153 }
154154
155155 r := registry.lookup(m.call.Name)
156156
157157 if m.isNested {
158− return tea.NewView(box.Render(r.Render(m)))
158+ return box.Render(r.Render(m))
159159 }
160− return tea.NewView(box.Render(r.Render(m)))
160+ return box.Render(r.Render(m))
161161}
162162
163163// State management methods
internal/tui/components/chat/sidebar/sidebar.go+13−11
@@ -13,7 +13,7 @@ import (
1313 "github.com/charmbracelet/crush/internal/diff"
1414 "github.com/charmbracelet/crush/internal/fsext"
1515 "github.com/charmbracelet/crush/internal/history"
16− "github.com/charmbracelet/crush/internal/logging"
16+
1717 "github.com/charmbracelet/crush/internal/lsp"
1818 "github.com/charmbracelet/crush/internal/lsp/protocol"
1919 "github.com/charmbracelet/crush/internal/pubsub"
@@ -94,7 +94,6 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
9494 case chat.SessionClearedMsg:
9595 m.session = session.Session{}
9696 case pubsub.Event[history.File]:
97− logging.Info("sidebar", "Received file history event", "file", msg.Payload.Path, "session", msg.Payload.SessionID)
9897 return m, m.handleFileHistoryEvent(msg)
9998 case pubsub.Event[session.Session]:
10099 if msg.Type == pubsub.UpdatedEvent {
@@ -106,7 +105,7 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
106105 return m, nil
107106}
108107
109−func (m *sidebarCmp) View() tea.View {
108+func (m *sidebarCmp) View() string {
110109 t := styles.CurrentTheme()
111110 parts := []string{}
112111 if !m.compactMode {
@@ -138,9 +137,7 @@ func (m *sidebarCmp) View() tea.View {
138137 m.mcpBlock(),
139138 )
140139
141− return tea.NewView(
142− lipgloss.JoinVertical(lipgloss.Left, parts...),
143− )
140+ return lipgloss.JoinVertical(lipgloss.Left, parts...)
144141}
145142
146143func (m *sidebarCmp) handleFileHistoryEvent(event pubsub.Event[history.File]) tea.Cmd {
@@ -161,6 +158,8 @@ func (m *sidebarCmp) handleFileHistoryEvent(event pubsub.Event[history.File]) te
161158 before := existing.History.initialVersion.Content
162159 after := existing.History.latestVersion.Content
163160 path := existing.History.initialVersion.Path
161+ cwd := config.Get().WorkingDir()
162+ path = strings.TrimPrefix(path, cwd)
164163 _, additions, deletions := diff.GenerateDiff(before, after, path)
165164 existing.Additions = additions
166165 existing.Deletions = deletions
@@ -214,7 +213,9 @@ func (m *sidebarCmp) loadSessionFiles() tea.Msg {
214213
215214 sessionFiles := make([]SessionFile, 0, len(fileMap))
216215 for path, fh := range fileMap {
217− _, additions, deletions := diff.GenerateDiff(fh.initialVersion.Content, fh.latestVersion.Content, fh.initialVersion.Path)
216+ cwd := config.Get().WorkingDir()
217+ path = strings.TrimPrefix(path, cwd)
218+ _, additions, deletions := diff.GenerateDiff(fh.initialVersion.Content, fh.latestVersion.Content, path)
218219 sessionFiles = append(sessionFiles, SessionFile{
219220 History: fh,
220221 FilePath: path,
@@ -297,7 +298,7 @@ func (m *sidebarCmp) filesBlock() string {
297298 }
298299
299300 extraContent := strings.Join(statusParts, " ")
300− cwd := config.WorkingDirectory() + string(os.PathSeparator)
301+ cwd := config.Get().WorkingDir() + string(os.PathSeparator)
301302 filePath := file.FilePath
302303 filePath = strings.TrimPrefix(filePath, cwd)
303304 filePath = fsext.DirTrim(fsext.PrettyPath(filePath), 2)
@@ -474,12 +475,13 @@ func formatTokensAndCost(tokens, contextWindow int64, cost float64) string {
474475}
475476
476477func (s *sidebarCmp) currentModelBlock() string {
477− model := config.GetAgentModel(config.AgentCoder)
478+ agentCfg := config.Get().Agents["coder"]
479+ model := config.Get().GetModelByType(agentCfg.Model)
478480
479481 t := styles.CurrentTheme()
480482
481483 modelIcon := t.S().Base.Foreground(t.FgSubtle).Render(styles.ModelIcon)
482− modelName := t.S().Text.Render(model.Name)
484+ modelName := t.S().Text.Render(model.Model)
483485 modelInfo := fmt.Sprintf("%s %s", modelIcon, modelName)
484486 parts := []string{
485487 modelInfo,
@@ -507,7 +509,7 @@ func (m *sidebarCmp) SetSession(session session.Session) tea.Cmd {
507509}
508510
509511func cwd() string {
510− cwd := config.WorkingDirectory()
512+ cwd := config.Get().WorkingDir()
511513 t := styles.CurrentTheme()
512514 // Replace home directory with ~, unless we're at the top level of the
513515 // home directory).
internal/tui/components/chat/splash/splash.go+2−2
@@ -60,9 +60,9 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
6060}
6161
6262// View implements SplashPage.
63−func (s *splashCmp) View() tea.View {
63+func (s *splashCmp) View() string {
6464 content := lipgloss.JoinVertical(lipgloss.Left, s.logoRendered)
65− return tea.NewView(content)
65+ return content
6666}
6767
6868func (s *splashCmp) logoBlock() string {
internal/tui/components/completions/completions.go+3−6
@@ -157,15 +157,12 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
157157}
158158
159159// View implements Completions.
160−func (c *completionsCmp) View() tea.View {
160+func (c *completionsCmp) View() string {
161161 if len(c.list.Items()) == 0 {
162− return tea.NewView(c.style().Render("No completions found"))
162+ return c.style().Render("No completions found")
163163 }
164164
165− view := tea.NewView(
166− c.style().Render(c.list.View().String()),
167− )
168− return view
165+ return c.style().Render(c.list.View())
169166}
170167
171168func (c *completionsCmp) style() lipgloss.Style {
internal/tui/components/completions/item.go+2−2
@@ -75,7 +75,7 @@ func (c *completionItemCmp) Update(tea.Msg) (tea.Model, tea.Cmd) {
7575}
7676
7777// View implements CommandItem.
78−func (c *completionItemCmp) View() tea.View {
78+func (c *completionItemCmp) View() string {
7979 t := styles.CurrentTheme()
8080
8181 itemStyle := t.S().Base.Padding(0, 1).Width(c.width)
@@ -135,7 +135,7 @@ func (c *completionItemCmp) View() tea.View {
135135 parts...,
136136 ),
137137 )
138− return tea.NewView(item)
138+ return item
139139}
140140
141141// Blur implements CommandItem.
internal/tui/components/core/helpers.go+1−1
@@ -5,7 +5,7 @@ import (
55 "strings"
66
77 "github.com/alecthomas/chroma/v2"
8− "github.com/charmbracelet/crush/internal/exp/diffview"
8+ "github.com/charmbracelet/crush/internal/tui/exp/diffview"
99 "github.com/charmbracelet/crush/internal/tui/styles"
1010 "github.com/charmbracelet/lipgloss/v2"
1111 "github.com/charmbracelet/x/ansi"
internal/tui/components/core/layout/container.go+9−5
@@ -72,7 +72,14 @@ func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7272 }
7373}
7474
75−func (c *container) View() tea.View {
75+func (c *container) Cursor() *tea.Cursor {
76+ if cursor, ok := c.content.(util.Cursor); ok {
77+ return cursor.Cursor()
78+ }
79+ return nil
80+}
81+
82+func (c *container) View() string {
7683 t := styles.CurrentTheme()
7784 width := c.width
7885 height := c.height
@@ -106,10 +113,7 @@ func (c *container) View() tea.View {
106113 PaddingLeft(c.paddingLeft)
107114
108115 contentView := c.content.View()
109− view := tea.NewView(style.Render(contentView.String()))
110− cursor := contentView.Cursor()
111− view.SetCursor(cursor)
112− return view
116+ return style.Render(contentView)
113117}
114118
115119func (c *container) SetSize(width, height int) tea.Cmd {
internal/tui/components/core/layout/split.go+28−21
@@ -1,9 +1,11 @@
11package layout
22
33import (
4+ "log/slog"
5+
46 "github.com/charmbracelet/bubbles/v2/key"
57 tea "github.com/charmbracelet/bubbletea/v2"
6− "github.com/charmbracelet/crush/internal/logging"
8+
79 "github.com/charmbracelet/crush/internal/tui/styles"
810 "github.com/charmbracelet/crush/internal/tui/util"
911 "github.com/charmbracelet/lipgloss/v2"
@@ -104,17 +106,34 @@ func (s *splitPaneLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
104106 return s, tea.Batch(cmds...)
105107}
106108
107−func (s *splitPaneLayout) View() tea.View {
109+func (s *splitPaneLayout) Cursor() *tea.Cursor {
110+ if s.bottomPanel != nil {
111+ if c, ok := s.bottomPanel.(util.Cursor); ok {
112+ return c.Cursor()
113+ }
114+ } else if s.rightPanel != nil {
115+ if c, ok := s.rightPanel.(util.Cursor); ok {
116+ return c.Cursor()
117+ }
118+ } else if s.leftPanel != nil {
119+ if c, ok := s.leftPanel.(util.Cursor); ok {
120+ return c.Cursor()
121+ }
122+ }
123+ return nil
124+}
125+
126+func (s *splitPaneLayout) View() string {
108127 var topSection string
109128
110129 if s.leftPanel != nil && s.rightPanel != nil {
111130 leftView := s.leftPanel.View()
112131 rightView := s.rightPanel.View()
113− topSection = lipgloss.JoinHorizontal(lipgloss.Top, leftView.String(), rightView.String())
132+ topSection = lipgloss.JoinHorizontal(lipgloss.Top, leftView, rightView)
114133 } else if s.leftPanel != nil {
115− topSection = s.leftPanel.View().String()
134+ topSection = s.leftPanel.View()
116135 } else if s.rightPanel != nil {
117− topSection = s.rightPanel.View().String()
136+ topSection = s.rightPanel.View()
118137 } else {
119138 topSection = ""
120139 }
@@ -123,38 +142,26 @@ func (s *splitPaneLayout) View() tea.View {
123142
124143 if s.bottomPanel != nil && topSection != "" {
125144 bottomView := s.bottomPanel.View()
126− finalView = lipgloss.JoinVertical(lipgloss.Left, topSection, bottomView.String())
145+ finalView = lipgloss.JoinVertical(lipgloss.Left, topSection, bottomView)
127146 } else if s.bottomPanel != nil {
128− finalView = s.bottomPanel.View().String()
147+ finalView = s.bottomPanel.View()
129148 } else {
130149 finalView = topSection
131150 }
132151
133− // TODO: think of a better way to handle multiple cursors
134− var cursor *tea.Cursor
135− if s.bottomPanel != nil {
136− cursor = s.bottomPanel.View().Cursor()
137− } else if s.rightPanel != nil {
138− cursor = s.rightPanel.View().Cursor()
139− } else if s.leftPanel != nil {
140− cursor = s.leftPanel.View().Cursor()
141− }
142−
143152 t := styles.CurrentTheme()
144153
145154 style := t.S().Base.
146155 Width(s.width).
147156 Height(s.height)
148157
149− view := tea.NewView(style.Render(finalView))
150− view.SetCursor(cursor)
151− return view
158+ return style.Render(finalView)
152159}
153160
154161func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd {
155162 s.width = width
156163 s.height = height
157− logging.Info("Setting split pane size", "width", width, "height", height)
164+ slog.Info("Setting split pane size", "width", width, "height", height)
158165
159166 var topHeight, bottomHeight int
160167 var cmds []tea.Cmd
internal/tui/components/core/list/list.go+13−9
@@ -280,12 +280,20 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
280280 return m, nil
281281}
282282
283+// Cursor returns the current cursor position in the input field.
284+func (m *model) Cursor() *tea.Cursor {
285+ if m.filterable && !m.hideFilterInput {
286+ return m.input.Cursor()
287+ }
288+ return nil
289+}
290+
283291// View renders the list to a string for display.
284292// Returns empty string if the list has no dimensions.
285293// Triggers re-rendering if needed before returning content.
286−func (m *model) View() tea.View {
294+func (m *model) View() string {
287295 if m.viewState.height == 0 || m.viewState.width == 0 {
288− return tea.NewView("") // No content to display
296+ return "" // No content to display
289297 }
290298 if m.renderState.needsRerender {
291299 m.renderVisible()
@@ -303,11 +311,7 @@ func (m *model) View() tea.View {
303311 content,
304312 )
305313 }
306− view := tea.NewView(content)
307− if m.filterable && !m.hideFilterInput {
308− view.SetCursor(m.input.Cursor())
309− }
310− return view
314+ return content
311315}
312316
313317// handleKeyPress processes keyboard input for list navigation.
@@ -833,7 +837,7 @@ func (m *model) rerenderItem(inx int) {
833837func (m *model) getItemLines(item util.Model) []string {
834838 var itemLines []string
835839
836− itemLines = strings.Split(item.View().String(), "\n")
840+ itemLines = strings.Split(item.View(), "\n")
837841
838842 if m.gapSize > 0 {
839843 gap := make([]string, m.gapSize)
@@ -1261,7 +1265,7 @@ func (m *model) filterSection(sect section, search string) *section {
12611265
12621266 // Check if section header itself matches
12631267 if sect.header != nil {
1264− headerText := strings.ToLower(sect.header.View().String())
1268+ headerText := strings.ToLower(sect.header.View())
12651269 if strings.Contains(headerText, search) {
12661270 hasHeaderMatch = true
12671271 // If header matches, include all items in the section
internal/tui/components/core/status/status.go+2−36
@@ -6,8 +6,6 @@ import (
66
77 "github.com/charmbracelet/bubbles/v2/help"
88 tea "github.com/charmbracelet/bubbletea/v2"
9− "github.com/charmbracelet/crush/internal/logging"
10− "github.com/charmbracelet/crush/internal/pubsub"
119 "github.com/charmbracelet/crush/internal/session"
1210 "github.com/charmbracelet/crush/internal/tui/styles"
1311 "github.com/charmbracelet/crush/internal/tui/util"
@@ -58,49 +56,17 @@ func (m *statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5856 return m, m.clearMessageCmd(ttl)
5957 case util.ClearStatusMsg:
6058 m.info = util.InfoMsg{}
61−
62− // Handle persistent logs
63− case pubsub.Event[logging.LogMessage]:
64− if msg.Payload.Persist {
65− switch msg.Payload.Level {
66− case "error":
67− m.info = util.InfoMsg{
68− Type: util.InfoTypeError,
69− Msg: msg.Payload.Message,
70− TTL: msg.Payload.PersistTime,
71− }
72− case "info":
73− m.info = util.InfoMsg{
74− Type: util.InfoTypeInfo,
75− Msg: msg.Payload.Message,
76− TTL: msg.Payload.PersistTime,
77− }
78− case "warn":
79− m.info = util.InfoMsg{
80− Type: util.InfoTypeWarn,
81− Msg: msg.Payload.Message,
82− TTL: msg.Payload.PersistTime,
83− }
84− default:
85− m.info = util.InfoMsg{
86− Type: util.InfoTypeInfo,
87− Msg: msg.Payload.Message,
88− TTL: msg.Payload.PersistTime,
89− }
90− }
91− return m, m.clearMessageCmd(m.info.TTL)
92− }
9359 }
9460 return m, nil
9561}
9662
97−func (m *statusCmp) View() tea.View {
63+func (m *statusCmp) View() string {
9864 t := styles.CurrentTheme()
9965 status := t.S().Base.Padding(0, 1, 1, 1).Render(m.help.View(m.keyMap))
10066 if m.info.Msg != "" {
10167 status = m.infoMsg()
10268 }
103− return tea.NewView(status)
69+ return status
10470}
10571
10672func (m *statusCmp) infoMsg() string {
internal/tui/components/dialogs/commands/arguments.go+10−10
@@ -139,7 +139,7 @@ func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
139139}
140140
141141// View implements CommandArgumentsDialog.
142−func (c *commandArgumentsDialogCmp) View() tea.View {
142+func (c *commandArgumentsDialogCmp) View() string {
143143 t := styles.CurrentTheme()
144144 baseStyle := t.S().Base
145145
@@ -188,19 +188,19 @@ func (c *commandArgumentsDialogCmp) View() tea.View {
188188 elements...,
189189 )
190190
191− view := tea.NewView(
192− baseStyle.Padding(1, 1, 0, 1).
193− Border(lipgloss.RoundedBorder()).
194− BorderForeground(t.BorderFocus).
195− Width(c.width).
196− Render(content),
197− )
191+ return baseStyle.Padding(1, 1, 0, 1).
192+ Border(lipgloss.RoundedBorder()).
193+ BorderForeground(t.BorderFocus).
194+ Width(c.width).
195+ Render(content)
196+}
197+
198+func (c *commandArgumentsDialogCmp) Cursor() *tea.Cursor {
198199 cursor := c.inputs[c.focusIndex].Cursor()
199200 if cursor != nil {
200201 cursor = c.moveCursor(cursor)
201202 }
202− view.SetCursor(cursor)
203− return view
203+ return cursor
204204}
205205
206206func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
internal/tui/components/dialogs/commands/commands.go+14−8
@@ -143,23 +143,29 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
143143 return c, nil
144144}
145145
146−func (c *commandDialogCmp) View() tea.View {
146+func (c *commandDialogCmp) View() string {
147147 t := styles.CurrentTheme()
148− listView := c.commandList.View()
148+ listView := c.commandList
149149 radio := c.commandTypeRadio()
150150 content := lipgloss.JoinVertical(
151151 lipgloss.Left,
152152 t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Commands", c.width-lipgloss.Width(radio)-5)+" "+radio),
153− listView.String(),
153+ listView.View(),
154154 "",
155155 t.S().Base.Width(c.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(c.help.View(c.keyMap)),
156156 )
157− v := tea.NewView(c.style().Render(content))
158− if listView.Cursor() != nil {
159− c := c.moveCursor(listView.Cursor())
160− v.SetCursor(c)
157+ return c.style().Render(content)
158+}
159+
160+func (c *commandDialogCmp) Cursor() *tea.Cursor {
161+ if cursor, ok := c.commandList.(util.Cursor); ok {
162+ cursor := cursor.Cursor()
163+ if cursor != nil {
164+ cursor = c.moveCursor(cursor)
165+ }
166+ return cursor
161167 }
162− return v
168+ return nil
163169}
164170
165171func (c *commandDialogCmp) commandTypeRadio() string {
internal/tui/components/dialogs/commands/item.go+2−2
@@ -35,12 +35,12 @@ func (m *itemSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) {
3535 return m, nil
3636}
3737
38−func (m *itemSectionModel) View() tea.View {
38+func (m *itemSectionModel) View() string {
3939 t := styles.CurrentTheme()
4040 title := ansi.Truncate(m.title, m.width-2, "…")
4141 style := t.S().Base.Padding(1, 1, 0, 1)
4242 title = t.S().Muted.Render(title)
43− return tea.NewView(style.Render(core.Section(title, m.width-2)))
43+ return style.Render(core.Section(title, m.width-2))
4444}
4545
4646func (m *itemSectionModel) GetSize() (int, int) {
internal/tui/components/dialogs/compact/compact.go+2−2
@@ -242,8 +242,8 @@ func (c *compactDialogCmp) render() string {
242242 Render(dialogContent)
243243}
244244
245−func (c *compactDialogCmp) View() tea.View {
246− return tea.NewView(c.render())
245+func (c *compactDialogCmp) View() string {
246+ return c.render()
247247}
248248
249249// SetSize sets the size of the component.
internal/tui/components/dialogs/dialogs.go+4−5
@@ -37,7 +37,7 @@ type DialogCmp interface {
3737 Dialogs() []DialogModel
3838 HasDialogs() bool
3939 GetLayers() []*lipgloss.Layer
40− ActiveView() *tea.View
40+ ActiveModel() util.Model
4141 ActiveDialogID() DialogID
4242}
4343
@@ -132,12 +132,11 @@ func (d dialogCmp) Dialogs() []DialogModel {
132132 return d.dialogs
133133}
134134
135−func (d dialogCmp) ActiveView() *tea.View {
135+func (d dialogCmp) ActiveModel() util.Model {
136136 if len(d.dialogs) == 0 {
137137 return nil
138138 }
139− view := d.dialogs[len(d.dialogs)-1].View()
140− return &view
139+ return d.dialogs[len(d.dialogs)-1]
141140}
142141
143142func (d dialogCmp) ActiveDialogID() DialogID {
@@ -150,7 +149,7 @@ func (d dialogCmp) ActiveDialogID() DialogID {
150149func (d dialogCmp) GetLayers() []*lipgloss.Layer {
151150 layers := []*lipgloss.Layer{}
152151 for _, dialog := range d.Dialogs() {
153− dialogView := dialog.View().String()
152+ dialogView := dialog.View()
154153 row, col := dialog.Position()
155154 layers = append(layers, lipgloss.NewLayer(dialogView).X(col).Y(row))
156155 }
internal/tui/components/dialogs/filepicker/filepicker.go+5−9
@@ -11,7 +11,6 @@ import (
1111 "github.com/charmbracelet/bubbles/v2/help"
1212 "github.com/charmbracelet/bubbles/v2/key"
1313 tea "github.com/charmbracelet/bubbletea/v2"
14− "github.com/charmbracelet/crush/internal/logging"
1514 "github.com/charmbracelet/crush/internal/message"
1615 "github.com/charmbracelet/crush/internal/tui/components/core"
1716 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
@@ -119,18 +118,15 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
119118 func() tea.Msg {
120119 isFileLarge, err := ValidateFileSize(path, maxAttachmentSize)
121120 if err != nil {
122− logging.ErrorPersist("unable to read the image")
123− return nil
121+ return util.ReportError(fmt.Errorf("unable to read the image: %w", err))
124122 }
125123 if isFileLarge {
126− logging.ErrorPersist("file too large, max 5MB")
127− return nil
124+ return util.ReportError(fmt.Errorf("file too large, max 5MB"))
128125 }
129126
130127 content, err := os.ReadFile(path)
131128 if err != nil {
132− logging.ErrorPersist("Unable read selected file")
133− return nil
129+ return util.ReportError(fmt.Errorf("unable to read the image: %w", err))
134130 }
135131
136132 mimeBufferSize := min(512, len(content))
@@ -148,7 +144,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
148144 return m, tea.Batch(cmds...)
149145}
150146
151−func (m *model) View() tea.View {
147+func (m *model) View() string {
152148 t := styles.CurrentTheme()
153149
154150 content := lipgloss.JoinVertical(
@@ -158,7 +154,7 @@ func (m *model) View() tea.View {
158154 m.filePicker.View(),
159155 t.S().Base.Width(m.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(m.help.View(m.keyMap)),
160156 )
161− return tea.NewView(m.style().Render(content))
157+ return m.style().Render(content)
162158}
163159
164160func (m *model) currentImage() string {
internal/tui/components/dialogs/init/init.go+2−2
@@ -147,8 +147,8 @@ func (m *initDialogCmp) render() string {
147147}
148148
149149// View implements tea.Model.
150−func (m *initDialogCmp) View() tea.View {
151− return tea.NewView(m.render())
150+func (m *initDialogCmp) View() string {
151+ return m.render()
152152}
153153
154154// SetSize sets the size of the component.
internal/tui/components/dialogs/models/models.go+41−31
@@ -31,8 +31,8 @@ const (
3131
3232// ModelSelectedMsg is sent when a model is selected
3333type ModelSelectedMsg struct {
34− Model config.PreferredModel
35− ModelType config.ModelType
34+ Model config.SelectedModel
35+ ModelType config.SelectedModelType
3636}
3737
3838// CloseModelDialogMsg is sent when a model is selected
@@ -115,19 +115,19 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
115115 items := m.modelList.Items()
116116 selectedItem := items[selectedItemInx].(completions.CompletionItem).Value().(ModelOption)
117117
118− var modelType config.ModelType
118+ var modelType config.SelectedModelType
119119 if m.modelType == LargeModelType {
120− modelType = config.LargeModel
120+ modelType = config.SelectedModelTypeLarge
121121 } else {
122− modelType = config.SmallModel
122+ modelType = config.SelectedModelTypeSmall
123123 }
124124
125125 return m, tea.Sequence(
126126 util.CmdHandler(dialogs.CloseDialogMsg{}),
127127 util.CmdHandler(ModelSelectedMsg{
128− Model: config.PreferredModel{
129− ModelID: selectedItem.Model.ID,
130− Provider: selectedItem.Provider.ID,
128+ Model: config.SelectedModel{
129+ Model: selectedItem.Model.ID,
130+ Provider: string(selectedItem.Provider.ID),
131131 },
132132 ModelType: modelType,
133133 }),
@@ -149,23 +149,29 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
149149 return m, nil
150150}
151151
152−func (m *modelDialogCmp) View() tea.View {
152+func (m *modelDialogCmp) View() string {
153153 t := styles.CurrentTheme()
154154 listView := m.modelList.View()
155155 radio := m.modelTypeRadio()
156156 content := lipgloss.JoinVertical(
157157 lipgloss.Left,
158158 t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Switch Model", m.width-lipgloss.Width(radio)-5)+" "+radio),
159− listView.String(),
159+ listView,
160160 "",
161161 t.S().Base.Width(m.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(m.help.View(m.keyMap)),
162162 )
163− v := tea.NewView(m.style().Render(content))
164− if listView.Cursor() != nil {
165− c := m.moveCursor(listView.Cursor())
166− v.SetCursor(c)
163+ return m.style().Render(content)
164+}
165+
166+func (m *modelDialogCmp) Cursor() *tea.Cursor {
167+ if cursor, ok := m.modelList.(util.Cursor); ok {
168+ cursor := cursor.Cursor()
169+ if cursor != nil {
170+ cursor = m.moveCursor(cursor)
171+ return cursor
172+ }
167173 }
168− return v
174+ return nil
169175}
170176
171177func (m *modelDialogCmp) style() lipgloss.Style {
@@ -218,35 +224,39 @@ func (m *modelDialogCmp) modelTypeRadio() string {
218224func (m *modelDialogCmp) SetModelType(modelType int) tea.Cmd {
219225 m.modelType = modelType
220226
221− providers := config.Providers()
227+ providers, err := config.Providers()
228+ if err != nil {
229+ return util.ReportError(err)
230+ }
231+
222232 modelItems := []util.Model{}
223233 selectIndex := 0
224234
225235 cfg := config.Get()
226− var currentModel config.PreferredModel
236+ var currentModel config.SelectedModel
227237 if m.modelType == LargeModelType {
228− currentModel = cfg.Models.Large
238+ currentModel = cfg.Models[config.SelectedModelTypeLarge]
229239 } else {
230− currentModel = cfg.Models.Small
240+ currentModel = cfg.Models[config.SelectedModelTypeSmall]
231241 }
232242
233243 // Create a map to track which providers we've already added
234− addedProviders := make(map[provider.InferenceProvider]bool)
244+ addedProviders := make(map[string]bool)
235245
236246 // First, add any configured providers that are not in the known providers list
237247 // These should appear at the top of the list
238248 knownProviders := provider.KnownProviders()
239249 for providerID, providerConfig := range cfg.Providers {
240− if providerConfig.Disabled {
250+ if providerConfig.Disable {
241251 continue
242252 }
243253
244254 // Check if this provider is not in the known providers list
245− if !slices.Contains(knownProviders, providerID) {
255+ if !slices.Contains(knownProviders, provider.InferenceProvider(providerID)) {
246256 // Convert config provider to provider.Provider format
247257 configProvider := provider.Provider{
248258 Name: string(providerID), // Use provider ID as name for unknown providers
249− ID: providerID,
259+ ID: provider.InferenceProvider(providerID),
250260 Models: make([]provider.Model, len(providerConfig.Models)),
251261 }
252262
@@ -254,7 +264,7 @@ func (m *modelDialogCmp) SetModelType(modelType int) tea.Cmd {
254264 for i, model := range providerConfig.Models {
255265 configProvider.Models[i] = provider.Model{
256266 ID: model.ID,
257− Name: model.Name,
267+ Model: model.Model,
258268 CostPer1MIn: model.CostPer1MIn,
259269 CostPer1MOut: model.CostPer1MOut,
260270 CostPer1MInCached: model.CostPer1MInCached,
@@ -263,7 +273,7 @@ func (m *modelDialogCmp) SetModelType(modelType int) tea.Cmd {
263273 DefaultMaxTokens: model.DefaultMaxTokens,
264274 CanReason: model.CanReason,
265275 HasReasoningEffort: model.HasReasoningEffort,
266− DefaultReasoningEffort: model.ReasoningEffort,
276+ DefaultReasoningEffort: model.DefaultReasoningEffort,
267277 SupportsImages: model.SupportsImages,
268278 }
269279 }
@@ -275,11 +285,11 @@ func (m *modelDialogCmp) SetModelType(modelType int) tea.Cmd {
275285 }
276286 modelItems = append(modelItems, commands.NewItemSection(name))
277287 for _, model := range configProvider.Models {
278− modelItems = append(modelItems, completions.NewCompletionItem(model.Name, ModelOption{
288+ modelItems = append(modelItems, completions.NewCompletionItem(model.Model, ModelOption{
279289 Provider: configProvider,
280290 Model: model,
281291 }))
282− if model.ID == currentModel.ModelID && configProvider.ID == currentModel.Provider {
292+ if model.ID == currentModel.Model && string(configProvider.ID) == currentModel.Provider {
283293 selectIndex = len(modelItems) - 1 // Set the selected index to the current model
284294 }
285295 }
@@ -290,12 +300,12 @@ func (m *modelDialogCmp) SetModelType(modelType int) tea.Cmd {
290300 // Then add the known providers from the predefined list
291301 for _, provider := range providers {
292302 // Skip if we already added this provider as an unknown provider
293− if addedProviders[provider.ID] {
303+ if addedProviders[string(provider.ID)] {
294304 continue
295305 }
296306
297307 // Check if this provider is configured and not disabled
298− if providerConfig, exists := cfg.Providers[provider.ID]; exists && providerConfig.Disabled {
308+ if providerConfig, exists := cfg.Providers[string(provider.ID)]; exists && providerConfig.Disable {
299309 continue
300310 }
301311
@@ -305,11 +315,11 @@ func (m *modelDialogCmp) SetModelType(modelType int) tea.Cmd {
305315 }
306316 modelItems = append(modelItems, commands.NewItemSection(name))
307317 for _, model := range provider.Models {
308− modelItems = append(modelItems, completions.NewCompletionItem(model.Name, ModelOption{
318+ modelItems = append(modelItems, completions.NewCompletionItem(model.Model, ModelOption{
309319 Provider: provider,
310320 Model: model,
311321 }))
312− if model.ID == currentModel.ModelID && provider.ID == currentModel.Provider {
322+ if model.ID == currentModel.Model && string(provider.ID) == currentModel.Provider {
313323 selectIndex = len(modelItems) - 1 // Set the selected index to the current model
314324 }
315325 }
internal/tui/components/dialogs/permissions/permissions.go+2−2
@@ -478,8 +478,8 @@ func (p *permissionDialogCmp) render() string {
478478 )
479479}
480480
481−func (p *permissionDialogCmp) View() tea.View {
482− return tea.NewView(p.render())
481+func (p *permissionDialogCmp) View() string {
482+ return p.render()
483483}
484484
485485func (p *permissionDialogCmp) SetSize() tea.Cmd {
internal/tui/components/dialogs/quit/quit.go+2−4
@@ -65,7 +65,7 @@ func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
6565}
6666
6767// View renders the quit dialog with Yes/No buttons.
68−func (q *quitDialogCmp) View() tea.View {
68+func (q *quitDialogCmp) View() string {
6969 t := styles.CurrentTheme()
7070 baseStyle := t.S().Base
7171 yesStyle := t.S().Text
@@ -100,9 +100,7 @@ func (q *quitDialogCmp) View() tea.View {
100100 Border(lipgloss.RoundedBorder()).
101101 BorderForeground(t.BorderFocus)
102102
103− return tea.NewView(
104− quitDialogStyle.Render(content),
105− )
103+ return quitDialogStyle.Render(content)
106104}
107105
108106func (q *quitDialogCmp) Position() (int, int) {
internal/tui/components/dialogs/sessions/sessions.go+13−7
@@ -122,23 +122,29 @@ func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
122122 return s, nil
123123}
124124
125−func (s *sessionDialogCmp) View() tea.View {
125+func (s *sessionDialogCmp) View() string {
126126 t := styles.CurrentTheme()
127127 listView := s.sessionsList.View()
128128 content := lipgloss.JoinVertical(
129129 lipgloss.Left,
130130 t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Switch Session", s.width-4)),
131− listView.String(),
131+ listView,
132132 "",
133133 t.S().Base.Width(s.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(s.help.View(s.keyMap)),
134134 )
135135
136− v := tea.NewView(s.style().Render(content))
137− if listView.Cursor() != nil {
138− c := s.moveCursor(listView.Cursor())
139− v.SetCursor(c)
136+ return s.style().Render(content)
137+}
138+
139+func (s *sessionDialogCmp) Cursor() *tea.Cursor {
140+ if cursor, ok := s.sessionsList.(util.Cursor); ok {
141+ cursor := cursor.Cursor()
142+ if cursor != nil {
143+ cursor = s.moveCursor(cursor)
144+ }
145+ return cursor
140146 }
141− return v
147+ return nil
142148}
143149
144150func (s *sessionDialogCmp) style() lipgloss.Style {
internal/tui/components/logs/details.godeleted−176
@@ -1,176 +0,0 @@
1−package logs
2−
3−import (
4− "fmt"
5− "strings"
6− "time"
7−
8− "github.com/charmbracelet/bubbles/v2/viewport"
9− tea "github.com/charmbracelet/bubbletea/v2"
10− "github.com/charmbracelet/crush/internal/logging"
11− "github.com/charmbracelet/crush/internal/tui/components/core/layout"
12− "github.com/charmbracelet/crush/internal/tui/styles"
13− "github.com/charmbracelet/crush/internal/tui/util"
14− "github.com/charmbracelet/lipgloss/v2"
15−)
16−
17−type DetailComponent interface {
18− util.Model
19− layout.Sizeable
20−}
21−
22−type detailCmp struct {
23− width, height int
24− currentLog logging.LogMessage
25− viewport viewport.Model
26−}
27−
28−func (i *detailCmp) Init() tea.Cmd {
29− messages := logging.List()
30− if len(messages) == 0 {
31− return nil
32− }
33− i.currentLog = messages[0]
34− return nil
35−}
36−
37−func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
38− switch msg := msg.(type) {
39− case selectedLogMsg:
40− if msg.ID != i.currentLog.ID {
41− i.currentLog = logging.LogMessage(msg)
42− i.updateContent()
43− }
44− }
45−
46− return i, nil
47−}
48−
49−func (i *detailCmp) updateContent() {
50− var content strings.Builder
51− t := styles.CurrentTheme()
52−
53− if i.currentLog.ID == "" {
54− content.WriteString(t.S().Muted.Render("No log selected"))
55− i.viewport.SetContent(content.String())
56− return
57− }
58−
59− // Level badge with background color
60− levelStyle := getLevelStyle(i.currentLog.Level)
61− levelBadge := levelStyle.Padding(0, 1).Render(strings.ToUpper(i.currentLog.Level))
62−
63− // Timestamp with relative time
64− timeStr := i.currentLog.Time.Format("2006-01-05 15:04:05 UTC")
65− relativeTime := getRelativeTime(i.currentLog.Time)
66− timeStyle := t.S().Muted
67−
68− // Header line
69− header := lipgloss.JoinHorizontal(
70− lipgloss.Left,
71− timeStr,
72− " ",
73− timeStyle.Render(relativeTime),
74− )
75−
76− content.WriteString(levelBadge)
77− content.WriteString("\n\n")
78− content.WriteString(header)
79− content.WriteString("\n\n")
80−
81− // Message section
82− messageHeaderStyle := t.S().Base.Foreground(t.Blue).Bold(true)
83− content.WriteString(messageHeaderStyle.Render("Message"))
84− content.WriteString("\n")
85− content.WriteString(i.currentLog.Message)
86− content.WriteString("\n\n")
87−
88− // Attributes section
89− if len(i.currentLog.Attributes) > 0 {
90− attrHeaderStyle := t.S().Base.Foreground(t.Blue).Bold(true)
91− content.WriteString(attrHeaderStyle.Render("Attributes"))
92− content.WriteString("\n")
93−
94− for _, attr := range i.currentLog.Attributes {
95− keyStyle := t.S().Base.Foreground(t.Accent)
96− valueStyle := t.S().Text
97− attrLine := fmt.Sprintf("%s: %s",
98− keyStyle.Render(attr.Key),
99− valueStyle.Render(attr.Value),
100− )
101− content.WriteString(attrLine)
102− content.WriteString("\n")
103− }
104− }
105−
106− i.viewport.SetContent(content.String())
107−}
108−
109−func getLevelStyle(level string) lipgloss.Style {
110− t := styles.CurrentTheme()
111− style := t.S().Base.Bold(true)
112−
113− switch strings.ToLower(level) {
114− case "info":
115− return style.Foreground(t.White).Background(t.Info)
116− case "warn", "warning":
117− return style.Foreground(t.White).Background(t.Warning)
118− case "error", "err":
119− return style.Foreground(t.White).Background(t.Error)
120− case "debug":
121− return style.Foreground(t.White).Background(t.Success)
122− case "fatal":
123− return style.Foreground(t.White).Background(t.Error)
124− default:
125− return style.Foreground(t.FgBase)
126− }
127−}
128−
129−func getRelativeTime(logTime time.Time) string {
130− now := time.Now()
131− diff := now.Sub(logTime)
132−
133− if diff < time.Minute {
134− return fmt.Sprintf("%ds ago", int(diff.Seconds()))
135− } else if diff < time.Hour {
136− return fmt.Sprintf("%dm ago", int(diff.Minutes()))
137− } else if diff < 24*time.Hour {
138− return fmt.Sprintf("%dh ago", int(diff.Hours()))
139− } else if diff < 30*24*time.Hour {
140− return fmt.Sprintf("%dd ago", int(diff.Hours()/24))
141− } else if diff < 365*24*time.Hour {
142− return fmt.Sprintf("%dmo ago", int(diff.Hours()/(24*30)))
143− } else {
144− return fmt.Sprintf("%dy ago", int(diff.Hours()/(24*365)))
145− }
146−}
147−
148−func (i *detailCmp) View() tea.View {
149− t := styles.CurrentTheme()
150− style := t.S().Base.
151− BorderStyle(lipgloss.RoundedBorder()).
152− BorderForeground(t.BorderFocus).
153− Width(i.width - 2). // Adjust width for border
154− Height(i.height - 2). // Adjust height for border
155− Padding(1)
156− return tea.NewView(style.Render(i.viewport.View()))
157−}
158−
159−func (i *detailCmp) GetSize() (int, int) {
160− return i.width, i.height
161−}
162−
163−func (i *detailCmp) SetSize(width int, height int) tea.Cmd {
164− i.width = width
165− i.height = height
166− i.viewport.SetWidth(i.width - 4)
167− i.viewport.SetHeight(i.height - 4)
168− i.updateContent()
169− return nil
170−}
171−
172−func NewLogsDetails() DetailComponent {
173− return &detailCmp{
174− viewport: viewport.New(),
175− }
176−}
internal/tui/components/logs/table.godeleted−197
@@ -1,197 +0,0 @@
1−package logs
2−
3−import (
4− "fmt"
5− "slices"
6− "strings"
7−
8− "github.com/charmbracelet/bubbles/v2/table"
9− tea "github.com/charmbracelet/bubbletea/v2"
10− "github.com/charmbracelet/crush/internal/logging"
11− "github.com/charmbracelet/crush/internal/pubsub"
12− "github.com/charmbracelet/crush/internal/tui/components/core/layout"
13− "github.com/charmbracelet/crush/internal/tui/styles"
14− "github.com/charmbracelet/crush/internal/tui/util"
15− "github.com/charmbracelet/lipgloss/v2"
16−)
17−
18−type TableComponent interface {
19− util.Model
20− layout.Sizeable
21−}
22−
23−type tableCmp struct {
24− table table.Model
25− logs []logging.LogMessage
26−}
27−
28−type selectedLogMsg logging.LogMessage
29−
30−func (i *tableCmp) Init() tea.Cmd {
31− i.logs = logging.List()
32− i.setRows()
33− return nil
34−}
35−
36−func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
37− var cmds []tea.Cmd
38− switch msg := msg.(type) {
39− case pubsub.Event[logging.LogMessage]:
40− return i, func() tea.Msg {
41− if msg.Type == pubsub.CreatedEvent {
42− rows := i.table.Rows()
43− for _, row := range rows {
44− if row[1] == msg.Payload.ID {
45− return nil // If the log already exists, do not add it again
46− }
47− }
48− i.logs = append(i.logs, msg.Payload)
49− i.table.SetRows(
50− append(
51− []table.Row{
52− logToRow(msg.Payload),
53− },
54− i.table.Rows()...,
55− ),
56− )
57− }
58− return selectedLogMsg(msg.Payload)
59− }
60− }
61− t, cmd := i.table.Update(msg)
62− cmds = append(cmds, cmd)
63− i.table = t
64−
65− cmds = append(cmds, func() tea.Msg {
66− for _, log := range logging.List() {
67− if log.ID == i.table.SelectedRow()[1] {
68− // If the selected row matches the log ID, return the selected log message
69− return selectedLogMsg(log)
70− }
71− }
72− return nil
73− })
74− return i, tea.Batch(cmds...)
75−}
76−
77−func (i *tableCmp) View() tea.View {
78− t := styles.CurrentTheme()
79− defaultStyles := table.DefaultStyles()
80−
81− // Header styling
82− defaultStyles.Header = defaultStyles.Header.
83− Foreground(t.Primary).
84− Bold(true).
85− BorderStyle(lipgloss.NormalBorder()).
86− BorderBottom(true).
87− BorderForeground(t.Border)
88−
89− // Selected row styling
90− defaultStyles.Selected = defaultStyles.Selected.
91− Foreground(t.FgSelected).
92− Background(t.Primary).
93− Bold(false)
94−
95− // Cell styling
96− defaultStyles.Cell = defaultStyles.Cell.
97− Foreground(t.FgBase)
98−
99− i.table.SetStyles(defaultStyles)
100− return tea.NewView(i.table.View())
101−}
102−
103−func (i *tableCmp) GetSize() (int, int) {
104− return i.table.Width(), i.table.Height()
105−}
106−
107−func (i *tableCmp) SetSize(width int, height int) tea.Cmd {
108− i.table.SetWidth(width)
109− i.table.SetHeight(height)
110−
111− columnWidth := (width - 10) / 4
112− i.table.SetColumns([]table.Column{
113− {
114− Title: "Level",
115− Width: 10,
116− },
117− {
118− Title: "ID",
119− Width: columnWidth,
120− },
121− {
122− Title: "Time",
123− Width: columnWidth,
124− },
125− {
126− Title: "Message",
127− Width: columnWidth,
128− },
129− {
130− Title: "Attributes",
131− Width: columnWidth,
132− },
133− })
134− return nil
135−}
136−
137−func (i *tableCmp) setRows() {
138− rows := []table.Row{}
139−
140− slices.SortFunc(i.logs, func(a, b logging.LogMessage) int {
141− if a.Time.Before(b.Time) {
142− return -1
143− }
144− if a.Time.After(b.Time) {
145− return 1
146− }
147− return 0
148− })
149−
150− for _, log := range i.logs {
151− rows = append(rows, logToRow(log))
152− }
153− i.table.SetRows(rows)
154−}
155−
156−func logToRow(log logging.LogMessage) table.Row {
157− // Format attributes as JSON string
158− var attrStr string
159− if len(log.Attributes) > 0 {
160− var parts []string
161− for _, attr := range log.Attributes {
162− parts = append(parts, fmt.Sprintf(`{"Key":"%s","Value":"%s"}`, attr.Key, attr.Value))
163− }
164− attrStr = "[" + strings.Join(parts, ",") + "]"
165− }
166−
167− // Format time with relative time
168− timeStr := log.Time.Format("2006-01-05 15:04:05 UTC")
169− relativeTime := getRelativeTime(log.Time)
170− fullTimeStr := timeStr + " " + relativeTime
171−
172− return table.Row{
173− strings.ToUpper(log.Level),
174− log.ID,
175− fullTimeStr,
176− log.Message,
177− attrStr,
178− }
179−}
180−
181−func NewLogsTable() TableComponent {
182− columns := []table.Column{
183− {Title: "Level"},
184− {Title: "ID"},
185− {Title: "Time"},
186− {Title: "Message"},
187− {Title: "Attributes"},
188− }
189−
190− tableModel := table.New(
191− table.WithColumns(columns),
192− )
193− tableModel.Focus()
194− return &tableCmp{
195− table: tableModel,
196− }
197−}
internal/exp/diffview/Taskfile.yaml → internal/tui/exp/diffview/Taskfile.yamlrenamed
The content is the same.
internal/exp/diffview/chroma.go → internal/tui/exp/diffview/chroma.gorenamed
The content is the same.
internal/exp/diffview/diffview.go → internal/tui/exp/diffview/diffview.gorenamed
The content is the same.
internal/exp/diffview/diffview_test.go → internal/tui/exp/diffview/diffview_test.gorenamed+1−1
@@ -7,7 +7,7 @@ import (
77 "testing"
88
99 "github.com/alecthomas/chroma/v2/styles"
10− "github.com/charmbracelet/crush/internal/exp/diffview"
10+ "github.com/charmbracelet/crush/internal/tui/exp/diffview"
1111 "github.com/charmbracelet/x/ansi"
1212 "github.com/charmbracelet/x/exp/golden"
1313)
internal/exp/diffview/split.go → internal/tui/exp/diffview/split.gorenamed
The content is the same.
internal/exp/diffview/style.go → internal/tui/exp/diffview/style.gorenamed
The content is the same.
internal/exp/diffview/testdata/TestDefault.after → internal/tui/exp/diffview/testdata/TestDefault.afterrenamed
The content is the same.
internal/exp/diffview/testdata/TestDefault.before → internal/tui/exp/diffview/testdata/TestDefault.beforerenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Split/CustomContextLines/DarkMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Split/CustomContextLines/DarkMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Split/CustomContextLines/LightMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Split/CustomContextLines/LightMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Split/Default/DarkMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Split/Default/DarkMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Split/Default/LightMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Split/Default/LightMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Split/LargeWidth/DarkMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Split/LargeWidth/DarkMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Split/LargeWidth/LightMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Split/LargeWidth/LightMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Split/MultipleHunks/DarkMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Split/MultipleHunks/DarkMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Split/MultipleHunks/LightMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Split/MultipleHunks/LightMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Split/Narrow/DarkMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Split/Narrow/DarkMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Split/Narrow/LightMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Split/Narrow/LightMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Split/NoLineNumbers/DarkMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Split/NoLineNumbers/DarkMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Split/NoLineNumbers/LightMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Split/NoLineNumbers/LightMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Split/NoSyntaxHighlight/DarkMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Split/NoSyntaxHighlight/DarkMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Split/NoSyntaxHighlight/LightMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Split/NoSyntaxHighlight/LightMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Split/SmallWidth/DarkMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Split/SmallWidth/DarkMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Split/SmallWidth/LightMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Split/SmallWidth/LightMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Unified/CustomContextLines/DarkMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Unified/CustomContextLines/DarkMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Unified/CustomContextLines/LightMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Unified/CustomContextLines/LightMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Unified/Default/DarkMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Unified/Default/DarkMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Unified/Default/LightMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Unified/Default/LightMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Unified/LargeWidth/DarkMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Unified/LargeWidth/DarkMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Unified/LargeWidth/LightMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Unified/LargeWidth/LightMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Unified/MultipleHunks/DarkMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Unified/MultipleHunks/DarkMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Unified/MultipleHunks/LightMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Unified/MultipleHunks/LightMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Unified/Narrow/DarkMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Unified/Narrow/DarkMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Unified/Narrow/LightMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Unified/Narrow/LightMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Unified/NoLineNumbers/DarkMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Unified/NoLineNumbers/DarkMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Unified/NoLineNumbers/LightMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Unified/NoLineNumbers/LightMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Unified/NoSyntaxHighlight/DarkMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Unified/NoSyntaxHighlight/DarkMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Unified/NoSyntaxHighlight/LightMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Unified/NoSyntaxHighlight/LightMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Unified/SmallWidth/DarkMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Unified/SmallWidth/DarkMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffView/Unified/SmallWidth/LightMode.golden → internal/tui/exp/diffview/testdata/TestDiffView/Unified/SmallWidth/LightMode.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf001.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf001.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf002.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf002.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf003.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf003.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf004.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf004.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf005.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf005.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf006.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf006.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf007.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf007.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf008.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf008.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf009.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf009.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf010.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf010.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf011.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf011.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf012.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf012.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf013.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf013.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf014.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf014.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf015.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf015.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf016.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf016.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf017.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf017.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf018.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf018.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf019.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf019.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf020.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf020.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf001.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf001.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf002.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf002.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf003.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf003.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf004.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf004.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf005.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf005.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf006.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf006.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf007.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf007.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf008.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf008.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf009.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf009.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf010.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf010.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf011.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf011.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf012.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf012.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf013.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf013.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf014.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf014.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf015.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf015.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf016.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf016.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf017.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf017.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf018.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf018.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf019.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf019.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf020.golden → internal/tui/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf020.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewTabs/Split.golden → internal/tui/exp/diffview/testdata/TestDiffViewTabs/Split.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewTabs/Unified.golden → internal/tui/exp/diffview/testdata/TestDiffViewTabs/Unified.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf001.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf001.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf002.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf002.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf003.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf003.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf004.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf004.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf005.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf005.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf006.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf006.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf007.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf007.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf008.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf008.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf009.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf009.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf010.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf010.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf011.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf011.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf012.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf012.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf013.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf013.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf014.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf014.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf015.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf015.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf016.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf016.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf017.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf017.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf018.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf018.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf019.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf019.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf020.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf020.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf021.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf021.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf022.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf022.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf023.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf023.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf024.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf024.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf025.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf025.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf026.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf026.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf027.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf027.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf028.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf028.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf029.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf029.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf030.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf030.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf031.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf031.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf032.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf032.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf033.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf033.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf034.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf034.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf035.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf035.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf036.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf036.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf037.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf037.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf038.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf038.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf039.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf039.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf040.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf040.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf041.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf041.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf042.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf042.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf043.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf043.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf044.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf044.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf045.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf045.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf046.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf046.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf047.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf047.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf048.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf048.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf049.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf049.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf050.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf050.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf051.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf051.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf052.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf052.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf053.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf053.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf054.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf054.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf055.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf055.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf056.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf056.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf057.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf057.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf058.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf058.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf059.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf059.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf060.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf060.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf061.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf061.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf062.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf062.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf063.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf063.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf064.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf064.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf065.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf065.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf066.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf066.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf067.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf067.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf068.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf068.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf069.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf069.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf070.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf070.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf071.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf071.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf072.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf072.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf073.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf073.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf074.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf074.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf075.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf075.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf076.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf076.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf077.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf077.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf078.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf078.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf079.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf079.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf080.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf080.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf081.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf081.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf082.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf082.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf083.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf083.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf084.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf084.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf085.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf085.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf086.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf086.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf087.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf087.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf088.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf088.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf089.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf089.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf090.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf090.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf091.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf091.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf092.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf092.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf093.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf093.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf094.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf094.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf095.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf095.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf096.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf096.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf097.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf097.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf098.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf098.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf099.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf099.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf100.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf100.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf101.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf101.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf102.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf102.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf103.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf103.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf104.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf104.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf105.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf105.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf106.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf106.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf107.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf107.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf108.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf108.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf109.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf109.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf110.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf110.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf001.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf001.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf002.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf002.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf003.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf003.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf004.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf004.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf005.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf005.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf006.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf006.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf007.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf007.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf008.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf008.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf009.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf009.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf010.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf010.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf011.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf011.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf012.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf012.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf013.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf013.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf014.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf014.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf015.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf015.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf016.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf016.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf017.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf017.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf018.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf018.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf019.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf019.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf020.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf020.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf021.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf021.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf022.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf022.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf023.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf023.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf024.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf024.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf025.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf025.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf026.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf026.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf027.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf027.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf028.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf028.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf029.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf029.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf030.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf030.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf031.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf031.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf032.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf032.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf033.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf033.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf034.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf034.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf035.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf035.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf036.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf036.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf037.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf037.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf038.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf038.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf039.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf039.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf040.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf040.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf041.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf041.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf042.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf042.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf043.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf043.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf044.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf044.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf045.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf045.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf046.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf046.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf047.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf047.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf048.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf048.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf049.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf049.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf050.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf050.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf051.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf051.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf052.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf052.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf053.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf053.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf054.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf054.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf055.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf055.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf056.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf056.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf057.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf057.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf058.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf058.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf059.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf059.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf060.golden → internal/tui/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf060.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf00.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf00.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf01.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf01.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf02.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf02.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf03.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf03.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf04.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf04.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf05.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf05.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf06.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf06.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf07.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf07.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf08.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf08.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf09.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf09.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf10.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf10.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf11.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf11.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf12.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf12.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf13.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf13.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf14.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf14.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf15.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf15.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf16.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf16.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf17.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf17.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf18.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf18.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf19.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf19.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf20.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf20.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf00.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf00.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf01.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf01.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf02.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf02.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf03.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf03.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf04.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf04.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf05.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf05.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf06.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf06.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf07.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf07.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf08.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf08.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf09.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf09.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf10.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf10.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf11.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf11.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf12.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf12.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf13.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf13.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf14.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf14.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf15.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf15.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf16.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf16.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf17.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf17.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf18.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf18.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf19.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf19.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf20.golden → internal/tui/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf20.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf00.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf00.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf01.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf01.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf02.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf02.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf03.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf03.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf04.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf04.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf05.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf05.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf06.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf06.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf07.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf07.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf08.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf08.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf09.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf09.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf10.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf10.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf11.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf11.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf12.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf12.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf13.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf13.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf14.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf14.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf15.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf15.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf16.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf16.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf00.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf00.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf01.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf01.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf02.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf02.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf03.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf03.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf04.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf04.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf05.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf05.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf06.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf06.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf07.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf07.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf08.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf08.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf09.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf09.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf10.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf10.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf11.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf11.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf12.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf12.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf13.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf13.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf14.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf14.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf15.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf15.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf16.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf16.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf00.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf00.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf01.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf01.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf02.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf02.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf03.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf03.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf04.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf04.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf05.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf05.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf06.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf06.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf07.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf07.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf08.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf08.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf09.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf09.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf10.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf10.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf11.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf11.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf12.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf12.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf13.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf13.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf14.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf14.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf15.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf15.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf16.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf16.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf00.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf00.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf01.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf01.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf02.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf02.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf03.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf03.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf04.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf04.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf05.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf05.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf06.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf06.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf07.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf07.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf08.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf08.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf09.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf09.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf10.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf10.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf11.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf11.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf12.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf12.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf13.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf13.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf14.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf14.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf15.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf15.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf16.golden → internal/tui/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf16.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestMultipleHunks.after → internal/tui/exp/diffview/testdata/TestMultipleHunks.afterrenamed
The content is the same.
internal/exp/diffview/testdata/TestMultipleHunks.before → internal/tui/exp/diffview/testdata/TestMultipleHunks.beforerenamed
The content is the same.
internal/exp/diffview/testdata/TestNarrow.after → internal/tui/exp/diffview/testdata/TestNarrow.afterrenamed
The content is the same.
internal/exp/diffview/testdata/TestNarrow.before → internal/tui/exp/diffview/testdata/TestNarrow.beforerenamed
The content is the same.
internal/exp/diffview/testdata/TestTabs.after → internal/tui/exp/diffview/testdata/TestTabs.afterrenamed
The content is the same.
internal/exp/diffview/testdata/TestTabs.before → internal/tui/exp/diffview/testdata/TestTabs.beforerenamed
The content is the same.
internal/exp/diffview/testdata/TestUdiff/ToUnifiedDiff/DefaultContextLines/Content.golden → internal/tui/exp/diffview/testdata/TestUdiff/ToUnifiedDiff/DefaultContextLines/Content.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestUdiff/ToUnifiedDiff/DefaultContextLines/JSON.golden → internal/tui/exp/diffview/testdata/TestUdiff/ToUnifiedDiff/DefaultContextLines/JSON.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestUdiff/ToUnifiedDiff/DefaultContextLinesPlusOne/Content.golden → internal/tui/exp/diffview/testdata/TestUdiff/ToUnifiedDiff/DefaultContextLinesPlusOne/Content.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestUdiff/ToUnifiedDiff/DefaultContextLinesPlusOne/JSON.golden → internal/tui/exp/diffview/testdata/TestUdiff/ToUnifiedDiff/DefaultContextLinesPlusOne/JSON.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestUdiff/ToUnifiedDiff/DefaultContextLinesPlusTwo/Content.golden → internal/tui/exp/diffview/testdata/TestUdiff/ToUnifiedDiff/DefaultContextLinesPlusTwo/Content.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestUdiff/ToUnifiedDiff/DefaultContextLinesPlusTwo/JSON.golden → internal/tui/exp/diffview/testdata/TestUdiff/ToUnifiedDiff/DefaultContextLinesPlusTwo/JSON.goldenrenamed
The content is the same.
internal/exp/diffview/testdata/TestUdiff/Unified.golden → internal/tui/exp/diffview/testdata/TestUdiff/Unified.goldenrenamed
The content is the same.
internal/exp/diffview/udiff_test.go → internal/tui/exp/diffview/udiff_test.gorenamed
The content is the same.
internal/exp/diffview/util.go → internal/tui/exp/diffview/util.gorenamed
The content is the same.
internal/exp/diffview/util_test.go → internal/tui/exp/diffview/util_test.gorenamed
The content is the same.
internal/tui/exp/list/list.gonew file+89
@@ -0,0 +1,89 @@
1+package list
2+
3+import (
4+ tea "github.com/charmbracelet/bubbletea/v2"
5+ "github.com/charmbracelet/crush/internal/tui/components/core/layout"
6+ "github.com/charmbracelet/crush/internal/tui/util"
7+)
8+
9+type Item interface {
10+ util.Model
11+ layout.Sizeable
12+}
13+
14+type List interface {
15+ util.Model
16+}
17+
18+type list struct {
19+ width, height int
20+ gap int
21+
22+ items []Item
23+
24+ renderedView string
25+
26+ // Filter options
27+ filterable bool
28+ filterPlaceholder string
29+}
30+
31+type listOption func(*list)
32+
33+// WithFilterable enables filtering on the list.
34+func WithFilterable(placeholder string) listOption {
35+ return func(l *list) {
36+ l.filterable = true
37+ l.filterPlaceholder = placeholder
38+ }
39+}
40+
41+// WithItems sets the initial items for the list.
42+func WithItems(items ...Item) listOption {
43+ return func(l *list) {
44+ l.items = items
45+ }
46+}
47+
48+// WithSize sets the size of the list.
49+func WithSize(width, height int) listOption {
50+ return func(l *list) {
51+ l.width = width
52+ l.height = height
53+ }
54+}
55+
56+// WithGap sets the gap between items in the list.
57+func WithGap(gap int) listOption {
58+ return func(l *list) {
59+ l.gap = gap
60+ }
61+}
62+
63+func New(opts ...listOption) List {
64+ list := &list{
65+ items: make([]Item, 0),
66+ }
67+ for _, opt := range opts {
68+ opt(list)
69+ }
70+ return list
71+}
72+
73+// Init implements List.
74+func (l *list) Init() tea.Cmd {
75+ if l.height <= 0 || l.width <= 0 {
76+ return nil
77+ }
78+ return nil
79+}
80+
81+// Update implements List.
82+func (l *list) Update(tea.Msg) (tea.Model, tea.Cmd) {
83+ panic("unimplemented")
84+}
85+
86+// View implements List.
87+func (l *list) View() string {
88+ panic("unimplemented")
89+}
internal/highlight/highlight.go → internal/tui/highlight/highlight.gorenamed
The content is the same.
internal/tui/keys.go−6
@@ -5,7 +5,6 @@ import (
55)
66
77type KeyMap struct {
8− Logs key.Binding
98 Quit key.Binding
109 Help key.Binding
1110 Commands key.Binding
@@ -16,10 +15,6 @@ type KeyMap struct {
1615
1716func DefaultKeyMap() KeyMap {
1817 return KeyMap{
19− Logs: key.NewBinding(
20− key.WithKeys("ctrl+l"),
21− key.WithHelp("ctrl+l", "logs"),
22− ),
2318 Quit: key.NewBinding(
2419 key.WithKeys("ctrl+c"),
2520 key.WithHelp("ctrl+c", "quit"),
@@ -47,7 +42,6 @@ func (k KeyMap) FullHelp() [][]key.Binding {
4742 k.Sessions,
4843 k.Quit,
4944 k.Help,
50− k.Logs,
5145 }
5246 slice = k.prependEscAndTab(slice)
5347 slice = append(slice, k.pageBindings...)
internal/tui/page/chat/chat.go+15−9
@@ -170,11 +170,12 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
170170 util.CmdHandler(ChatFocusedMsg{Focused: false}),
171171 )
172172 case key.Matches(msg, p.keyMap.AddAttachment):
173− model := config.GetAgentModel(config.AgentCoder)
173+ agentCfg := config.Get().Agents["coder"]
174+ model := config.Get().GetModelByType(agentCfg.Model)
174175 if model.SupportsImages {
175176 return p, util.CmdHandler(OpenFilePickerMsg{})
176177 } else {
177− return p, util.ReportWarn("File attachments are not supported by the current model: " + model.Name)
178+ return p, util.ReportWarn("File attachments are not supported by the current model: " + model.Model)
178179 }
179180 case key.Matches(msg, p.keyMap.Tab):
180181 if p.session.ID == "" {
@@ -302,7 +303,7 @@ func (p *chatPage) GetSize() (int, int) {
302303 return p.layout.GetSize()
303304}
304305
305−func (p *chatPage) View() tea.View {
306+func (p *chatPage) View() string {
306307 if !p.compactMode || p.session.ID == "" {
307308 // If not in compact mode or there is no session, we don't show the header
308309 return p.layout.View()
@@ -310,8 +311,8 @@ func (p *chatPage) View() tea.View {
310311 layoutView := p.layout.View()
311312 chatView := strings.Join(
312313 []string{
313− p.header.View().String(),
314− layoutView.String(),
314+ p.header.View(),
315+ layoutView,
315316 }, "\n",
316317 )
317318 layers := []*lipgloss.Layer{
@@ -326,7 +327,7 @@ func (p *chatPage) View() tea.View {
326327 details := style.Render(
327328 lipgloss.JoinVertical(
328329 lipgloss.Left,
329− p.compactSidebar.View().String(),
330+ p.compactSidebar.View(),
330331 version,
331332 ),
332333 )
@@ -335,9 +336,14 @@ func (p *chatPage) View() tea.View {
335336 canvas := lipgloss.NewCanvas(
336337 layers...,
337338 )
338− view := tea.NewView(canvas.Render())
339− view.SetCursor(layoutView.Cursor())
340− return view
339+ return canvas.Render()
340+}
341+
342+func (p *chatPage) Cursor() *tea.Cursor {
343+ if v, ok := p.layout.(util.Cursor); ok {
344+ return v.Cursor()
345+ }
346+ return nil
341347}
342348
343349func (p *chatPage) Bindings() []key.Binding {
internal/tui/page/logs/keys.godeleted−43
@@ -1,43 +0,0 @@
1−package logs
2−
3−import (
4− "github.com/charmbracelet/bubbles/v2/key"
5−)
6−
7−type KeyMap struct {
8− Back key.Binding
9−}
10−
11−func DefaultKeyMap() KeyMap {
12− return KeyMap{
13− Back: key.NewBinding(
14− key.WithKeys("esc", "backspace"),
15− key.WithHelp("esc/backspace", "back to chat"),
16− ),
17− }
18−}
19−
20−// KeyBindings implements layout.KeyMapProvider
21−func (k KeyMap) KeyBindings() []key.Binding {
22− return []key.Binding{
23− k.Back,
24− }
25−}
26−
27−// FullHelp implements help.KeyMap.
28−func (k KeyMap) FullHelp() [][]key.Binding {
29− m := [][]key.Binding{}
30− slice := k.KeyBindings()
31− for i := 0; i < len(slice); i += 4 {
32− end := min(i+4, len(slice))
33− m = append(m, slice[i:end])
34− }
35− return m
36−}
37−
38−// ShortHelp implements help.KeyMap.
39−func (k KeyMap) ShortHelp() []key.Binding {
40− return []key.Binding{
41− k.Back,
42− }
43−}
internal/tui/page/logs/logs.godeleted−100
@@ -1,100 +0,0 @@
1−package logs
2−
3−import (
4− "github.com/charmbracelet/bubbles/v2/key"
5− tea "github.com/charmbracelet/bubbletea/v2"
6− "github.com/charmbracelet/crush/internal/tui/components/core"
7− "github.com/charmbracelet/crush/internal/tui/components/core/layout"
8− logsComponents "github.com/charmbracelet/crush/internal/tui/components/logs"
9− "github.com/charmbracelet/crush/internal/tui/page"
10− "github.com/charmbracelet/crush/internal/tui/page/chat"
11− "github.com/charmbracelet/crush/internal/tui/styles"
12− "github.com/charmbracelet/crush/internal/tui/util"
13− "github.com/charmbracelet/lipgloss/v2"
14−)
15−
16−var LogsPage page.PageID = "logs"
17−
18−type LogPage interface {
19− util.Model
20− layout.Sizeable
21−}
22−
23−type logsPage struct {
24− width, height int
25− table logsComponents.TableComponent
26− details logsComponents.DetailComponent
27− keyMap KeyMap
28−}
29−
30−func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
31− var cmds []tea.Cmd
32− switch msg := msg.(type) {
33− case tea.WindowSizeMsg:
34− p.width = msg.Width
35− p.height = msg.Height
36− return p, p.SetSize(msg.Width, msg.Height)
37− case tea.KeyMsg:
38− switch {
39− case key.Matches(msg, p.keyMap.Back):
40− return p, util.CmdHandler(page.PageChangeMsg{ID: chat.ChatPageID})
41− }
42− }
43−
44− table, cmd := p.table.Update(msg)
45− cmds = append(cmds, cmd)
46− p.table = table.(logsComponents.TableComponent)
47− details, cmd := p.details.Update(msg)
48− cmds = append(cmds, cmd)
49− p.details = details.(logsComponents.DetailComponent)
50−
51− return p, tea.Batch(cmds...)
52−}
53−
54−func (p *logsPage) View() tea.View {
55− baseStyle := styles.CurrentTheme().S().Base
56− style := baseStyle.Width(p.width).Height(p.height).Padding(1)
57− title := core.Title("Logs", p.width-2)
58−
59− return tea.NewView(
60− style.Render(
61− lipgloss.JoinVertical(lipgloss.Top,
62− title,
63− p.details.View().String(),
64− p.table.View().String(),
65− ),
66− ),
67− )
68−}
69−
70−// GetSize implements LogPage.
71−func (p *logsPage) GetSize() (int, int) {
72− return p.width, p.height
73−}
74−
75−// SetSize implements LogPage.
76−func (p *logsPage) SetSize(width int, height int) tea.Cmd {
77− p.width = width
78− p.height = height
79− availableHeight := height - 2 // Padding for top and bottom
80− availableHeight -= 1 // title height
81− return tea.Batch(
82− p.table.SetSize(width-2, availableHeight/2),
83− p.details.SetSize(width-2, availableHeight/2),
84− )
85−}
86−
87−func (p *logsPage) Init() tea.Cmd {
88− return tea.Batch(
89− p.table.Init(),
90− p.details.Init(),
91− )
92−}
93−
94−func NewLogsPage() LogPage {
95− return &logsPage{
96− details: logsComponents.NewLogsDetails(),
97− table: logsComponents.NewLogsTable(),
98− keyMap: DefaultKeyMap(),
99− }
100−}
internal/tui/styles/theme.go+1−1
@@ -10,7 +10,7 @@ import (
1010 "github.com/charmbracelet/bubbles/v2/textarea"
1111 "github.com/charmbracelet/bubbles/v2/textinput"
1212 tea "github.com/charmbracelet/bubbletea/v2"
13− "github.com/charmbracelet/crush/internal/exp/diffview"
13+ "github.com/charmbracelet/crush/internal/tui/exp/diffview"
1414 "github.com/charmbracelet/glamour/v2/ansi"
1515 "github.com/charmbracelet/lipgloss/v2"
1616 "github.com/charmbracelet/x/exp/charmtone"
internal/tui/tui.go+22−34
@@ -9,7 +9,6 @@ import (
99 "github.com/charmbracelet/crush/internal/app"
1010 "github.com/charmbracelet/crush/internal/config"
1111 "github.com/charmbracelet/crush/internal/llm/agent"
12− "github.com/charmbracelet/crush/internal/logging"
1312 "github.com/charmbracelet/crush/internal/permission"
1413 "github.com/charmbracelet/crush/internal/pubsub"
1514 cmpChat "github.com/charmbracelet/crush/internal/tui/components/chat"
@@ -27,7 +26,6 @@ import (
2726 "github.com/charmbracelet/crush/internal/tui/components/dialogs/sessions"
2827 "github.com/charmbracelet/crush/internal/tui/page"
2928 "github.com/charmbracelet/crush/internal/tui/page/chat"
30− "github.com/charmbracelet/crush/internal/tui/page/logs"
3129 "github.com/charmbracelet/crush/internal/tui/styles"
3230 "github.com/charmbracelet/crush/internal/tui/util"
3331 "github.com/charmbracelet/lipgloss/v2"
@@ -84,6 +82,9 @@ func (a appModel) Init() tea.Cmd {
8482 return nil
8583 })
8684
85+ // Enable mouse support.
86+ cmds = append(cmds, tea.EnableMouseAllMotion)
87+
8788 return tea.Batch(cmds...)
8889}
8990
@@ -135,20 +136,6 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
135136 a.selectedSessionID = msg.ID
136137 case cmpChat.SessionClearedMsg:
137138 a.selectedSessionID = ""
138− // Logs
139− case pubsub.Event[logging.LogMessage]:
140− // Send to the status component
141− s, statusCmd := a.status.Update(msg)
142− a.status = s.(status.StatusCmp)
143− cmds = append(cmds, statusCmd)
144−
145− // If the current page is logs, update the logs view
146− if a.currentPage == logs.LogsPage {
147− updated, pageCmd := a.pages[a.currentPage].Update(msg)
148− a.pages[a.currentPage] = updated.(util.Model)
149− cmds = append(cmds, pageCmd)
150− }
151− return a, tea.Batch(cmds...)
152139 // Commands
153140 case commands.SwitchSessionsMsg:
154141 return a, func() tea.Msg {
@@ -176,15 +163,14 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
176163
177164 // Update the agent with the new model/provider configuration
178165 if err := a.app.UpdateAgentModel(); err != nil {
179− logging.ErrorPersist(fmt.Sprintf("Failed to update agent model: %v", err))
180− return a, util.ReportError(fmt.Errorf("model changed to %s but failed to update agent: %v", msg.Model.ModelID, err))
166+ return a, util.ReportError(fmt.Errorf("model changed to %s but failed to update agent: %v", msg.Model.Model, err))
181167 }
182168
183169 modelTypeName := "large"
184− if msg.ModelType == config.SmallModel {
170+ if msg.ModelType == config.SelectedModelTypeSmall {
185171 modelTypeName = "small"
186172 }
187− return a, util.ReportInfo(fmt.Sprintf("%s model changed to %s", modelTypeName, msg.Model.ModelID))
173+ return a, util.ReportInfo(fmt.Sprintf("%s model changed to %s", modelTypeName, msg.Model.Model))
188174
189175 // File Picker
190176 case chat.OpenFilePickerMsg:
@@ -348,10 +334,6 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
348334 },
349335 )
350336 return tea.Sequence(cmds...)
351− // Page navigation
352− case key.Matches(msg, a.keyMap.Logs):
353− return a.moveToPage(logs.LogsPage)
354−
355337 default:
356338 if a.dialog.HasDialogs() {
357339 u, dialogCmd := a.dialog.Update(msg)
@@ -397,9 +379,9 @@ func (a *appModel) View() tea.View {
397379 a.status.SetKeyMap(a.keyMap)
398380 pageView := page.View()
399381 components := []string{
400− pageView.String(),
382+ pageView,
401383 }
402− components = append(components, a.status.View().String())
384+ components = append(components, a.status.View())
403385
404386 appView := lipgloss.JoinVertical(lipgloss.Top, components...)
405387 layers := []*lipgloss.Layer{
@@ -412,14 +394,20 @@ func (a *appModel) View() tea.View {
412394 )
413395 }
414396
415− cursor := pageView.Cursor()
416− activeView := a.dialog.ActiveView()
397+ var cursor *tea.Cursor
398+ if v, ok := page.(util.Cursor); ok {
399+ cursor = v.Cursor()
400+ }
401+ activeView := a.dialog.ActiveModel()
417402 if activeView != nil {
418− cursor = activeView.Cursor()
403+ cursor = nil // Reset cursor if a dialog is active unless it implements util.Cursor
404+ if v, ok := activeView.(util.Cursor); ok {
405+ cursor = v.Cursor()
406+ }
419407 }
420408
421409 if a.completions.Open() && cursor != nil {
422− cmp := a.completions.View().String()
410+ cmp := a.completions.View()
423411 x, y := a.completions.Position()
424412 layers = append(
425413 layers,
@@ -431,10 +419,11 @@ func (a *appModel) View() tea.View {
431419 layers...,
432420 )
433421
422+ var view tea.View
434423 t := styles.CurrentTheme()
435− view := tea.NewView(canvas.Render())
436− view.SetBackgroundColor(t.BgBase)
437− view.SetCursor(cursor)
424+ view.Layer = canvas
425+ view.BackgroundColor = t.BgBase
426+ view.Cursor = cursor
438427 return view
439428}
440429
@@ -453,7 +442,6 @@ func New(app *app.App) tea.Model {
453442
454443 pages: map[page.PageID]util.Model{
455444 chat.ChatPageID: chatPage,
456− logs.LogsPage: logs.NewLogsPage(),
457445 },
458446
459447 dialog: dialogs.NewDialogCmp(),
internal/tui/util/util.go+5−1
@@ -6,9 +6,13 @@ import (
66 tea "github.com/charmbracelet/bubbletea/v2"
77)
88
9+type Cursor interface {
10+ Cursor() *tea.Cursor
11+}
12+
913type Model interface {
1014 tea.Model
11− tea.Viewable
15+ tea.ViewModel
1216}
1317
1418func CmdHandler(msg tea.Msg) tea.Cmd {
main.go+9−5
@@ -1,25 +1,29 @@
11package main
22
33import (
4+ "fmt"
5+ "log/slog"
46 "net/http"
57 "os"
68
79 _ "net/http/pprof" // profiling
810
11+ _ "github.com/joho/godotenv/autoload" // automatically load .env files
12+
913 "github.com/charmbracelet/crush/cmd"
10− "github.com/charmbracelet/crush/internal/logging"
14+ "github.com/charmbracelet/crush/internal/log"
1115)
1216
1317func main() {
14− defer logging.RecoverPanic("main", func() {
15− logging.ErrorPersist("Application terminated due to unhandled panic")
18+ defer log.RecoverPanic("main", func() {
19+ slog.Error("Application terminated due to unhandled panic")
1620 })
1721
1822 if os.Getenv("CRUSH_PROFILE") != "" {
1923 go func() {
20− logging.Info("Serving pprof at localhost:6060")
24+ slog.Info("Serving pprof at localhost:6060")
2125 if httpErr := http.ListenAndServe("localhost:6060", nil); httpErr != nil {
22− logging.Error("Failed to pprof listen: %v", httpErr)
26+ slog.Error(fmt.Sprintf("Failed to pprof listen: %v", httpErr))
2327 }
2428 }()
2529 }
todos.mddeleted−51
@@ -1,51 +0,0 @@
1−## TODOs before release
2−
3−- [x] Implement help
4− - [x] Show full help
5− - [x] Make help dependent on the focused pane and page
6−- [x] Implement current model in the sidebar
7−- [x] Implement LSP errors
8−- [x] Implement changed files
9− - [x] Implement initial load
10− - [x] Implement realtime file changes
11−- [ ] Events when tool error
12−- [ ] Support bash commands
13−- [ ] Editor attachments fixes
14− - [ ] Reimplement removing attachments
15−- [ ] Fix the logs view
16− - [ ] Review the implementation
17− - [ ] The page lags
18− - [ ] Make the logs long lived ?
19−- [ ] Add all possible actions to the commands
20−- [ ] Parallel tool calls and permissions
21− - [ ] Run the tools in parallel and add results in parallel
22− - [ ] Show multiple permissions dialogs
23−- [ ] Add another space around buttons
24−- [ ] Completions
25− - [ ] Should change the help to show the completions stuff
26− - [ ] Should make it wider
27− - [ ] Tab and ctrl+y should accept
28− - [ ] Words should line up
29− - [ ] If there are no completions and cick tab/ctrl+y/enter it should close it
30−- [ ] Investigate messages issues
31− - [ ] Make the agent separator look like the
32− - [ ] Cleanup tool calls (watch all states)
33− - [ ] Weird behavior sometimes the message does not update
34− - [ ] Message length (I saw the message go beyond the correct length when there are errors)
35− - [ ] Address UX issues
36− - [ ] Fix issue with numbers (padding) view tool
37−- [x] Implement responsive mode
38−- [ ] Update interactive mode to use the spinner
39−- [ ] Revisit the core list component
40− - [ ] This component has become super complex we might need to fix this.
41−- [ ] Handle correct LSP and MCP status icon
42−- [x] Investigate ways to make the spinner less CPU intensive
43−- [ ] General cleanup and documentation
44−- [ ] Update the readme
45−
46−## Maybe
47−
48−- [ ] Revisit the provider/model/configs
49−- [ ] Implement correct persistent shell
50−- [ ] Store file read/write time somewhere so that the we can make sure that even if we restart we do not need to re-read the same file
51−- [ ] Send updates to the UI when new LSP diagnostics are available
vendor/cloud.google.com/go/LICENSEnew file+202
@@ -0,0 +1,202 @@
1+
2+ Apache License
3+ Version 2.0, January 2004
4+ http://www.apache.org/licenses/
5+
6+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7+
8+ 1. Definitions.
9+
10+ "License" shall mean the terms and conditions for use, reproduction,
11+ and distribution as defined by Sections 1 through 9 of this document.
12+
13+ "Licensor" shall mean the copyright owner or entity authorized by
14+ the copyright owner that is granting the License.
15+
16+ "Legal Entity" shall mean the union of the acting entity and all
17+ other entities that control, are controlled by, or are under common
18+ control with that entity. For the purposes of this definition,
19+ "control" means (i) the power, direct or indirect, to cause the
20+ direction or management of such entity, whether by contract or
21+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22+ outstanding shares, or (iii) beneficial ownership of such entity.
23+
24+ "You" (or "Your") shall mean an individual or Legal Entity
25+ exercising permissions granted by this License.
26+
27+ "Source" form shall mean the preferred form for making modifications,
28+ including but not limited to software source code, documentation
29+ source, and configuration files.
30+
31+ "Object" form shall mean any form resulting from mechanical
32+ transformation or translation of a Source form, including but
33+ not limited to compiled object code, generated documentation,
34+ and conversions to other media types.
35+
36+ "Work" shall mean the work of authorship, whether in Source or
37+ Object form, made available under the License, as indicated by a
38+ copyright notice that is included in or attached to the work
39+ (an example is provided in the Appendix below).
40+
41+ "Derivative Works" shall mean any work, whether in Source or Object
42+ form, that is based on (or derived from) the Work and for which the
43+ editorial revisions, annotations, elaborations, or other modifications
44+ represent, as a whole, an original work of authorship. For the purposes
45+ of this License, Derivative Works shall not include works that remain
46+ separable from, or merely link (or bind by name) to the interfaces of,
47+ the Work and Derivative Works thereof.
48+
49+ "Contribution" shall mean any work of authorship, including
50+ the original version of the Work and any modifications or additions
51+ to that Work or Derivative Works thereof, that is intentionally
52+ submitted to Licensor for inclusion in the Work by the copyright owner
53+ or by an individual or Legal Entity authorized to submit on behalf of
54+ the copyright owner. For the purposes of this definition, "submitted"
55+ means any form of electronic, verbal, or written communication sent
56+ to the Licensor or its representatives, including but not limited to
57+ communication on electronic mailing lists, source code control systems,
58+ and issue tracking systems that are managed by, or on behalf of, the
59+ Licensor for the purpose of discussing and improving the Work, but
60+ excluding communication that is conspicuously marked or otherwise
61+ designated in writing by the copyright owner as "Not a Contribution."
62+
63+ "Contributor" shall mean Licensor and any individual or Legal Entity
64+ on behalf of whom a Contribution has been received by Licensor and
65+ subsequently incorporated within the Work.
66+
67+ 2. Grant of Copyright License. Subject to the terms and conditions of
68+ this License, each Contributor hereby grants to You a perpetual,
69+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70+ copyright license to reproduce, prepare Derivative Works of,
71+ publicly display, publicly perform, sublicense, and distribute the
72+ Work and such Derivative Works in Source or Object form.
73+
74+ 3. Grant of Patent License. Subject to the terms and conditions of
75+ this License, each Contributor hereby grants to You a perpetual,
76+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77+ (except as stated in this section) patent license to make, have made,
78+ use, offer to sell, sell, import, and otherwise transfer the Work,
79+ where such license applies only to those patent claims licensable
80+ by such Contributor that are necessarily infringed by their
81+ Contribution(s) alone or by combination of their Contribution(s)
82+ with the Work to which such Contribution(s) was submitted. If You
83+ institute patent litigation against any entity (including a
84+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85+ or a Contribution incorporated within the Work constitutes direct
86+ or contributory patent infringement, then any patent licenses
87+ granted to You under this License for that Work shall terminate
88+ as of the date such litigation is filed.
89+
90+ 4. Redistribution. You may reproduce and distribute copies of the
91+ Work or Derivative Works thereof in any medium, with or without
92+ modifications, and in Source or Object form, provided that You
93+ meet the following conditions:
94+
95+ (a) You must give any other recipients of the Work or
96+ Derivative Works a copy of this License; and
97+
98+ (b) You must cause any modified files to carry prominent notices
99+ stating that You changed the files; and
100+
101+ (c) You must retain, in the Source form of any Derivative Works
102+ that You distribute, all copyright, patent, trademark, and
103+ attribution notices from the Source form of the Work,
104+ excluding those notices that do not pertain to any part of
105+ the Derivative Works; and
106+
107+ (d) If the Work includes a "NOTICE" text file as part of its
108+ distribution, then any Derivative Works that You distribute must
109+ include a readable copy of the attribution notices contained
110+ within such NOTICE file, excluding those notices that do not
111+ pertain to any part of the Derivative Works, in at least one
112+ of the following places: within a NOTICE text file distributed
113+ as part of the Derivative Works; within the Source form or
114+ documentation, if provided along with the Derivative Works; or,
115+ within a display generated by the Derivative Works, if and
116+ wherever such third-party notices normally appear. The contents
117+ of the NOTICE file are for informational purposes only and
118+ do not modify the License. You may add Your own attribution
119+ notices within Derivative Works that You distribute, alongside
120+ or as an addendum to the NOTICE text from the Work, provided
121+ that such additional attribution notices cannot be construed
122+ as modifying the License.
123+
124+ You may add Your own copyright statement to Your modifications and
125+ may provide additional or different license terms and conditions
126+ for use, reproduction, or distribution of Your modifications, or
127+ for any such Derivative Works as a whole, provided Your use,
128+ reproduction, and distribution of the Work otherwise complies with
129+ the conditions stated in this License.
130+
131+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132+ any Contribution intentionally submitted for inclusion in the Work
133+ by You to the Licensor shall be under the terms and conditions of
134+ this License, without any additional terms or conditions.
135+ Notwithstanding the above, nothing herein shall supersede or modify
136+ the terms of any separate license agreement you may have executed
137+ with Licensor regarding such Contributions.
138+
139+ 6. Trademarks. This License does not grant permission to use the trade
140+ names, trademarks, service marks, or product names of the Licensor,
141+ except as required for reasonable and customary use in describing the
142+ origin of the Work and reproducing the content of the NOTICE file.
143+
144+ 7. Disclaimer of Warranty. Unless required by applicable law or
145+ agreed to in writing, Licensor provides the Work (and each
146+ Contributor provides its Contributions) on an "AS IS" BASIS,
147+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148+ implied, including, without limitation, any warranties or conditions
149+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150+ PARTICULAR PURPOSE. You are solely responsible for determining the
151+ appropriateness of using or redistributing the Work and assume any
152+ risks associated with Your exercise of permissions under this License.
153+
154+ 8. Limitation of Liability. In no event and under no legal theory,
155+ whether in tort (including negligence), contract, or otherwise,
156+ unless required by applicable law (such as deliberate and grossly
157+ negligent acts) or agreed to in writing, shall any Contributor be
158+ liable to You for damages, including any direct, indirect, special,
159+ incidental, or consequential damages of any character arising as a
160+ result of this License or out of the use or inability to use the
161+ Work (including but not limited to damages for loss of goodwill,
162+ work stoppage, computer failure or malfunction, or any and all
163+ other commercial damages or losses), even if such Contributor
164+ has been advised of the possibility of such damages.
165+
166+ 9. Accepting Warranty or Additional Liability. While redistributing
167+ the Work or Derivative Works thereof, You may choose to offer,
168+ and charge a fee for, acceptance of support, warranty, indemnity,
169+ or other liability obligations and/or rights consistent with this
170+ License. However, in accepting such obligations, You may act only
171+ on Your own behalf and on Your sole responsibility, not on behalf
172+ of any other Contributor, and only if You agree to indemnify,
173+ defend, and hold each Contributor harmless for any liability
174+ incurred by, or claims asserted against, such Contributor by reason
175+ of your accepting any such warranty or additional liability.
176+
177+ END OF TERMS AND CONDITIONS
178+
179+ APPENDIX: How to apply the Apache License to your work.
180+
181+ To apply the Apache License to your work, attach the following
182+ boilerplate notice, with the fields enclosed by brackets "[]"
183+ replaced with your own identifying information. (Don't include
184+ the brackets!) The text should be enclosed in the appropriate
185+ comment syntax for the file format. We also recommend that a
186+ file or class name and description of purpose be included on the
187+ same "printed page" as the copyright notice for easier
188+ identification within third-party archives.
189+
190+ Copyright [yyyy] [name of copyright owner]
191+
192+ Licensed under the Apache License, Version 2.0 (the "License");
193+ you may not use this file except in compliance with the License.
194+ You may obtain a copy of the License at
195+
196+ http://www.apache.org/licenses/LICENSE-2.0
197+
198+ Unless required by applicable law or agreed to in writing, software
199+ distributed under the License is distributed on an "AS IS" BASIS,
200+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201+ See the License for the specific language governing permissions and
202+ limitations under the License.
vendor/cloud.google.com/go/auth/CHANGES.mdnew file+368
@@ -0,0 +1,368 @@
1+# Changelog
2+
3+## [0.13.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.12.1...auth/v0.13.0) (2024-12-13)
4+
5+
6+### Features
7+
8+* **auth:** Add logging support ([#11079](https://github.com/googleapis/google-cloud-go/issues/11079)) ([c80e31d](https://github.com/googleapis/google-cloud-go/commit/c80e31df5ecb33a810be3dfb9d9e27ac531aa91d))
9+* **auth:** Pass logger from auth layer to metadata package ([#11288](https://github.com/googleapis/google-cloud-go/issues/11288)) ([b552efd](https://github.com/googleapis/google-cloud-go/commit/b552efd6ab34e5dfded18438e0fbfd925805614f))
10+
11+
12+### Bug Fixes
13+
14+* **auth:** Check compute cred type before non-default flag for DP ([#11255](https://github.com/googleapis/google-cloud-go/issues/11255)) ([4347ca1](https://github.com/googleapis/google-cloud-go/commit/4347ca141892be8ae813399b4b437662a103bc90))
15+
16+## [0.12.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.12.0...auth/v0.12.1) (2024-12-10)
17+
18+
19+### Bug Fixes
20+
21+* **auth:** Correct typo in link ([#11160](https://github.com/googleapis/google-cloud-go/issues/11160)) ([af6fb46](https://github.com/googleapis/google-cloud-go/commit/af6fb46d7cd694ddbe8c9d63bc4cdcd62b9fb2c1))
22+
23+## [0.12.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.11.0...auth/v0.12.0) (2024-12-04)
24+
25+
26+### Features
27+
28+* **auth:** Add support for providing custom certificate URL ([#11006](https://github.com/googleapis/google-cloud-go/issues/11006)) ([ebf3657](https://github.com/googleapis/google-cloud-go/commit/ebf36579724afb375d3974cf1da38f703e3b7dbc)), refs [#11005](https://github.com/googleapis/google-cloud-go/issues/11005)
29+
30+
31+### Bug Fixes
32+
33+* **auth:** Ensure endpoints are present in Validator ([#11209](https://github.com/googleapis/google-cloud-go/issues/11209)) ([106cd53](https://github.com/googleapis/google-cloud-go/commit/106cd53309facaef1b8ea78376179f523f6912b9)), refs [#11006](https://github.com/googleapis/google-cloud-go/issues/11006) [#11190](https://github.com/googleapis/google-cloud-go/issues/11190) [#11189](https://github.com/googleapis/google-cloud-go/issues/11189) [#11188](https://github.com/googleapis/google-cloud-go/issues/11188)
34+
35+## [0.11.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.10.2...auth/v0.11.0) (2024-11-21)
36+
37+
38+### Features
39+
40+* **auth:** Add universe domain support to mTLS ([#11159](https://github.com/googleapis/google-cloud-go/issues/11159)) ([117748b](https://github.com/googleapis/google-cloud-go/commit/117748ba1cfd4ae62a6a4feb7e30951cb2bc9344))
41+
42+## [0.10.2](https://github.com/googleapis/google-cloud-go/compare/auth/v0.10.1...auth/v0.10.2) (2024-11-12)
43+
44+
45+### Bug Fixes
46+
47+* **auth:** Restore use of grpc.Dial ([#11118](https://github.com/googleapis/google-cloud-go/issues/11118)) ([2456b94](https://github.com/googleapis/google-cloud-go/commit/2456b943b7b8aaabd4d8bfb7572c0f477ae0db45)), refs [#7556](https://github.com/googleapis/google-cloud-go/issues/7556)
48+
49+## [0.10.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.10.0...auth/v0.10.1) (2024-11-06)
50+
51+
52+### Bug Fixes
53+
54+* **auth:** Restore Application Default Credentials support to idtoken ([#11083](https://github.com/googleapis/google-cloud-go/issues/11083)) ([8771f2e](https://github.com/googleapis/google-cloud-go/commit/8771f2ea9807ab822083808e0678392edff3b4f2))
55+* **auth:** Skip impersonate universe domain check if empty ([#11086](https://github.com/googleapis/google-cloud-go/issues/11086)) ([87159c1](https://github.com/googleapis/google-cloud-go/commit/87159c1059d4a18d1367ce62746a838a94964ab6))
56+
57+## [0.10.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.9...auth/v0.10.0) (2024-10-30)
58+
59+
60+### Features
61+
62+* **auth:** Add universe domain support to credentials/impersonate ([#10953](https://github.com/googleapis/google-cloud-go/issues/10953)) ([e06cb64](https://github.com/googleapis/google-cloud-go/commit/e06cb6499f7eda3aef08ab18ff197016f667684b))
63+
64+## [0.9.9](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.8...auth/v0.9.9) (2024-10-22)
65+
66+
67+### Bug Fixes
68+
69+* **auth:** Fallback cert lookups for missing files ([#11013](https://github.com/googleapis/google-cloud-go/issues/11013)) ([bd76695](https://github.com/googleapis/google-cloud-go/commit/bd766957ec238b7c40ddbabb369e612dc9b07313)), refs [#10844](https://github.com/googleapis/google-cloud-go/issues/10844)
70+* **auth:** Replace MDS endpoint universe_domain with universe-domain ([#11000](https://github.com/googleapis/google-cloud-go/issues/11000)) ([6a1586f](https://github.com/googleapis/google-cloud-go/commit/6a1586f2ce9974684affaea84e7b629313b4d114))
71+
72+## [0.9.8](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.7...auth/v0.9.8) (2024-10-09)
73+
74+
75+### Bug Fixes
76+
77+* **auth:** Restore OpenTelemetry handling in transports ([#10968](https://github.com/googleapis/google-cloud-go/issues/10968)) ([08c6d04](https://github.com/googleapis/google-cloud-go/commit/08c6d04901c1a20e219b2d86df41dbaa6d7d7b55)), refs [#10962](https://github.com/googleapis/google-cloud-go/issues/10962)
78+* **auth:** Try talk to plaintext S2A if credentials can not be found for mTLS-S2A ([#10941](https://github.com/googleapis/google-cloud-go/issues/10941)) ([0f0bf2d](https://github.com/googleapis/google-cloud-go/commit/0f0bf2d18c97dd8b65bcf0099f0802b5631c6287))
79+
80+## [0.9.7](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.6...auth/v0.9.7) (2024-10-01)
81+
82+
83+### Bug Fixes
84+
85+* **auth:** Restore support for non-default service accounts for DirectPath ([#10937](https://github.com/googleapis/google-cloud-go/issues/10937)) ([a38650e](https://github.com/googleapis/google-cloud-go/commit/a38650edbf420223077498cafa537aec74b37aad)), refs [#10907](https://github.com/googleapis/google-cloud-go/issues/10907)
86+
87+## [0.9.6](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.5...auth/v0.9.6) (2024-09-30)
88+
89+
90+### Bug Fixes
91+
92+* **auth:** Make aws credentials provider retrieve fresh credentials ([#10920](https://github.com/googleapis/google-cloud-go/issues/10920)) ([250fbf8](https://github.com/googleapis/google-cloud-go/commit/250fbf87d858d865e399a241b7e537c4ff0c3dd8))
93+
94+## [0.9.5](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.4...auth/v0.9.5) (2024-09-25)
95+
96+
97+### Bug Fixes
98+
99+* **auth:** Restore support for GOOGLE_CLOUD_UNIVERSE_DOMAIN env ([#10915](https://github.com/googleapis/google-cloud-go/issues/10915)) ([94caaaa](https://github.com/googleapis/google-cloud-go/commit/94caaaa061362d0e00ef6214afcc8a0a3e7ebfb2))
100+* **auth:** Skip directpath credentials overwrite when it's not on GCE ([#10833](https://github.com/googleapis/google-cloud-go/issues/10833)) ([7e5e8d1](https://github.com/googleapis/google-cloud-go/commit/7e5e8d10b761b0a6e43e19a028528db361bc07b1))
101+* **auth:** Use new context for non-blocking token refresh ([#10919](https://github.com/googleapis/google-cloud-go/issues/10919)) ([cf7102d](https://github.com/googleapis/google-cloud-go/commit/cf7102d33a21be1e5a9d47a49456b3a57c43b350))
102+
103+## [0.9.4](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.3...auth/v0.9.4) (2024-09-11)
104+
105+
106+### Bug Fixes
107+
108+* **auth:** Enable self-signed JWT for non-GDU universe domain ([#10831](https://github.com/googleapis/google-cloud-go/issues/10831)) ([f9869f7](https://github.com/googleapis/google-cloud-go/commit/f9869f7903cfd34d1b97c25d0dc5669d2c5138e6))
109+
110+## [0.9.3](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.2...auth/v0.9.3) (2024-09-03)
111+
112+
113+### Bug Fixes
114+
115+* **auth:** Choose quota project envvar over file when both present ([#10807](https://github.com/googleapis/google-cloud-go/issues/10807)) ([2d8dd77](https://github.com/googleapis/google-cloud-go/commit/2d8dd7700eff92d4b95027be55e26e1e7aa79181)), refs [#10804](https://github.com/googleapis/google-cloud-go/issues/10804)
116+
117+## [0.9.2](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.1...auth/v0.9.2) (2024-08-30)
118+
119+
120+### Bug Fixes
121+
122+* **auth:** Handle non-Transport DefaultTransport ([#10733](https://github.com/googleapis/google-cloud-go/issues/10733)) ([98d91dc](https://github.com/googleapis/google-cloud-go/commit/98d91dc8316b247498fab41ab35e57a0446fe556)), refs [#10742](https://github.com/googleapis/google-cloud-go/issues/10742)
123+* **auth:** Make sure quota option takes precedence over env/file ([#10797](https://github.com/googleapis/google-cloud-go/issues/10797)) ([f1b050d](https://github.com/googleapis/google-cloud-go/commit/f1b050d56d804b245cab048c2980d32b0eaceb4e)), refs [#10795](https://github.com/googleapis/google-cloud-go/issues/10795)
124+
125+
126+### Documentation
127+
128+* **auth:** Fix Go doc comment link ([#10751](https://github.com/googleapis/google-cloud-go/issues/10751)) ([015acfa](https://github.com/googleapis/google-cloud-go/commit/015acfab4d172650928bb1119bc2cd6307b9a437))
129+
130+## [0.9.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.0...auth/v0.9.1) (2024-08-22)
131+
132+
133+### Bug Fixes
134+
135+* **auth:** Setting expireEarly to default when the value is 0 ([#10732](https://github.com/googleapis/google-cloud-go/issues/10732)) ([5e67869](https://github.com/googleapis/google-cloud-go/commit/5e67869a31e9e8ecb4eeebd2cfa11a761c3b1948))
136+
137+## [0.9.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.8.1...auth/v0.9.0) (2024-08-16)
138+
139+
140+### Features
141+
142+* **auth:** Auth library can talk to S2A over mTLS ([#10634](https://github.com/googleapis/google-cloud-go/issues/10634)) ([5250a13](https://github.com/googleapis/google-cloud-go/commit/5250a13ec95b8d4eefbe0158f82857ff2189cb45))
143+
144+## [0.8.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.8.0...auth/v0.8.1) (2024-08-13)
145+
146+
147+### Bug Fixes
148+
149+* **auth:** Make default client creation more lenient ([#10669](https://github.com/googleapis/google-cloud-go/issues/10669)) ([1afb9ee](https://github.com/googleapis/google-cloud-go/commit/1afb9ee1ee9de9810722800018133304a0ca34d1)), refs [#10638](https://github.com/googleapis/google-cloud-go/issues/10638)
150+
151+## [0.8.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.7.3...auth/v0.8.0) (2024-08-07)
152+
153+
154+### Features
155+
156+* **auth:** Adds support for X509 workload identity federation ([#10373](https://github.com/googleapis/google-cloud-go/issues/10373)) ([5d07505](https://github.com/googleapis/google-cloud-go/commit/5d075056cbe27bb1da4072a26070c41f8999eb9b))
157+
158+## [0.7.3](https://github.com/googleapis/google-cloud-go/compare/auth/v0.7.2...auth/v0.7.3) (2024-08-01)
159+
160+
161+### Bug Fixes
162+
163+* **auth/oauth2adapt:** Update dependencies ([257c40b](https://github.com/googleapis/google-cloud-go/commit/257c40bd6d7e59730017cf32bda8823d7a232758))
164+* **auth:** Disable automatic universe domain check for MDS ([#10620](https://github.com/googleapis/google-cloud-go/issues/10620)) ([7cea5ed](https://github.com/googleapis/google-cloud-go/commit/7cea5edd5a0c1e6bca558696f5607879141910e8))
165+* **auth:** Update dependencies ([257c40b](https://github.com/googleapis/google-cloud-go/commit/257c40bd6d7e59730017cf32bda8823d7a232758))
166+
167+## [0.7.2](https://github.com/googleapis/google-cloud-go/compare/auth/v0.7.1...auth/v0.7.2) (2024-07-22)
168+
169+
170+### Bug Fixes
171+
172+* **auth:** Use default client for universe metadata lookup ([#10551](https://github.com/googleapis/google-cloud-go/issues/10551)) ([d9046fd](https://github.com/googleapis/google-cloud-go/commit/d9046fdd1435d1ce48f374806c1def4cb5ac6cd3)), refs [#10544](https://github.com/googleapis/google-cloud-go/issues/10544)
173+
174+## [0.7.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.7.0...auth/v0.7.1) (2024-07-10)
175+
176+
177+### Bug Fixes
178+
179+* **auth:** Bump google.golang.org/grpc@v1.64.1 ([8ecc4e9](https://github.com/googleapis/google-cloud-go/commit/8ecc4e9622e5bbe9b90384d5848ab816027226c5))
180+
181+## [0.7.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.6.1...auth/v0.7.0) (2024-07-09)
182+
183+
184+### Features
185+
186+* **auth:** Add workload X509 cert provider as a default cert provider ([#10479](https://github.com/googleapis/google-cloud-go/issues/10479)) ([c51ee6c](https://github.com/googleapis/google-cloud-go/commit/c51ee6cf65ce05b4d501083e49d468c75ac1ea63))
187+
188+
189+### Bug Fixes
190+
191+* **auth/oauth2adapt:** Bump google.golang.org/api@v0.187.0 ([8fa9e39](https://github.com/googleapis/google-cloud-go/commit/8fa9e398e512fd8533fd49060371e61b5725a85b))
192+* **auth:** Bump google.golang.org/api@v0.187.0 ([8fa9e39](https://github.com/googleapis/google-cloud-go/commit/8fa9e398e512fd8533fd49060371e61b5725a85b))
193+* **auth:** Check len of slices, not non-nil ([#10483](https://github.com/googleapis/google-cloud-go/issues/10483)) ([0a966a1](https://github.com/googleapis/google-cloud-go/commit/0a966a183e5f0e811977216d736d875b7233e942))
194+
195+## [0.6.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.6.0...auth/v0.6.1) (2024-07-01)
196+
197+
198+### Bug Fixes
199+
200+* **auth:** Support gRPC API keys ([#10460](https://github.com/googleapis/google-cloud-go/issues/10460)) ([daa6646](https://github.com/googleapis/google-cloud-go/commit/daa6646d2af5d7fb5b30489f4934c7db89868c7c))
201+* **auth:** Update http and grpc transports to support token exchange over mTLS ([#10397](https://github.com/googleapis/google-cloud-go/issues/10397)) ([c6dfdcf](https://github.com/googleapis/google-cloud-go/commit/c6dfdcf893c3f971eba15026c12db0a960ae81f2))
202+
203+## [0.6.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.5.2...auth/v0.6.0) (2024-06-25)
204+
205+
206+### Features
207+
208+* **auth:** Add non-blocking token refresh for compute MDS ([#10263](https://github.com/googleapis/google-cloud-go/issues/10263)) ([9ac350d](https://github.com/googleapis/google-cloud-go/commit/9ac350da11a49b8e2174d3fc5b1a5070fec78b4e))
209+
210+
211+### Bug Fixes
212+
213+* **auth:** Return error if envvar detected file returns an error ([#10431](https://github.com/googleapis/google-cloud-go/issues/10431)) ([e52b9a7](https://github.com/googleapis/google-cloud-go/commit/e52b9a7c45468827f5d220ab00965191faeb9d05))
214+
215+## [0.5.2](https://github.com/googleapis/google-cloud-go/compare/auth/v0.5.1...auth/v0.5.2) (2024-06-24)
216+
217+
218+### Bug Fixes
219+
220+* **auth:** Fetch initial token when CachedTokenProviderOptions.DisableAutoRefresh is true ([#10415](https://github.com/googleapis/google-cloud-go/issues/10415)) ([3266763](https://github.com/googleapis/google-cloud-go/commit/32667635ca2efad05cd8c087c004ca07d7406913)), refs [#10414](https://github.com/googleapis/google-cloud-go/issues/10414)
221+
222+## [0.5.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.5.0...auth/v0.5.1) (2024-05-31)
223+
224+
225+### Bug Fixes
226+
227+* **auth:** Pass through client to 2LO and 3LO flows ([#10290](https://github.com/googleapis/google-cloud-go/issues/10290)) ([685784e](https://github.com/googleapis/google-cloud-go/commit/685784ea84358c15e9214bdecb307d37aa3b6d2f))
228+
229+## [0.5.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.4.2...auth/v0.5.0) (2024-05-28)
230+
231+
232+### Features
233+
234+* **auth:** Adds X509 workload certificate provider ([#10233](https://github.com/googleapis/google-cloud-go/issues/10233)) ([17a9db7](https://github.com/googleapis/google-cloud-go/commit/17a9db73af35e3d1a7a25ac4fd1377a103de6150))
235+
236+## [0.4.2](https://github.com/googleapis/google-cloud-go/compare/auth/v0.4.1...auth/v0.4.2) (2024-05-16)
237+
238+
239+### Bug Fixes
240+
241+* **auth:** Enable client certificates by default only for GDU ([#10151](https://github.com/googleapis/google-cloud-go/issues/10151)) ([7c52978](https://github.com/googleapis/google-cloud-go/commit/7c529786275a39b7e00525f7d5e7be0d963e9e15))
242+* **auth:** Handle non-Transport DefaultTransport ([#10162](https://github.com/googleapis/google-cloud-go/issues/10162)) ([fa3bfdb](https://github.com/googleapis/google-cloud-go/commit/fa3bfdb23aaa45b34394a8b61e753b3587506782)), refs [#10159](https://github.com/googleapis/google-cloud-go/issues/10159)
243+* **auth:** Have refresh time match docs ([#10147](https://github.com/googleapis/google-cloud-go/issues/10147)) ([bcb5568](https://github.com/googleapis/google-cloud-go/commit/bcb5568c07a54dd3d2e869d15f502b0741a609e8))
244+* **auth:** Update compute token fetching error with named prefix ([#10180](https://github.com/googleapis/google-cloud-go/issues/10180)) ([4573504](https://github.com/googleapis/google-cloud-go/commit/4573504828d2928bebedc875d87650ba227829ea))
245+
246+## [0.4.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.4.0...auth/v0.4.1) (2024-05-09)
247+
248+
249+### Bug Fixes
250+
251+* **auth:** Don't try to detect default creds it opt configured ([#10143](https://github.com/googleapis/google-cloud-go/issues/10143)) ([804632e](https://github.com/googleapis/google-cloud-go/commit/804632e7c5b0b85ff522f7951114485e256eb5bc))
252+
253+## [0.4.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.3.0...auth/v0.4.0) (2024-05-07)
254+
255+
256+### Features
257+
258+* **auth:** Enable client certificates by default ([#10102](https://github.com/googleapis/google-cloud-go/issues/10102)) ([9013e52](https://github.com/googleapis/google-cloud-go/commit/9013e5200a6ec0f178ed91acb255481ffb073a2c))
259+
260+
261+### Bug Fixes
262+
263+* **auth:** Get s2a logic up to date ([#10093](https://github.com/googleapis/google-cloud-go/issues/10093)) ([4fe9ae4](https://github.com/googleapis/google-cloud-go/commit/4fe9ae4b7101af2a5221d6d6b2e77b479305bb06))
264+
265+## [0.3.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.2.2...auth/v0.3.0) (2024-04-23)
266+
267+
268+### Features
269+
270+* **auth/httptransport:** Add ability to customize transport ([#10023](https://github.com/googleapis/google-cloud-go/issues/10023)) ([72c7f6b](https://github.com/googleapis/google-cloud-go/commit/72c7f6bbec3136cc7a62788fc7186bc33ef6c3b3)), refs [#9812](https://github.com/googleapis/google-cloud-go/issues/9812) [#9814](https://github.com/googleapis/google-cloud-go/issues/9814)
271+
272+
273+### Bug Fixes
274+
275+* **auth/credentials:** Error on bad file name if explicitly set ([#10018](https://github.com/googleapis/google-cloud-go/issues/10018)) ([55beaa9](https://github.com/googleapis/google-cloud-go/commit/55beaa993aaf052d8be39766afc6777c3c2a0bdd)), refs [#9809](https://github.com/googleapis/google-cloud-go/issues/9809)
276+
277+## [0.2.2](https://github.com/googleapis/google-cloud-go/compare/auth/v0.2.1...auth/v0.2.2) (2024-04-19)
278+
279+
280+### Bug Fixes
281+
282+* **auth:** Add internal opt to skip validation on transports ([#9999](https://github.com/googleapis/google-cloud-go/issues/9999)) ([9e20ef8](https://github.com/googleapis/google-cloud-go/commit/9e20ef89f6287d6bd03b8697d5898dc43b4a77cf)), refs [#9823](https://github.com/googleapis/google-cloud-go/issues/9823)
283+* **auth:** Set secure flag for gRPC conn pools ([#10002](https://github.com/googleapis/google-cloud-go/issues/10002)) ([14e3956](https://github.com/googleapis/google-cloud-go/commit/14e3956dfd736399731b5ee8d9b178ae085cf7ba)), refs [#9833](https://github.com/googleapis/google-cloud-go/issues/9833)
284+
285+## [0.2.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.2.0...auth/v0.2.1) (2024-04-18)
286+
287+
288+### Bug Fixes
289+
290+* **auth:** Default gRPC token type to Bearer if not set ([#9800](https://github.com/googleapis/google-cloud-go/issues/9800)) ([5284066](https://github.com/googleapis/google-cloud-go/commit/5284066670b6fe65d79089cfe0199c9660f87fc7))
291+
292+## [0.2.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.1.1...auth/v0.2.0) (2024-04-15)
293+
294+### Breaking Changes
295+
296+In the below mentioned commits there were a few large breaking changes since the
297+last release of the module.
298+
299+1. The `Credentials` type has been moved to the root of the module as it is
300+ becoming the core abstraction for the whole module.
301+2. Because of the above mentioned change many functions that previously
302+ returned a `TokenProvider` now return `Credentials`. Similarly, these
303+ functions have been renamed to be more specific.
304+3. Most places that used to take an optional `TokenProvider` now accept
305+ `Credentials`. You can make a `Credentials` from a `TokenProvider` using the
306+ constructor found in the `auth` package.
307+4. The `detect` package has been renamed to `credentials`. With this change some
308+ function signatures were also updated for better readability.
309+5. Derivative auth flows like `impersonate` and `downscope` have been moved to
310+ be under the new `credentials` package.
311+
312+Although these changes are disruptive we think that they are for the best of the
313+long-term health of the module. We do not expect any more large breaking changes
314+like these in future revisions, even before 1.0.0. This version will be the
315+first version of the auth library that our client libraries start to use and
316+depend on.
317+
318+### Features
319+
320+* **auth/credentials/externalaccount:** Add default TokenURL ([#9700](https://github.com/googleapis/google-cloud-go/issues/9700)) ([81830e6](https://github.com/googleapis/google-cloud-go/commit/81830e6848ceefd055aa4d08f933d1154455a0f6))
321+* **auth:** Add downscope.Options.UniverseDomain ([#9634](https://github.com/googleapis/google-cloud-go/issues/9634)) ([52cf7d7](https://github.com/googleapis/google-cloud-go/commit/52cf7d780853594291c4e34302d618299d1f5a1d))
322+* **auth:** Add universe domain to grpctransport and httptransport ([#9663](https://github.com/googleapis/google-cloud-go/issues/9663)) ([67d353b](https://github.com/googleapis/google-cloud-go/commit/67d353beefe3b607c08c891876fbd95ab89e5fe3)), refs [#9670](https://github.com/googleapis/google-cloud-go/issues/9670)
323+* **auth:** Add UniverseDomain to DetectOptions ([#9536](https://github.com/googleapis/google-cloud-go/issues/9536)) ([3618d3f](https://github.com/googleapis/google-cloud-go/commit/3618d3f7061615c0e189f376c75abc201203b501))
324+* **auth:** Make package externalaccount public ([#9633](https://github.com/googleapis/google-cloud-go/issues/9633)) ([a0978d8](https://github.com/googleapis/google-cloud-go/commit/a0978d8e96968399940ebd7d092539772bf9caac))
325+* **auth:** Move credentials to base auth package ([#9590](https://github.com/googleapis/google-cloud-go/issues/9590)) ([1a04baf](https://github.com/googleapis/google-cloud-go/commit/1a04bafa83c27342b9308d785645e1e5423ea10d))
326+* **auth:** Refactor public sigs to use Credentials ([#9603](https://github.com/googleapis/google-cloud-go/issues/9603)) ([69cb240](https://github.com/googleapis/google-cloud-go/commit/69cb240c530b1f7173a9af2555c19e9a1beb56c5))
327+
328+
329+### Bug Fixes
330+
331+* **auth/oauth2adapt:** Update protobuf dep to v1.33.0 ([30b038d](https://github.com/googleapis/google-cloud-go/commit/30b038d8cac0b8cd5dd4761c87f3f298760dd33a))
332+* **auth:** Fix uint32 conversion ([9221c7f](https://github.com/googleapis/google-cloud-go/commit/9221c7fa12cef9d5fb7ddc92f41f1d6204971c7b))
333+* **auth:** Port sts expires fix ([#9618](https://github.com/googleapis/google-cloud-go/issues/9618)) ([7bec97b](https://github.com/googleapis/google-cloud-go/commit/7bec97b2f51ed3ac4f9b88bf100d301da3f5d1bd))
334+* **auth:** Read universe_domain from all credentials files ([#9632](https://github.com/googleapis/google-cloud-go/issues/9632)) ([16efbb5](https://github.com/googleapis/google-cloud-go/commit/16efbb52e39ea4a319e5ee1e95c0e0305b6d9824))
335+* **auth:** Remove content-type header from idms get requests ([#9508](https://github.com/googleapis/google-cloud-go/issues/9508)) ([8589f41](https://github.com/googleapis/google-cloud-go/commit/8589f41599d265d7c3d46a3d86c9fab2329cbdd9))
336+* **auth:** Update protobuf dep to v1.33.0 ([30b038d](https://github.com/googleapis/google-cloud-go/commit/30b038d8cac0b8cd5dd4761c87f3f298760dd33a))
337+
338+## [0.1.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.1.0...auth/v0.1.1) (2024-03-10)
339+
340+
341+### Bug Fixes
342+
343+* **auth/impersonate:** Properly send default detect params ([#9529](https://github.com/googleapis/google-cloud-go/issues/9529)) ([5b6b8be](https://github.com/googleapis/google-cloud-go/commit/5b6b8bef577f82707e51f5cc5d258d5bdf90218f)), refs [#9136](https://github.com/googleapis/google-cloud-go/issues/9136)
344+* **auth:** Update grpc-go to v1.56.3 ([343cea8](https://github.com/googleapis/google-cloud-go/commit/343cea8c43b1e31ae21ad50ad31d3b0b60143f8c))
345+* **auth:** Update grpc-go to v1.59.0 ([81a97b0](https://github.com/googleapis/google-cloud-go/commit/81a97b06cb28b25432e4ece595c55a9857e960b7))
346+
347+## 0.1.0 (2023-10-18)
348+
349+
350+### Features
351+
352+* **auth:** Add base auth package ([#8465](https://github.com/googleapis/google-cloud-go/issues/8465)) ([6a45f26](https://github.com/googleapis/google-cloud-go/commit/6a45f26b809b64edae21f312c18d4205f96b180e))
353+* **auth:** Add cert support to httptransport ([#8569](https://github.com/googleapis/google-cloud-go/issues/8569)) ([37e3435](https://github.com/googleapis/google-cloud-go/commit/37e3435f8e98595eafab481bdfcb31a4c56fa993))
354+* **auth:** Add Credentials.UniverseDomain() ([#8654](https://github.com/googleapis/google-cloud-go/issues/8654)) ([af0aa1e](https://github.com/googleapis/google-cloud-go/commit/af0aa1ed8015bc8fe0dd87a7549ae029107cbdb8))
355+* **auth:** Add detect package ([#8491](https://github.com/googleapis/google-cloud-go/issues/8491)) ([d977419](https://github.com/googleapis/google-cloud-go/commit/d977419a3269f6acc193df77a2136a6eb4b4add7))
356+* **auth:** Add downscope package ([#8532](https://github.com/googleapis/google-cloud-go/issues/8532)) ([dda9bff](https://github.com/googleapis/google-cloud-go/commit/dda9bff8ec70e6d104901b4105d13dcaa4e2404c))
357+* **auth:** Add grpctransport package ([#8625](https://github.com/googleapis/google-cloud-go/issues/8625)) ([69a8347](https://github.com/googleapis/google-cloud-go/commit/69a83470bdcc7ed10c6c36d1abc3b7cfdb8a0ee5))
358+* **auth:** Add httptransport package ([#8567](https://github.com/googleapis/google-cloud-go/issues/8567)) ([6898597](https://github.com/googleapis/google-cloud-go/commit/6898597d2ea95d630fcd00fd15c58c75ea843bff))
359+* **auth:** Add idtoken package ([#8580](https://github.com/googleapis/google-cloud-go/issues/8580)) ([a79e693](https://github.com/googleapis/google-cloud-go/commit/a79e693e97e4e3e1c6742099af3dbc58866d88fe))
360+* **auth:** Add impersonate package ([#8578](https://github.com/googleapis/google-cloud-go/issues/8578)) ([e29ba0c](https://github.com/googleapis/google-cloud-go/commit/e29ba0cb7bd3888ab9e808087027dc5a32474c04))
361+* **auth:** Add support for external accounts in detect ([#8508](https://github.com/googleapis/google-cloud-go/issues/8508)) ([62210d5](https://github.com/googleapis/google-cloud-go/commit/62210d5d3e56e8e9f35db8e6ac0defec19582507))
362+* **auth:** Port external account changes ([#8697](https://github.com/googleapis/google-cloud-go/issues/8697)) ([5823db5](https://github.com/googleapis/google-cloud-go/commit/5823db5d633069999b58b9131a7f9cd77e82c899))
363+
364+
365+### Bug Fixes
366+
367+* **auth/oauth2adapt:** Update golang.org/x/net to v0.17.0 ([174da47](https://github.com/googleapis/google-cloud-go/commit/174da47254fefb12921bbfc65b7829a453af6f5d))
368+* **auth:** Update golang.org/x/net to v0.17.0 ([174da47](https://github.com/googleapis/google-cloud-go/commit/174da47254fefb12921bbfc65b7829a453af6f5d))
vendor/cloud.google.com/go/auth/LICENSEnew file+202
@@ -0,0 +1,202 @@
1+
2+ Apache License
3+ Version 2.0, January 2004
4+ http://www.apache.org/licenses/
5+
6+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7+
8+ 1. Definitions.
9+
10+ "License" shall mean the terms and conditions for use, reproduction,
11+ and distribution as defined by Sections 1 through 9 of this document.
12+
13+ "Licensor" shall mean the copyright owner or entity authorized by
14+ the copyright owner that is granting the License.
15+
16+ "Legal Entity" shall mean the union of the acting entity and all
17+ other entities that control, are controlled by, or are under common
18+ control with that entity. For the purposes of this definition,
19+ "control" means (i) the power, direct or indirect, to cause the
20+ direction or management of such entity, whether by contract or
21+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22+ outstanding shares, or (iii) beneficial ownership of such entity.
23+
24+ "You" (or "Your") shall mean an individual or Legal Entity
25+ exercising permissions granted by this License.
26+
27+ "Source" form shall mean the preferred form for making modifications,
28+ including but not limited to software source code, documentation
29+ source, and configuration files.
30+
31+ "Object" form shall mean any form resulting from mechanical
32+ transformation or translation of a Source form, including but
33+ not limited to compiled object code, generated documentation,
34+ and conversions to other media types.
35+
36+ "Work" shall mean the work of authorship, whether in Source or
37+ Object form, made available under the License, as indicated by a
38+ copyright notice that is included in or attached to the work
39+ (an example is provided in the Appendix below).
40+
41+ "Derivative Works" shall mean any work, whether in Source or Object
42+ form, that is based on (or derived from) the Work and for which the
43+ editorial revisions, annotations, elaborations, or other modifications
44+ represent, as a whole, an original work of authorship. For the purposes
45+ of this License, Derivative Works shall not include works that remain
46+ separable from, or merely link (or bind by name) to the interfaces of,
47+ the Work and Derivative Works thereof.
48+
49+ "Contribution" shall mean any work of authorship, including
50+ the original version of the Work and any modifications or additions
51+ to that Work or Derivative Works thereof, that is intentionally
52+ submitted to Licensor for inclusion in the Work by the copyright owner
53+ or by an individual or Legal Entity authorized to submit on behalf of
54+ the copyright owner. For the purposes of this definition, "submitted"
55+ means any form of electronic, verbal, or written communication sent
56+ to the Licensor or its representatives, including but not limited to
57+ communication on electronic mailing lists, source code control systems,
58+ and issue tracking systems that are managed by, or on behalf of, the
59+ Licensor for the purpose of discussing and improving the Work, but
60+ excluding communication that is conspicuously marked or otherwise
61+ designated in writing by the copyright owner as "Not a Contribution."
62+
63+ "Contributor" shall mean Licensor and any individual or Legal Entity
64+ on behalf of whom a Contribution has been received by Licensor and
65+ subsequently incorporated within the Work.
66+
67+ 2. Grant of Copyright License. Subject to the terms and conditions of
68+ this License, each Contributor hereby grants to You a perpetual,
69+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70+ copyright license to reproduce, prepare Derivative Works of,
71+ publicly display, publicly perform, sublicense, and distribute the
72+ Work and such Derivative Works in Source or Object form.
73+
74+ 3. Grant of Patent License. Subject to the terms and conditions of
75+ this License, each Contributor hereby grants to You a perpetual,
76+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77+ (except as stated in this section) patent license to make, have made,
78+ use, offer to sell, sell, import, and otherwise transfer the Work,
79+ where such license applies only to those patent claims licensable
80+ by such Contributor that are necessarily infringed by their
81+ Contribution(s) alone or by combination of their Contribution(s)
82+ with the Work to which such Contribution(s) was submitted. If You
83+ institute patent litigation against any entity (including a
84+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85+ or a Contribution incorporated within the Work constitutes direct
86+ or contributory patent infringement, then any patent licenses
87+ granted to You under this License for that Work shall terminate
88+ as of the date such litigation is filed.
89+
90+ 4. Redistribution. You may reproduce and distribute copies of the
91+ Work or Derivative Works thereof in any medium, with or without
92+ modifications, and in Source or Object form, provided that You
93+ meet the following conditions:
94+
95+ (a) You must give any other recipients of the Work or
96+ Derivative Works a copy of this License; and
97+
98+ (b) You must cause any modified files to carry prominent notices
99+ stating that You changed the files; and
100+
101+ (c) You must retain, in the Source form of any Derivative Works
102+ that You distribute, all copyright, patent, trademark, and
103+ attribution notices from the Source form of the Work,
104+ excluding those notices that do not pertain to any part of
105+ the Derivative Works; and
106+
107+ (d) If the Work includes a "NOTICE" text file as part of its
108+ distribution, then any Derivative Works that You distribute must
109+ include a readable copy of the attribution notices contained
110+ within such NOTICE file, excluding those notices that do not
111+ pertain to any part of the Derivative Works, in at least one
112+ of the following places: within a NOTICE text file distributed
113+ as part of the Derivative Works; within the Source form or
114+ documentation, if provided along with the Derivative Works; or,
115+ within a display generated by the Derivative Works, if and
116+ wherever such third-party notices normally appear. The contents
117+ of the NOTICE file are for informational purposes only and
118+ do not modify the License. You may add Your own attribution
119+ notices within Derivative Works that You distribute, alongside
120+ or as an addendum to the NOTICE text from the Work, provided
121+ that such additional attribution notices cannot be construed
122+ as modifying the License.
123+
124+ You may add Your own copyright statement to Your modifications and
125+ may provide additional or different license terms and conditions
126+ for use, reproduction, or distribution of Your modifications, or
127+ for any such Derivative Works as a whole, provided Your use,
128+ reproduction, and distribution of the Work otherwise complies with
129+ the conditions stated in this License.
130+
131+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132+ any Contribution intentionally submitted for inclusion in the Work
133+ by You to the Licensor shall be under the terms and conditions of
134+ this License, without any additional terms or conditions.
135+ Notwithstanding the above, nothing herein shall supersede or modify
136+ the terms of any separate license agreement you may have executed
137+ with Licensor regarding such Contributions.
138+
139+ 6. Trademarks. This License does not grant permission to use the trade
140+ names, trademarks, service marks, or product names of the Licensor,
141+ except as required for reasonable and customary use in describing the
142+ origin of the Work and reproducing the content of the NOTICE file.
143+
144+ 7. Disclaimer of Warranty. Unless required by applicable law or
145+ agreed to in writing, Licensor provides the Work (and each
146+ Contributor provides its Contributions) on an "AS IS" BASIS,
147+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148+ implied, including, without limitation, any warranties or conditions
149+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150+ PARTICULAR PURPOSE. You are solely responsible for determining the
151+ appropriateness of using or redistributing the Work and assume any
152+ risks associated with Your exercise of permissions under this License.
153+
154+ 8. Limitation of Liability. In no event and under no legal theory,
155+ whether in tort (including negligence), contract, or otherwise,
156+ unless required by applicable law (such as deliberate and grossly
157+ negligent acts) or agreed to in writing, shall any Contributor be
158+ liable to You for damages, including any direct, indirect, special,
159+ incidental, or consequential damages of any character arising as a
160+ result of this License or out of the use or inability to use the
161+ Work (including but not limited to damages for loss of goodwill,
162+ work stoppage, computer failure or malfunction, or any and all
163+ other commercial damages or losses), even if such Contributor
164+ has been advised of the possibility of such damages.
165+
166+ 9. Accepting Warranty or Additional Liability. While redistributing
167+ the Work or Derivative Works thereof, You may choose to offer,
168+ and charge a fee for, acceptance of support, warranty, indemnity,
169+ or other liability obligations and/or rights consistent with this
170+ License. However, in accepting such obligations, You may act only
171+ on Your own behalf and on Your sole responsibility, not on behalf
172+ of any other Contributor, and only if You agree to indemnify,
173+ defend, and hold each Contributor harmless for any liability
174+ incurred by, or claims asserted against, such Contributor by reason
175+ of your accepting any such warranty or additional liability.
176+
177+ END OF TERMS AND CONDITIONS
178+
179+ APPENDIX: How to apply the Apache License to your work.
180+
181+ To apply the Apache License to your work, attach the following
182+ boilerplate notice, with the fields enclosed by brackets "[]"
183+ replaced with your own identifying information. (Don't include
184+ the brackets!) The text should be enclosed in the appropriate
185+ comment syntax for the file format. We also recommend that a
186+ file or class name and description of purpose be included on the
187+ same "printed page" as the copyright notice for easier
188+ identification within third-party archives.
189+
190+ Copyright [yyyy] [name of copyright owner]
191+
192+ Licensed under the Apache License, Version 2.0 (the "License");
193+ you may not use this file except in compliance with the License.
194+ You may obtain a copy of the License at
195+
196+ http://www.apache.org/licenses/LICENSE-2.0
197+
198+ Unless required by applicable law or agreed to in writing, software
199+ distributed under the License is distributed on an "AS IS" BASIS,
200+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201+ See the License for the specific language governing permissions and
202+ limitations under the License.
vendor/cloud.google.com/go/auth/README.mdnew file+40
@@ -0,0 +1,40 @@
1+# Google Auth Library for Go
2+
3+[](https://pkg.go.dev/cloud.google.com/go/auth)
4+
5+## Install
6+
7+``` bash
8+go get cloud.google.com/go/auth@latest
9+```
10+
11+## Usage
12+
13+The most common way this library is used is transitively, by default, from any
14+of our Go client libraries.
15+
16+### Notable use-cases
17+
18+- To create a credential directly please see examples in the
19+ [credentials](https://pkg.go.dev/cloud.google.com/go/auth/credentials)
20+ package.
21+- To create a authenticated HTTP client please see examples in the
22+ [httptransport](https://pkg.go.dev/cloud.google.com/go/auth/httptransport)
23+ package.
24+- To create a authenticated gRPC connection please see examples in the
25+ [grpctransport](https://pkg.go.dev/cloud.google.com/go/auth/grpctransport)
26+ package.
27+- To create an ID token please see examples in the
28+ [idtoken](https://pkg.go.dev/cloud.google.com/go/auth/credentials/idtoken)
29+ package.
30+
31+## Contributing
32+
33+Contributions are welcome. Please, see the
34+[CONTRIBUTING](https://github.com/GoogleCloudPlatform/google-cloud-go/blob/main/CONTRIBUTING.md)
35+document for details.
36+
37+Please note that this project is released with a Contributor Code of Conduct.
38+By participating in this project you agree to abide by its terms.
39+See [Contributor Code of Conduct](https://github.com/GoogleCloudPlatform/google-cloud-go/blob/main/CONTRIBUTING.md#contributor-code-of-conduct)
40+for more information.
vendor/cloud.google.com/go/auth/auth.gonew file+618
@@ -0,0 +1,618 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+// Package auth provides utilities for managing Google Cloud credentials,
16+// including functionality for creating, caching, and refreshing OAuth2 tokens.
17+// It offers customizable options for different OAuth2 flows, such as 2-legged
18+// (2LO) and 3-legged (3LO) OAuth, along with support for PKCE and automatic
19+// token management.
20+package auth
21+
22+import (
23+ "context"
24+ "encoding/json"
25+ "errors"
26+ "fmt"
27+ "log/slog"
28+ "net/http"
29+ "net/url"
30+ "strings"
31+ "sync"
32+ "time"
33+
34+ "cloud.google.com/go/auth/internal"
35+ "cloud.google.com/go/auth/internal/jwt"
36+ "github.com/googleapis/gax-go/v2/internallog"
37+)
38+
39+const (
40+ // Parameter keys for AuthCodeURL method to support PKCE.
41+ codeChallengeKey = "code_challenge"
42+ codeChallengeMethodKey = "code_challenge_method"
43+
44+ // Parameter key for Exchange method to support PKCE.
45+ codeVerifierKey = "code_verifier"
46+
47+ // 3 minutes and 45 seconds before expiration. The shortest MDS cache is 4 minutes,
48+ // so we give it 15 seconds to refresh it's cache before attempting to refresh a token.
49+ defaultExpiryDelta = 225 * time.Second
50+
51+ universeDomainDefault = "googleapis.com"
52+)
53+
54+// tokenState represents different states for a [Token].
55+type tokenState int
56+
57+const (
58+ // fresh indicates that the [Token] is valid. It is not expired or close to
59+ // expired, or the token has no expiry.
60+ fresh tokenState = iota
61+ // stale indicates that the [Token] is close to expired, and should be
62+ // refreshed. The token can be used normally.
63+ stale
64+ // invalid indicates that the [Token] is expired or invalid. The token
65+ // cannot be used for a normal operation.
66+ invalid
67+)
68+
69+var (
70+ defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
71+ defaultHeader = &jwt.Header{Algorithm: jwt.HeaderAlgRSA256, Type: jwt.HeaderType}
72+
73+ // for testing
74+ timeNow = time.Now
75+)
76+
77+// TokenProvider specifies an interface for anything that can return a token.
78+type TokenProvider interface {
79+ // Token returns a Token or an error.
80+ // The Token returned must be safe to use
81+ // concurrently.
82+ // The returned Token must not be modified.
83+ // The context provided must be sent along to any requests that are made in
84+ // the implementing code.
85+ Token(context.Context) (*Token, error)
86+}
87+
88+// Token holds the credential token used to authorized requests. All fields are
89+// considered read-only.
90+type Token struct {
91+ // Value is the token used to authorize requests. It is usually an access
92+ // token but may be other types of tokens such as ID tokens in some flows.
93+ Value string
94+ // Type is the type of token Value is. If uninitialized, it should be
95+ // assumed to be a "Bearer" token.
96+ Type string
97+ // Expiry is the time the token is set to expire.
98+ Expiry time.Time
99+ // Metadata may include, but is not limited to, the body of the token
100+ // response returned by the server.
101+ Metadata map[string]interface{} // TODO(codyoss): maybe make a method to flatten metadata to avoid []string for url.Values
102+}
103+
104+// IsValid reports that a [Token] is non-nil, has a [Token.Value], and has not
105+// expired. A token is considered expired if [Token.Expiry] has passed or will
106+// pass in the next 225 seconds.
107+func (t *Token) IsValid() bool {
108+ return t.isValidWithEarlyExpiry(defaultExpiryDelta)
109+}
110+
111+// MetadataString is a convenience method for accessing string values in the
112+// token's metadata. Returns an empty string if the metadata is nil or the value
113+// for the given key cannot be cast to a string.
114+func (t *Token) MetadataString(k string) string {
115+ if t.Metadata == nil {
116+ return ""
117+ }
118+ s, ok := t.Metadata[k].(string)
119+ if !ok {
120+ return ""
121+ }
122+ return s
123+}
124+
125+func (t *Token) isValidWithEarlyExpiry(earlyExpiry time.Duration) bool {
126+ if t.isEmpty() {
127+ return false
128+ }
129+ if t.Expiry.IsZero() {
130+ return true
131+ }
132+ return !t.Expiry.Round(0).Add(-earlyExpiry).Before(timeNow())
133+}
134+
135+func (t *Token) isEmpty() bool {
136+ return t == nil || t.Value == ""
137+}
138+
139+// Credentials holds Google credentials, including
140+// [Application Default Credentials].
141+//
142+// [Application Default Credentials]: https://developers.google.com/accounts/docs/application-default-credentials
143+type Credentials struct {
144+ json []byte
145+ projectID CredentialsPropertyProvider
146+ quotaProjectID CredentialsPropertyProvider
147+ // universeDomain is the default service domain for a given Cloud universe.
148+ universeDomain CredentialsPropertyProvider
149+
150+ TokenProvider
151+}
152+
153+// JSON returns the bytes associated with the the file used to source
154+// credentials if one was used.
155+func (c *Credentials) JSON() []byte {
156+ return c.json
157+}
158+
159+// ProjectID returns the associated project ID from the underlying file or
160+// environment.
161+func (c *Credentials) ProjectID(ctx context.Context) (string, error) {
162+ if c.projectID == nil {
163+ return internal.GetProjectID(c.json, ""), nil
164+ }
165+ v, err := c.projectID.GetProperty(ctx)
166+ if err != nil {
167+ return "", err
168+ }
169+ return internal.GetProjectID(c.json, v), nil
170+}
171+
172+// QuotaProjectID returns the associated quota project ID from the underlying
173+// file or environment.
174+func (c *Credentials) QuotaProjectID(ctx context.Context) (string, error) {
175+ if c.quotaProjectID == nil {
176+ return internal.GetQuotaProject(c.json, ""), nil
177+ }
178+ v, err := c.quotaProjectID.GetProperty(ctx)
179+ if err != nil {
180+ return "", err
181+ }
182+ return internal.GetQuotaProject(c.json, v), nil
183+}
184+
185+// UniverseDomain returns the default service domain for a given Cloud universe.
186+// The default value is "googleapis.com".
187+func (c *Credentials) UniverseDomain(ctx context.Context) (string, error) {
188+ if c.universeDomain == nil {
189+ return universeDomainDefault, nil
190+ }
191+ v, err := c.universeDomain.GetProperty(ctx)
192+ if err != nil {
193+ return "", err
194+ }
195+ if v == "" {
196+ return universeDomainDefault, nil
197+ }
198+ return v, err
199+}
200+
201+// CredentialsPropertyProvider provides an implementation to fetch a property
202+// value for [Credentials].
203+type CredentialsPropertyProvider interface {
204+ GetProperty(context.Context) (string, error)
205+}
206+
207+// CredentialsPropertyFunc is a type adapter to allow the use of ordinary
208+// functions as a [CredentialsPropertyProvider].
209+type CredentialsPropertyFunc func(context.Context) (string, error)
210+
211+// GetProperty loads the properly value provided the given context.
212+func (p CredentialsPropertyFunc) GetProperty(ctx context.Context) (string, error) {
213+ return p(ctx)
214+}
215+
216+// CredentialsOptions are used to configure [Credentials].
217+type CredentialsOptions struct {
218+ // TokenProvider is a means of sourcing a token for the credentials. Required.
219+ TokenProvider TokenProvider
220+ // JSON is the raw contents of the credentials file if sourced from a file.
221+ JSON []byte
222+ // ProjectIDProvider resolves the project ID associated with the
223+ // credentials.
224+ ProjectIDProvider CredentialsPropertyProvider
225+ // QuotaProjectIDProvider resolves the quota project ID associated with the
226+ // credentials.
227+ QuotaProjectIDProvider CredentialsPropertyProvider
228+ // UniverseDomainProvider resolves the universe domain with the credentials.
229+ UniverseDomainProvider CredentialsPropertyProvider
230+}
231+
232+// NewCredentials returns new [Credentials] from the provided options.
233+func NewCredentials(opts *CredentialsOptions) *Credentials {
234+ creds := &Credentials{
235+ TokenProvider: opts.TokenProvider,
236+ json: opts.JSON,
237+ projectID: opts.ProjectIDProvider,
238+ quotaProjectID: opts.QuotaProjectIDProvider,
239+ universeDomain: opts.UniverseDomainProvider,
240+ }
241+
242+ return creds
243+}
244+
245+// CachedTokenProviderOptions provides options for configuring a cached
246+// [TokenProvider].
247+type CachedTokenProviderOptions struct {
248+ // DisableAutoRefresh makes the TokenProvider always return the same token,
249+ // even if it is expired. The default is false. Optional.
250+ DisableAutoRefresh bool
251+ // ExpireEarly configures the amount of time before a token expires, that it
252+ // should be refreshed. If unset, the default value is 3 minutes and 45
253+ // seconds. Optional.
254+ ExpireEarly time.Duration
255+ // DisableAsyncRefresh configures a synchronous workflow that refreshes
256+ // tokens in a blocking manner. The default is false. Optional.
257+ DisableAsyncRefresh bool
258+}
259+
260+func (ctpo *CachedTokenProviderOptions) autoRefresh() bool {
261+ if ctpo == nil {
262+ return true
263+ }
264+ return !ctpo.DisableAutoRefresh
265+}
266+
267+func (ctpo *CachedTokenProviderOptions) expireEarly() time.Duration {
268+ if ctpo == nil || ctpo.ExpireEarly == 0 {
269+ return defaultExpiryDelta
270+ }
271+ return ctpo.ExpireEarly
272+}
273+
274+func (ctpo *CachedTokenProviderOptions) blockingRefresh() bool {
275+ if ctpo == nil {
276+ return false
277+ }
278+ return ctpo.DisableAsyncRefresh
279+}
280+
281+// NewCachedTokenProvider wraps a [TokenProvider] to cache the tokens returned
282+// by the underlying provider. By default it will refresh tokens asynchronously
283+// a few minutes before they expire.
284+func NewCachedTokenProvider(tp TokenProvider, opts *CachedTokenProviderOptions) TokenProvider {
285+ if ctp, ok := tp.(*cachedTokenProvider); ok {
286+ return ctp
287+ }
288+ return &cachedTokenProvider{
289+ tp: tp,
290+ autoRefresh: opts.autoRefresh(),
291+ expireEarly: opts.expireEarly(),
292+ blockingRefresh: opts.blockingRefresh(),
293+ }
294+}
295+
296+type cachedTokenProvider struct {
297+ tp TokenProvider
298+ autoRefresh bool
299+ expireEarly time.Duration
300+ blockingRefresh bool
301+
302+ mu sync.Mutex
303+ cachedToken *Token
304+ // isRefreshRunning ensures that the non-blocking refresh will only be
305+ // attempted once, even if multiple callers enter the Token method.
306+ isRefreshRunning bool
307+ // isRefreshErr ensures that the non-blocking refresh will only be attempted
308+ // once per refresh window if an error is encountered.
309+ isRefreshErr bool
310+}
311+
312+func (c *cachedTokenProvider) Token(ctx context.Context) (*Token, error) {
313+ if c.blockingRefresh {
314+ return c.tokenBlocking(ctx)
315+ }
316+ return c.tokenNonBlocking(ctx)
317+}
318+
319+func (c *cachedTokenProvider) tokenNonBlocking(ctx context.Context) (*Token, error) {
320+ switch c.tokenState() {
321+ case fresh:
322+ c.mu.Lock()
323+ defer c.mu.Unlock()
324+ return c.cachedToken, nil
325+ case stale:
326+ // Call tokenAsync with a new Context because the user-provided context
327+ // may have a short timeout incompatible with async token refresh.
328+ c.tokenAsync(context.Background())
329+ // Return the stale token immediately to not block customer requests to Cloud services.
330+ c.mu.Lock()
331+ defer c.mu.Unlock()
332+ return c.cachedToken, nil
333+ default: // invalid
334+ return c.tokenBlocking(ctx)
335+ }
336+}
337+
338+// tokenState reports the token's validity.
339+func (c *cachedTokenProvider) tokenState() tokenState {
340+ c.mu.Lock()
341+ defer c.mu.Unlock()
342+ t := c.cachedToken
343+ now := timeNow()
344+ if t == nil || t.Value == "" {
345+ return invalid
346+ } else if t.Expiry.IsZero() {
347+ return fresh
348+ } else if now.After(t.Expiry.Round(0)) {
349+ return invalid
350+ } else if now.After(t.Expiry.Round(0).Add(-c.expireEarly)) {
351+ return stale
352+ }
353+ return fresh
354+}
355+
356+// tokenAsync uses a bool to ensure that only one non-blocking token refresh
357+// happens at a time, even if multiple callers have entered this function
358+// concurrently. This avoids creating an arbitrary number of concurrent
359+// goroutines. Retries should be attempted and managed within the Token method.
360+// If the refresh attempt fails, no further attempts are made until the refresh
361+// window expires and the token enters the invalid state, at which point the
362+// blocking call to Token should likely return the same error on the main goroutine.
363+func (c *cachedTokenProvider) tokenAsync(ctx context.Context) {
364+ fn := func() {
365+ c.mu.Lock()
366+ c.isRefreshRunning = true
367+ c.mu.Unlock()
368+ t, err := c.tp.Token(ctx)
369+ c.mu.Lock()
370+ defer c.mu.Unlock()
371+ c.isRefreshRunning = false
372+ if err != nil {
373+ // Discard errors from the non-blocking refresh, but prevent further
374+ // attempts.
375+ c.isRefreshErr = true
376+ return
377+ }
378+ c.cachedToken = t
379+ }
380+ c.mu.Lock()
381+ defer c.mu.Unlock()
382+ if !c.isRefreshRunning && !c.isRefreshErr {
383+ go fn()
384+ }
385+}
386+
387+func (c *cachedTokenProvider) tokenBlocking(ctx context.Context) (*Token, error) {
388+ c.mu.Lock()
389+ defer c.mu.Unlock()
390+ c.isRefreshErr = false
391+ if c.cachedToken.IsValid() || (!c.autoRefresh && !c.cachedToken.isEmpty()) {
392+ return c.cachedToken, nil
393+ }
394+ t, err := c.tp.Token(ctx)
395+ if err != nil {
396+ return nil, err
397+ }
398+ c.cachedToken = t
399+ return t, nil
400+}
401+
402+// Error is a error associated with retrieving a [Token]. It can hold useful
403+// additional details for debugging.
404+type Error struct {
405+ // Response is the HTTP response associated with error. The body will always
406+ // be already closed and consumed.
407+ Response *http.Response
408+ // Body is the HTTP response body.
409+ Body []byte
410+ // Err is the underlying wrapped error.
411+ Err error
412+
413+ // code returned in the token response
414+ code string
415+ // description returned in the token response
416+ description string
417+ // uri returned in the token response
418+ uri string
419+}
420+
421+func (e *Error) Error() string {
422+ if e.code != "" {
423+ s := fmt.Sprintf("auth: %q", e.code)
424+ if e.description != "" {
425+ s += fmt.Sprintf(" %q", e.description)
426+ }
427+ if e.uri != "" {
428+ s += fmt.Sprintf(" %q", e.uri)
429+ }
430+ return s
431+ }
432+ return fmt.Sprintf("auth: cannot fetch token: %v\nResponse: %s", e.Response.StatusCode, e.Body)
433+}
434+
435+// Temporary returns true if the error is considered temporary and may be able
436+// to be retried.
437+func (e *Error) Temporary() bool {
438+ if e.Response == nil {
439+ return false
440+ }
441+ sc := e.Response.StatusCode
442+ return sc == http.StatusInternalServerError || sc == http.StatusServiceUnavailable || sc == http.StatusRequestTimeout || sc == http.StatusTooManyRequests
443+}
444+
445+func (e *Error) Unwrap() error {
446+ return e.Err
447+}
448+
449+// Style describes how the token endpoint wants to receive the ClientID and
450+// ClientSecret.
451+type Style int
452+
453+const (
454+ // StyleUnknown means the value has not been initiated. Sending this in
455+ // a request will cause the token exchange to fail.
456+ StyleUnknown Style = iota
457+ // StyleInParams sends client info in the body of a POST request.
458+ StyleInParams
459+ // StyleInHeader sends client info using Basic Authorization header.
460+ StyleInHeader
461+)
462+
463+// Options2LO is the configuration settings for doing a 2-legged JWT OAuth2 flow.
464+type Options2LO struct {
465+ // Email is the OAuth2 client ID. This value is set as the "iss" in the
466+ // JWT.
467+ Email string
468+ // PrivateKey contains the contents of an RSA private key or the
469+ // contents of a PEM file that contains a private key. It is used to sign
470+ // the JWT created.
471+ PrivateKey []byte
472+ // TokenURL is th URL the JWT is sent to. Required.
473+ TokenURL string
474+ // PrivateKeyID is the ID of the key used to sign the JWT. It is used as the
475+ // "kid" in the JWT header. Optional.
476+ PrivateKeyID string
477+ // Subject is the used for to impersonate a user. It is used as the "sub" in
478+ // the JWT.m Optional.
479+ Subject string
480+ // Scopes specifies requested permissions for the token. Optional.
481+ Scopes []string
482+ // Expires specifies the lifetime of the token. Optional.
483+ Expires time.Duration
484+ // Audience specifies the "aud" in the JWT. Optional.
485+ Audience string
486+ // PrivateClaims allows specifying any custom claims for the JWT. Optional.
487+ PrivateClaims map[string]interface{}
488+
489+ // Client is the client to be used to make the underlying token requests.
490+ // Optional.
491+ Client *http.Client
492+ // UseIDToken requests that the token returned be an ID token if one is
493+ // returned from the server. Optional.
494+ UseIDToken bool
495+ // Logger is used for debug logging. If provided, logging will be enabled
496+ // at the loggers configured level. By default logging is disabled unless
497+ // enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
498+ // logger will be used. Optional.
499+ Logger *slog.Logger
500+}
501+
502+func (o *Options2LO) client() *http.Client {
503+ if o.Client != nil {
504+ return o.Client
505+ }
506+ return internal.DefaultClient()
507+}
508+
509+func (o *Options2LO) validate() error {
510+ if o == nil {
511+ return errors.New("auth: options must be provided")
512+ }
513+ if o.Email == "" {
514+ return errors.New("auth: email must be provided")
515+ }
516+ if len(o.PrivateKey) == 0 {
517+ return errors.New("auth: private key must be provided")
518+ }
519+ if o.TokenURL == "" {
520+ return errors.New("auth: token URL must be provided")
521+ }
522+ return nil
523+}
524+
525+// New2LOTokenProvider returns a [TokenProvider] from the provided options.
526+func New2LOTokenProvider(opts *Options2LO) (TokenProvider, error) {
527+ if err := opts.validate(); err != nil {
528+ return nil, err
529+ }
530+ return tokenProvider2LO{opts: opts, Client: opts.client(), logger: internallog.New(opts.Logger)}, nil
531+}
532+
533+type tokenProvider2LO struct {
534+ opts *Options2LO
535+ Client *http.Client
536+ logger *slog.Logger
537+}
538+
539+func (tp tokenProvider2LO) Token(ctx context.Context) (*Token, error) {
540+ pk, err := internal.ParseKey(tp.opts.PrivateKey)
541+ if err != nil {
542+ return nil, err
543+ }
544+ claimSet := &jwt.Claims{
545+ Iss: tp.opts.Email,
546+ Scope: strings.Join(tp.opts.Scopes, " "),
547+ Aud: tp.opts.TokenURL,
548+ AdditionalClaims: tp.opts.PrivateClaims,
549+ Sub: tp.opts.Subject,
550+ }
551+ if t := tp.opts.Expires; t > 0 {
552+ claimSet.Exp = time.Now().Add(t).Unix()
553+ }
554+ if aud := tp.opts.Audience; aud != "" {
555+ claimSet.Aud = aud
556+ }
557+ h := *defaultHeader
558+ h.KeyID = tp.opts.PrivateKeyID
559+ payload, err := jwt.EncodeJWS(&h, claimSet, pk)
560+ if err != nil {
561+ return nil, err
562+ }
563+ v := url.Values{}
564+ v.Set("grant_type", defaultGrantType)
565+ v.Set("assertion", payload)
566+ req, err := http.NewRequestWithContext(ctx, "POST", tp.opts.TokenURL, strings.NewReader(v.Encode()))
567+ if err != nil {
568+ return nil, err
569+ }
570+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
571+ tp.logger.DebugContext(ctx, "2LO token request", "request", internallog.HTTPRequest(req, []byte(v.Encode())))
572+ resp, body, err := internal.DoRequest(tp.Client, req)
573+ if err != nil {
574+ return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
575+ }
576+ tp.logger.DebugContext(ctx, "2LO token response", "response", internallog.HTTPResponse(resp, body))
577+ if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
578+ return nil, &Error{
579+ Response: resp,
580+ Body: body,
581+ }
582+ }
583+ // tokenRes is the JSON response body.
584+ var tokenRes struct {
585+ AccessToken string `json:"access_token"`
586+ TokenType string `json:"token_type"`
587+ IDToken string `json:"id_token"`
588+ ExpiresIn int64 `json:"expires_in"`
589+ }
590+ if err := json.Unmarshal(body, &tokenRes); err != nil {
591+ return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
592+ }
593+ token := &Token{
594+ Value: tokenRes.AccessToken,
595+ Type: tokenRes.TokenType,
596+ }
597+ token.Metadata = make(map[string]interface{})
598+ json.Unmarshal(body, &token.Metadata) // no error checks for optional fields
599+
600+ if secs := tokenRes.ExpiresIn; secs > 0 {
601+ token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
602+ }
603+ if v := tokenRes.IDToken; v != "" {
604+ // decode returned id token to get expiry
605+ claimSet, err := jwt.DecodeJWS(v)
606+ if err != nil {
607+ return nil, fmt.Errorf("auth: error decoding JWT token: %w", err)
608+ }
609+ token.Expiry = time.Unix(claimSet.Exp, 0)
610+ }
611+ if tp.opts.UseIDToken {
612+ if tokenRes.IDToken == "" {
613+ return nil, fmt.Errorf("auth: response doesn't have JWT token")
614+ }
615+ token.Value = tokenRes.IDToken
616+ }
617+ return token, nil
618+}
vendor/cloud.google.com/go/auth/credentials/compute.gonew file+90
@@ -0,0 +1,90 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package credentials
16+
17+import (
18+ "context"
19+ "encoding/json"
20+ "errors"
21+ "fmt"
22+ "net/url"
23+ "strings"
24+ "time"
25+
26+ "cloud.google.com/go/auth"
27+ "cloud.google.com/go/compute/metadata"
28+)
29+
30+var (
31+ computeTokenMetadata = map[string]interface{}{
32+ "auth.google.tokenSource": "compute-metadata",
33+ "auth.google.serviceAccount": "default",
34+ }
35+ computeTokenURI = "instance/service-accounts/default/token"
36+)
37+
38+// computeTokenProvider creates a [cloud.google.com/go/auth.TokenProvider] that
39+// uses the metadata service to retrieve tokens.
40+func computeTokenProvider(opts *DetectOptions, client *metadata.Client) auth.TokenProvider {
41+ return auth.NewCachedTokenProvider(&computeProvider{
42+ scopes: opts.Scopes,
43+ client: client,
44+ }, &auth.CachedTokenProviderOptions{
45+ ExpireEarly: opts.EarlyTokenRefresh,
46+ DisableAsyncRefresh: opts.DisableAsyncRefresh,
47+ })
48+}
49+
50+// computeProvider fetches tokens from the google cloud metadata service.
51+type computeProvider struct {
52+ scopes []string
53+ client *metadata.Client
54+}
55+
56+type metadataTokenResp struct {
57+ AccessToken string `json:"access_token"`
58+ ExpiresInSec int `json:"expires_in"`
59+ TokenType string `json:"token_type"`
60+}
61+
62+func (cs *computeProvider) Token(ctx context.Context) (*auth.Token, error) {
63+ tokenURI, err := url.Parse(computeTokenURI)
64+ if err != nil {
65+ return nil, err
66+ }
67+ if len(cs.scopes) > 0 {
68+ v := url.Values{}
69+ v.Set("scopes", strings.Join(cs.scopes, ","))
70+ tokenURI.RawQuery = v.Encode()
71+ }
72+ tokenJSON, err := cs.client.GetWithContext(ctx, tokenURI.String())
73+ if err != nil {
74+ return nil, fmt.Errorf("credentials: cannot fetch token: %w", err)
75+ }
76+ var res metadataTokenResp
77+ if err := json.NewDecoder(strings.NewReader(tokenJSON)).Decode(&res); err != nil {
78+ return nil, fmt.Errorf("credentials: invalid token JSON from metadata: %w", err)
79+ }
80+ if res.ExpiresInSec == 0 || res.AccessToken == "" {
81+ return nil, errors.New("credentials: incomplete token received from metadata")
82+ }
83+ return &auth.Token{
84+ Value: res.AccessToken,
85+ Type: res.TokenType,
86+ Expiry: time.Now().Add(time.Duration(res.ExpiresInSec) * time.Second),
87+ Metadata: computeTokenMetadata,
88+ }, nil
89+
90+}
vendor/cloud.google.com/go/auth/credentials/detect.gonew file+279
@@ -0,0 +1,279 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package credentials
16+
17+import (
18+ "context"
19+ "encoding/json"
20+ "errors"
21+ "fmt"
22+ "log/slog"
23+ "net/http"
24+ "os"
25+ "time"
26+
27+ "cloud.google.com/go/auth"
28+ "cloud.google.com/go/auth/internal"
29+ "cloud.google.com/go/auth/internal/credsfile"
30+ "cloud.google.com/go/compute/metadata"
31+ "github.com/googleapis/gax-go/v2/internallog"
32+)
33+
34+const (
35+ // jwtTokenURL is Google's OAuth 2.0 token URL to use with the JWT(2LO) flow.
36+ jwtTokenURL = "https://oauth2.googleapis.com/token"
37+
38+ // Google's OAuth 2.0 default endpoints.
39+ googleAuthURL = "https://accounts.google.com/o/oauth2/auth"
40+ googleTokenURL = "https://oauth2.googleapis.com/token"
41+
42+ // GoogleMTLSTokenURL is Google's default OAuth2.0 mTLS endpoint.
43+ GoogleMTLSTokenURL = "https://oauth2.mtls.googleapis.com/token"
44+
45+ // Help on default credentials
46+ adcSetupURL = "https://cloud.google.com/docs/authentication/external/set-up-adc"
47+)
48+
49+var (
50+ // for testing
51+ allowOnGCECheck = true
52+)
53+
54+// OnGCE reports whether this process is running in Google Cloud.
55+func OnGCE() bool {
56+ // TODO(codyoss): once all libs use this auth lib move metadata check here
57+ return allowOnGCECheck && metadata.OnGCE()
58+}
59+
60+// DetectDefault searches for "Application Default Credentials" and returns
61+// a credential based on the [DetectOptions] provided.
62+//
63+// It looks for credentials in the following places, preferring the first
64+// location found:
65+//
66+// - A JSON file whose path is specified by the GOOGLE_APPLICATION_CREDENTIALS
67+// environment variable. For workload identity federation, refer to
68+// https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation
69+// on how to generate the JSON configuration file for on-prem/non-Google
70+// cloud platforms.
71+// - A JSON file in a location known to the gcloud command-line tool. On
72+// Windows, this is %APPDATA%/gcloud/application_default_credentials.json. On
73+// other systems, $HOME/.config/gcloud/application_default_credentials.json.
74+// - On Google Compute Engine, Google App Engine standard second generation
75+// runtimes, and Google App Engine flexible environment, it fetches
76+// credentials from the metadata server.
77+func DetectDefault(opts *DetectOptions) (*auth.Credentials, error) {
78+ if err := opts.validate(); err != nil {
79+ return nil, err
80+ }
81+ if len(opts.CredentialsJSON) > 0 {
82+ return readCredentialsFileJSON(opts.CredentialsJSON, opts)
83+ }
84+ if opts.CredentialsFile != "" {
85+ return readCredentialsFile(opts.CredentialsFile, opts)
86+ }
87+ if filename := os.Getenv(credsfile.GoogleAppCredsEnvVar); filename != "" {
88+ creds, err := readCredentialsFile(filename, opts)
89+ if err != nil {
90+ return nil, err
91+ }
92+ return creds, nil
93+ }
94+
95+ fileName := credsfile.GetWellKnownFileName()
96+ if b, err := os.ReadFile(fileName); err == nil {
97+ return readCredentialsFileJSON(b, opts)
98+ }
99+
100+ if OnGCE() {
101+ metadataClient := metadata.NewWithOptions(&metadata.Options{
102+ Logger: opts.logger(),
103+ })
104+ return auth.NewCredentials(&auth.CredentialsOptions{
105+ TokenProvider: computeTokenProvider(opts, metadataClient),
106+ ProjectIDProvider: auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) {
107+ return metadataClient.ProjectIDWithContext(ctx)
108+ }),
109+ UniverseDomainProvider: &internal.ComputeUniverseDomainProvider{
110+ MetadataClient: metadataClient,
111+ },
112+ }), nil
113+ }
114+
115+ return nil, fmt.Errorf("credentials: could not find default credentials. See %v for more information", adcSetupURL)
116+}
117+
118+// DetectOptions provides configuration for [DetectDefault].
119+type DetectOptions struct {
120+ // Scopes that credentials tokens should have. Example:
121+ // https://www.googleapis.com/auth/cloud-platform. Required if Audience is
122+ // not provided.
123+ Scopes []string
124+ // Audience that credentials tokens should have. Only applicable for 2LO
125+ // flows with service accounts. If specified, scopes should not be provided.
126+ Audience string
127+ // Subject is the user email used for [domain wide delegation](https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority).
128+ // Optional.
129+ Subject string
130+ // EarlyTokenRefresh configures how early before a token expires that it
131+ // should be refreshed. Once the token’s time until expiration has entered
132+ // this refresh window the token is considered valid but stale. If unset,
133+ // the default value is 3 minutes and 45 seconds. Optional.
134+ EarlyTokenRefresh time.Duration
135+ // DisableAsyncRefresh configures a synchronous workflow that refreshes
136+ // stale tokens while blocking. The default is false. Optional.
137+ DisableAsyncRefresh bool
138+ // AuthHandlerOptions configures an authorization handler and other options
139+ // for 3LO flows. It is required, and only used, for client credential
140+ // flows.
141+ AuthHandlerOptions *auth.AuthorizationHandlerOptions
142+ // TokenURL allows to set the token endpoint for user credential flows. If
143+ // unset the default value is: https://oauth2.googleapis.com/token.
144+ // Optional.
145+ TokenURL string
146+ // STSAudience is the audience sent to when retrieving an STS token.
147+ // Currently this only used for GDCH auth flow, for which it is required.
148+ STSAudience string
149+ // CredentialsFile overrides detection logic and sources a credential file
150+ // from the provided filepath. If provided, CredentialsJSON must not be.
151+ // Optional.
152+ CredentialsFile string
153+ // CredentialsJSON overrides detection logic and uses the JSON bytes as the
154+ // source for the credential. If provided, CredentialsFile must not be.
155+ // Optional.
156+ CredentialsJSON []byte
157+ // UseSelfSignedJWT directs service account based credentials to create a
158+ // self-signed JWT with the private key found in the file, skipping any
159+ // network requests that would normally be made. Optional.
160+ UseSelfSignedJWT bool
161+ // Client configures the underlying client used to make network requests
162+ // when fetching tokens. Optional.
163+ Client *http.Client
164+ // UniverseDomain is the default service domain for a given Cloud universe.
165+ // The default value is "googleapis.com". This option is ignored for
166+ // authentication flows that do not support universe domain. Optional.
167+ UniverseDomain string
168+ // Logger is used for debug logging. If provided, logging will be enabled
169+ // at the loggers configured level. By default logging is disabled unless
170+ // enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
171+ // logger will be used. Optional.
172+ Logger *slog.Logger
173+}
174+
175+func (o *DetectOptions) validate() error {
176+ if o == nil {
177+ return errors.New("credentials: options must be provided")
178+ }
179+ if len(o.Scopes) > 0 && o.Audience != "" {
180+ return errors.New("credentials: both scopes and audience were provided")
181+ }
182+ if len(o.CredentialsJSON) > 0 && o.CredentialsFile != "" {
183+ return errors.New("credentials: both credentials file and JSON were provided")
184+ }
185+ return nil
186+}
187+
188+func (o *DetectOptions) tokenURL() string {
189+ if o.TokenURL != "" {
190+ return o.TokenURL
191+ }
192+ return googleTokenURL
193+}
194+
195+func (o *DetectOptions) scopes() []string {
196+ scopes := make([]string, len(o.Scopes))
197+ copy(scopes, o.Scopes)
198+ return scopes
199+}
200+
201+func (o *DetectOptions) client() *http.Client {
202+ if o.Client != nil {
203+ return o.Client
204+ }
205+ return internal.DefaultClient()
206+}
207+
208+func (o *DetectOptions) logger() *slog.Logger {
209+ return internallog.New(o.Logger)
210+}
211+
212+func readCredentialsFile(filename string, opts *DetectOptions) (*auth.Credentials, error) {
213+ b, err := os.ReadFile(filename)
214+ if err != nil {
215+ return nil, err
216+ }
217+ return readCredentialsFileJSON(b, opts)
218+}
219+
220+func readCredentialsFileJSON(b []byte, opts *DetectOptions) (*auth.Credentials, error) {
221+ // attempt to parse jsonData as a Google Developers Console client_credentials.json.
222+ config := clientCredConfigFromJSON(b, opts)
223+ if config != nil {
224+ if config.AuthHandlerOpts == nil {
225+ return nil, errors.New("credentials: auth handler must be specified for this credential filetype")
226+ }
227+ tp, err := auth.New3LOTokenProvider(config)
228+ if err != nil {
229+ return nil, err
230+ }
231+ return auth.NewCredentials(&auth.CredentialsOptions{
232+ TokenProvider: tp,
233+ JSON: b,
234+ }), nil
235+ }
236+ return fileCredentials(b, opts)
237+}
238+
239+func clientCredConfigFromJSON(b []byte, opts *DetectOptions) *auth.Options3LO {
240+ var creds credsfile.ClientCredentialsFile
241+ var c *credsfile.Config3LO
242+ if err := json.Unmarshal(b, &creds); err != nil {
243+ return nil
244+ }
245+ switch {
246+ case creds.Web != nil:
247+ c = creds.Web
248+ case creds.Installed != nil:
249+ c = creds.Installed
250+ default:
251+ return nil
252+ }
253+ if len(c.RedirectURIs) < 1 {
254+ return nil
255+ }
256+ var handleOpts *auth.AuthorizationHandlerOptions
257+ if opts.AuthHandlerOptions != nil {
258+ handleOpts = &auth.AuthorizationHandlerOptions{
259+ Handler: opts.AuthHandlerOptions.Handler,
260+ State: opts.AuthHandlerOptions.State,
261+ PKCEOpts: opts.AuthHandlerOptions.PKCEOpts,
262+ }
263+ }
264+ return &auth.Options3LO{
265+ ClientID: c.ClientID,
266+ ClientSecret: c.ClientSecret,
267+ RedirectURL: c.RedirectURIs[0],
268+ Scopes: opts.scopes(),
269+ AuthURL: c.AuthURI,
270+ TokenURL: c.TokenURI,
271+ Client: opts.client(),
272+ Logger: opts.logger(),
273+ EarlyTokenExpiry: opts.EarlyTokenRefresh,
274+ AuthHandlerOpts: handleOpts,
275+ // TODO(codyoss): refactor this out. We need to add in auto-detection
276+ // for this use case.
277+ AuthStyle: auth.StyleInParams,
278+ }
279+}
vendor/cloud.google.com/go/auth/credentials/doc.gonew file+45
@@ -0,0 +1,45 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+// Package credentials provides support for making OAuth2 authorized and
16+// authenticated HTTP requests to Google APIs. It supports the Web server flow,
17+// client-side credentials, service accounts, Google Compute Engine service
18+// accounts, Google App Engine service accounts and workload identity federation
19+// from non-Google cloud platforms.
20+//
21+// A brief overview of the package follows. For more information, please read
22+// https://developers.google.com/accounts/docs/OAuth2
23+// and
24+// https://developers.google.com/accounts/docs/application-default-credentials.
25+// For more information on using workload identity federation, refer to
26+// https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation.
27+//
28+// # Credentials
29+//
30+// The [cloud.google.com/go/auth.Credentials] type represents Google
31+// credentials, including Application Default Credentials.
32+//
33+// Use [DetectDefault] to obtain Application Default Credentials.
34+//
35+// Application Default Credentials support workload identity federation to
36+// access Google Cloud resources from non-Google Cloud platforms including Amazon
37+// Web Services (AWS), Microsoft Azure or any identity provider that supports
38+// OpenID Connect (OIDC). Workload identity federation is recommended for
39+// non-Google Cloud environments as it avoids the need to download, manage, and
40+// store service account private keys locally.
41+//
42+// # Workforce Identity Federation
43+//
44+// For more information on this feature see [cloud.google.com/go/auth/credentials/externalaccount].
45+package credentials
vendor/cloud.google.com/go/auth/credentials/filetypes.gonew file+231
@@ -0,0 +1,231 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package credentials
16+
17+import (
18+ "errors"
19+ "fmt"
20+
21+ "cloud.google.com/go/auth"
22+ "cloud.google.com/go/auth/credentials/internal/externalaccount"
23+ "cloud.google.com/go/auth/credentials/internal/externalaccountuser"
24+ "cloud.google.com/go/auth/credentials/internal/gdch"
25+ "cloud.google.com/go/auth/credentials/internal/impersonate"
26+ internalauth "cloud.google.com/go/auth/internal"
27+ "cloud.google.com/go/auth/internal/credsfile"
28+)
29+
30+func fileCredentials(b []byte, opts *DetectOptions) (*auth.Credentials, error) {
31+ fileType, err := credsfile.ParseFileType(b)
32+ if err != nil {
33+ return nil, err
34+ }
35+
36+ var projectID, universeDomain string
37+ var tp auth.TokenProvider
38+ switch fileType {
39+ case credsfile.ServiceAccountKey:
40+ f, err := credsfile.ParseServiceAccount(b)
41+ if err != nil {
42+ return nil, err
43+ }
44+ tp, err = handleServiceAccount(f, opts)
45+ if err != nil {
46+ return nil, err
47+ }
48+ projectID = f.ProjectID
49+ universeDomain = resolveUniverseDomain(opts.UniverseDomain, f.UniverseDomain)
50+ case credsfile.UserCredentialsKey:
51+ f, err := credsfile.ParseUserCredentials(b)
52+ if err != nil {
53+ return nil, err
54+ }
55+ tp, err = handleUserCredential(f, opts)
56+ if err != nil {
57+ return nil, err
58+ }
59+ universeDomain = f.UniverseDomain
60+ case credsfile.ExternalAccountKey:
61+ f, err := credsfile.ParseExternalAccount(b)
62+ if err != nil {
63+ return nil, err
64+ }
65+ tp, err = handleExternalAccount(f, opts)
66+ if err != nil {
67+ return nil, err
68+ }
69+ universeDomain = resolveUniverseDomain(opts.UniverseDomain, f.UniverseDomain)
70+ case credsfile.ExternalAccountAuthorizedUserKey:
71+ f, err := credsfile.ParseExternalAccountAuthorizedUser(b)
72+ if err != nil {
73+ return nil, err
74+ }
75+ tp, err = handleExternalAccountAuthorizedUser(f, opts)
76+ if err != nil {
77+ return nil, err
78+ }
79+ universeDomain = f.UniverseDomain
80+ case credsfile.ImpersonatedServiceAccountKey:
81+ f, err := credsfile.ParseImpersonatedServiceAccount(b)
82+ if err != nil {
83+ return nil, err
84+ }
85+ tp, err = handleImpersonatedServiceAccount(f, opts)
86+ if err != nil {
87+ return nil, err
88+ }
89+ universeDomain = resolveUniverseDomain(opts.UniverseDomain, f.UniverseDomain)
90+ case credsfile.GDCHServiceAccountKey:
91+ f, err := credsfile.ParseGDCHServiceAccount(b)
92+ if err != nil {
93+ return nil, err
94+ }
95+ tp, err = handleGDCHServiceAccount(f, opts)
96+ if err != nil {
97+ return nil, err
98+ }
99+ projectID = f.Project
100+ universeDomain = f.UniverseDomain
101+ default:
102+ return nil, fmt.Errorf("credentials: unsupported filetype %q", fileType)
103+ }
104+ return auth.NewCredentials(&auth.CredentialsOptions{
105+ TokenProvider: auth.NewCachedTokenProvider(tp, &auth.CachedTokenProviderOptions{
106+ ExpireEarly: opts.EarlyTokenRefresh,
107+ }),
108+ JSON: b,
109+ ProjectIDProvider: internalauth.StaticCredentialsProperty(projectID),
110+ // TODO(codyoss): only set quota project here if there was a user override
111+ UniverseDomainProvider: internalauth.StaticCredentialsProperty(universeDomain),
112+ }), nil
113+}
114+
115+// resolveUniverseDomain returns optsUniverseDomain if non-empty, in order to
116+// support configuring universe-specific credentials in code. Auth flows
117+// unsupported for universe domain should not use this func, but should instead
118+// simply set the file universe domain on the credentials.
119+func resolveUniverseDomain(optsUniverseDomain, fileUniverseDomain string) string {
120+ if optsUniverseDomain != "" {
121+ return optsUniverseDomain
122+ }
123+ return fileUniverseDomain
124+}
125+
126+func handleServiceAccount(f *credsfile.ServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
127+ ud := resolveUniverseDomain(opts.UniverseDomain, f.UniverseDomain)
128+ if opts.UseSelfSignedJWT {
129+ return configureSelfSignedJWT(f, opts)
130+ } else if ud != "" && ud != internalauth.DefaultUniverseDomain {
131+ // For non-GDU universe domains, token exchange is impossible and services
132+ // must support self-signed JWTs.
133+ opts.UseSelfSignedJWT = true
134+ return configureSelfSignedJWT(f, opts)
135+ }
136+ opts2LO := &auth.Options2LO{
137+ Email: f.ClientEmail,
138+ PrivateKey: []byte(f.PrivateKey),
139+ PrivateKeyID: f.PrivateKeyID,
140+ Scopes: opts.scopes(),
141+ TokenURL: f.TokenURL,
142+ Subject: opts.Subject,
143+ Client: opts.client(),
144+ Logger: opts.logger(),
145+ }
146+ if opts2LO.TokenURL == "" {
147+ opts2LO.TokenURL = jwtTokenURL
148+ }
149+ return auth.New2LOTokenProvider(opts2LO)
150+}
151+
152+func handleUserCredential(f *credsfile.UserCredentialsFile, opts *DetectOptions) (auth.TokenProvider, error) {
153+ opts3LO := &auth.Options3LO{
154+ ClientID: f.ClientID,
155+ ClientSecret: f.ClientSecret,
156+ Scopes: opts.scopes(),
157+ AuthURL: googleAuthURL,
158+ TokenURL: opts.tokenURL(),
159+ AuthStyle: auth.StyleInParams,
160+ EarlyTokenExpiry: opts.EarlyTokenRefresh,
161+ RefreshToken: f.RefreshToken,
162+ Client: opts.client(),
163+ Logger: opts.logger(),
164+ }
165+ return auth.New3LOTokenProvider(opts3LO)
166+}
167+
168+func handleExternalAccount(f *credsfile.ExternalAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
169+ externalOpts := &externalaccount.Options{
170+ Audience: f.Audience,
171+ SubjectTokenType: f.SubjectTokenType,
172+ TokenURL: f.TokenURL,
173+ TokenInfoURL: f.TokenInfoURL,
174+ ServiceAccountImpersonationURL: f.ServiceAccountImpersonationURL,
175+ ClientSecret: f.ClientSecret,
176+ ClientID: f.ClientID,
177+ CredentialSource: f.CredentialSource,
178+ QuotaProjectID: f.QuotaProjectID,
179+ Scopes: opts.scopes(),
180+ WorkforcePoolUserProject: f.WorkforcePoolUserProject,
181+ Client: opts.client(),
182+ Logger: opts.logger(),
183+ IsDefaultClient: opts.Client == nil,
184+ }
185+ if f.ServiceAccountImpersonation != nil {
186+ externalOpts.ServiceAccountImpersonationLifetimeSeconds = f.ServiceAccountImpersonation.TokenLifetimeSeconds
187+ }
188+ return externalaccount.NewTokenProvider(externalOpts)
189+}
190+
191+func handleExternalAccountAuthorizedUser(f *credsfile.ExternalAccountAuthorizedUserFile, opts *DetectOptions) (auth.TokenProvider, error) {
192+ externalOpts := &externalaccountuser.Options{
193+ Audience: f.Audience,
194+ RefreshToken: f.RefreshToken,
195+ TokenURL: f.TokenURL,
196+ TokenInfoURL: f.TokenInfoURL,
197+ ClientID: f.ClientID,
198+ ClientSecret: f.ClientSecret,
199+ Scopes: opts.scopes(),
200+ Client: opts.client(),
201+ Logger: opts.logger(),
202+ }
203+ return externalaccountuser.NewTokenProvider(externalOpts)
204+}
205+
206+func handleImpersonatedServiceAccount(f *credsfile.ImpersonatedServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
207+ if f.ServiceAccountImpersonationURL == "" || f.CredSource == nil {
208+ return nil, errors.New("missing 'source_credentials' field or 'service_account_impersonation_url' in credentials")
209+ }
210+
211+ tp, err := fileCredentials(f.CredSource, opts)
212+ if err != nil {
213+ return nil, err
214+ }
215+ return impersonate.NewTokenProvider(&impersonate.Options{
216+ URL: f.ServiceAccountImpersonationURL,
217+ Scopes: opts.scopes(),
218+ Tp: tp,
219+ Delegates: f.Delegates,
220+ Client: opts.client(),
221+ Logger: opts.logger(),
222+ })
223+}
224+
225+func handleGDCHServiceAccount(f *credsfile.GDCHServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
226+ return gdch.NewTokenProvider(f, &gdch.Options{
227+ STSAudience: opts.STSAudience,
228+ Client: opts.client(),
229+ Logger: opts.logger(),
230+ })
231+}
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/aws_provider.gonew file+531
@@ -0,0 +1,531 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package externalaccount
16+
17+import (
18+ "bytes"
19+ "context"
20+ "crypto/hmac"
21+ "crypto/sha256"
22+ "encoding/hex"
23+ "encoding/json"
24+ "errors"
25+ "fmt"
26+ "log/slog"
27+ "net/http"
28+ "net/url"
29+ "os"
30+ "path"
31+ "sort"
32+ "strings"
33+ "time"
34+
35+ "cloud.google.com/go/auth/internal"
36+ "github.com/googleapis/gax-go/v2/internallog"
37+)
38+
39+var (
40+ // getenv aliases os.Getenv for testing
41+ getenv = os.Getenv
42+)
43+
44+const (
45+ // AWS Signature Version 4 signing algorithm identifier.
46+ awsAlgorithm = "AWS4-HMAC-SHA256"
47+
48+ // The termination string for the AWS credential scope value as defined in
49+ // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
50+ awsRequestType = "aws4_request"
51+
52+ // The AWS authorization header name for the security session token if available.
53+ awsSecurityTokenHeader = "x-amz-security-token"
54+
55+ // The name of the header containing the session token for metadata endpoint calls
56+ awsIMDSv2SessionTokenHeader = "X-aws-ec2-metadata-token"
57+
58+ awsIMDSv2SessionTTLHeader = "X-aws-ec2-metadata-token-ttl-seconds"
59+
60+ awsIMDSv2SessionTTL = "300"
61+
62+ // The AWS authorization header name for the auto-generated date.
63+ awsDateHeader = "x-amz-date"
64+
65+ defaultRegionalCredentialVerificationURL = "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
66+
67+ // Supported AWS configuration environment variables.
68+ awsAccessKeyIDEnvVar = "AWS_ACCESS_KEY_ID"
69+ awsDefaultRegionEnvVar = "AWS_DEFAULT_REGION"
70+ awsRegionEnvVar = "AWS_REGION"
71+ awsSecretAccessKeyEnvVar = "AWS_SECRET_ACCESS_KEY"
72+ awsSessionTokenEnvVar = "AWS_SESSION_TOKEN"
73+
74+ awsTimeFormatLong = "20060102T150405Z"
75+ awsTimeFormatShort = "20060102"
76+ awsProviderType = "aws"
77+)
78+
79+type awsSubjectProvider struct {
80+ EnvironmentID string
81+ RegionURL string
82+ RegionalCredVerificationURL string
83+ CredVerificationURL string
84+ IMDSv2SessionTokenURL string
85+ TargetResource string
86+ requestSigner *awsRequestSigner
87+ region string
88+ securityCredentialsProvider AwsSecurityCredentialsProvider
89+ reqOpts *RequestOptions
90+
91+ Client *http.Client
92+ logger *slog.Logger
93+}
94+
95+func (sp *awsSubjectProvider) subjectToken(ctx context.Context) (string, error) {
96+ // Set Defaults
97+ if sp.RegionalCredVerificationURL == "" {
98+ sp.RegionalCredVerificationURL = defaultRegionalCredentialVerificationURL
99+ }
100+ headers := make(map[string]string)
101+ if sp.shouldUseMetadataServer() {
102+ awsSessionToken, err := sp.getAWSSessionToken(ctx)
103+ if err != nil {
104+ return "", err
105+ }
106+
107+ if awsSessionToken != "" {
108+ headers[awsIMDSv2SessionTokenHeader] = awsSessionToken
109+ }
110+ }
111+
112+ awsSecurityCredentials, err := sp.getSecurityCredentials(ctx, headers)
113+ if err != nil {
114+ return "", err
115+ }
116+ if sp.region, err = sp.getRegion(ctx, headers); err != nil {
117+ return "", err
118+ }
119+ sp.requestSigner = &awsRequestSigner{
120+ RegionName: sp.region,
121+ AwsSecurityCredentials: awsSecurityCredentials,
122+ }
123+
124+ // Generate the signed request to AWS STS GetCallerIdentity API.
125+ // Use the required regional endpoint. Otherwise, the request will fail.
126+ req, err := http.NewRequestWithContext(ctx, "POST", strings.Replace(sp.RegionalCredVerificationURL, "{region}", sp.region, 1), nil)
127+ if err != nil {
128+ return "", err
129+ }
130+ // The full, canonical resource name of the workload identity pool
131+ // provider, with or without the HTTPS prefix.
132+ // Including this header as part of the signature is recommended to
133+ // ensure data integrity.
134+ if sp.TargetResource != "" {
135+ req.Header.Set("x-goog-cloud-target-resource", sp.TargetResource)
136+ }
137+ sp.requestSigner.signRequest(req)
138+
139+ /*
140+ The GCP STS endpoint expects the headers to be formatted as:
141+ # [
142+ # {key: 'x-amz-date', value: '...'},
143+ # {key: 'Authorization', value: '...'},
144+ # ...
145+ # ]
146+ # And then serialized as:
147+ # quote(json.dumps({
148+ # url: '...',
149+ # method: 'POST',
150+ # headers: [{key: 'x-amz-date', value: '...'}, ...]
151+ # }))
152+ */
153+
154+ awsSignedReq := awsRequest{
155+ URL: req.URL.String(),
156+ Method: "POST",
157+ }
158+ for headerKey, headerList := range req.Header {
159+ for _, headerValue := range headerList {
160+ awsSignedReq.Headers = append(awsSignedReq.Headers, awsRequestHeader{
161+ Key: headerKey,
162+ Value: headerValue,
163+ })
164+ }
165+ }
166+ sort.Slice(awsSignedReq.Headers, func(i, j int) bool {
167+ headerCompare := strings.Compare(awsSignedReq.Headers[i].Key, awsSignedReq.Headers[j].Key)
168+ if headerCompare == 0 {
169+ return strings.Compare(awsSignedReq.Headers[i].Value, awsSignedReq.Headers[j].Value) < 0
170+ }
171+ return headerCompare < 0
172+ })
173+
174+ result, err := json.Marshal(awsSignedReq)
175+ if err != nil {
176+ return "", err
177+ }
178+ return url.QueryEscape(string(result)), nil
179+}
180+
181+func (sp *awsSubjectProvider) providerType() string {
182+ if sp.securityCredentialsProvider != nil {
183+ return programmaticProviderType
184+ }
185+ return awsProviderType
186+}
187+
188+func (sp *awsSubjectProvider) getAWSSessionToken(ctx context.Context) (string, error) {
189+ if sp.IMDSv2SessionTokenURL == "" {
190+ return "", nil
191+ }
192+ req, err := http.NewRequestWithContext(ctx, "PUT", sp.IMDSv2SessionTokenURL, nil)
193+ if err != nil {
194+ return "", err
195+ }
196+ req.Header.Set(awsIMDSv2SessionTTLHeader, awsIMDSv2SessionTTL)
197+
198+ sp.logger.DebugContext(ctx, "aws session token request", "request", internallog.HTTPRequest(req, nil))
199+ resp, body, err := internal.DoRequest(sp.Client, req)
200+ if err != nil {
201+ return "", err
202+ }
203+ sp.logger.DebugContext(ctx, "aws session token response", "response", internallog.HTTPResponse(resp, body))
204+ if resp.StatusCode != http.StatusOK {
205+ return "", fmt.Errorf("credentials: unable to retrieve AWS session token: %s", body)
206+ }
207+ return string(body), nil
208+}
209+
210+func (sp *awsSubjectProvider) getRegion(ctx context.Context, headers map[string]string) (string, error) {
211+ if sp.securityCredentialsProvider != nil {
212+ return sp.securityCredentialsProvider.AwsRegion(ctx, sp.reqOpts)
213+ }
214+ if canRetrieveRegionFromEnvironment() {
215+ if envAwsRegion := getenv(awsRegionEnvVar); envAwsRegion != "" {
216+ return envAwsRegion, nil
217+ }
218+ return getenv(awsDefaultRegionEnvVar), nil
219+ }
220+
221+ if sp.RegionURL == "" {
222+ return "", errors.New("credentials: unable to determine AWS region")
223+ }
224+
225+ req, err := http.NewRequestWithContext(ctx, "GET", sp.RegionURL, nil)
226+ if err != nil {
227+ return "", err
228+ }
229+
230+ for name, value := range headers {
231+ req.Header.Add(name, value)
232+ }
233+ sp.logger.DebugContext(ctx, "aws region request", "request", internallog.HTTPRequest(req, nil))
234+ resp, body, err := internal.DoRequest(sp.Client, req)
235+ if err != nil {
236+ return "", err
237+ }
238+ sp.logger.DebugContext(ctx, "aws region response", "response", internallog.HTTPResponse(resp, body))
239+ if resp.StatusCode != http.StatusOK {
240+ return "", fmt.Errorf("credentials: unable to retrieve AWS region - %s", body)
241+ }
242+
243+ // This endpoint will return the region in format: us-east-2b.
244+ // Only the us-east-2 part should be used.
245+ bodyLen := len(body)
246+ if bodyLen == 0 {
247+ return "", nil
248+ }
249+ return string(body[:bodyLen-1]), nil
250+}
251+
252+func (sp *awsSubjectProvider) getSecurityCredentials(ctx context.Context, headers map[string]string) (result *AwsSecurityCredentials, err error) {
253+ if sp.securityCredentialsProvider != nil {
254+ return sp.securityCredentialsProvider.AwsSecurityCredentials(ctx, sp.reqOpts)
255+ }
256+ if canRetrieveSecurityCredentialFromEnvironment() {
257+ return &AwsSecurityCredentials{
258+ AccessKeyID: getenv(awsAccessKeyIDEnvVar),
259+ SecretAccessKey: getenv(awsSecretAccessKeyEnvVar),
260+ SessionToken: getenv(awsSessionTokenEnvVar),
261+ }, nil
262+ }
263+
264+ roleName, err := sp.getMetadataRoleName(ctx, headers)
265+ if err != nil {
266+ return
267+ }
268+ credentials, err := sp.getMetadataSecurityCredentials(ctx, roleName, headers)
269+ if err != nil {
270+ return
271+ }
272+
273+ if credentials.AccessKeyID == "" {
274+ return result, errors.New("credentials: missing AccessKeyId credential")
275+ }
276+ if credentials.SecretAccessKey == "" {
277+ return result, errors.New("credentials: missing SecretAccessKey credential")
278+ }
279+
280+ return credentials, nil
281+}
282+
283+func (sp *awsSubjectProvider) getMetadataSecurityCredentials(ctx context.Context, roleName string, headers map[string]string) (*AwsSecurityCredentials, error) {
284+ var result *AwsSecurityCredentials
285+
286+ req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s", sp.CredVerificationURL, roleName), nil)
287+ if err != nil {
288+ return result, err
289+ }
290+ for name, value := range headers {
291+ req.Header.Add(name, value)
292+ }
293+ sp.logger.DebugContext(ctx, "aws security credential request", "request", internallog.HTTPRequest(req, nil))
294+ resp, body, err := internal.DoRequest(sp.Client, req)
295+ if err != nil {
296+ return result, err
297+ }
298+ sp.logger.DebugContext(ctx, "aws security credential response", "response", internallog.HTTPResponse(resp, body))
299+ if resp.StatusCode != http.StatusOK {
300+ return result, fmt.Errorf("credentials: unable to retrieve AWS security credentials - %s", body)
301+ }
302+ if err := json.Unmarshal(body, &result); err != nil {
303+ return nil, err
304+ }
305+ return result, nil
306+}
307+
308+func (sp *awsSubjectProvider) getMetadataRoleName(ctx context.Context, headers map[string]string) (string, error) {
309+ if sp.CredVerificationURL == "" {
310+ return "", errors.New("credentials: unable to determine the AWS metadata server security credentials endpoint")
311+ }
312+ req, err := http.NewRequestWithContext(ctx, "GET", sp.CredVerificationURL, nil)
313+ if err != nil {
314+ return "", err
315+ }
316+ for name, value := range headers {
317+ req.Header.Add(name, value)
318+ }
319+
320+ sp.logger.DebugContext(ctx, "aws metadata role request", "request", internallog.HTTPRequest(req, nil))
321+ resp, body, err := internal.DoRequest(sp.Client, req)
322+ if err != nil {
323+ return "", err
324+ }
325+ sp.logger.DebugContext(ctx, "aws metadata role response", "response", internallog.HTTPResponse(resp, body))
326+ if resp.StatusCode != http.StatusOK {
327+ return "", fmt.Errorf("credentials: unable to retrieve AWS role name - %s", body)
328+ }
329+ return string(body), nil
330+}
331+
332+// awsRequestSigner is a utility class to sign http requests using a AWS V4 signature.
333+type awsRequestSigner struct {
334+ RegionName string
335+ AwsSecurityCredentials *AwsSecurityCredentials
336+}
337+
338+// signRequest adds the appropriate headers to an http.Request
339+// or returns an error if something prevented this.
340+func (rs *awsRequestSigner) signRequest(req *http.Request) error {
341+ // req is assumed non-nil
342+ signedRequest := cloneRequest(req)
343+ timestamp := Now()
344+ signedRequest.Header.Set("host", requestHost(req))
345+ if rs.AwsSecurityCredentials.SessionToken != "" {
346+ signedRequest.Header.Set(awsSecurityTokenHeader, rs.AwsSecurityCredentials.SessionToken)
347+ }
348+ if signedRequest.Header.Get("date") == "" {
349+ signedRequest.Header.Set(awsDateHeader, timestamp.Format(awsTimeFormatLong))
350+ }
351+ authorizationCode, err := rs.generateAuthentication(signedRequest, timestamp)
352+ if err != nil {
353+ return err
354+ }
355+ signedRequest.Header.Set("Authorization", authorizationCode)
356+ req.Header = signedRequest.Header
357+ return nil
358+}
359+
360+func (rs *awsRequestSigner) generateAuthentication(req *http.Request, timestamp time.Time) (string, error) {
361+ canonicalHeaderColumns, canonicalHeaderData := canonicalHeaders(req)
362+ dateStamp := timestamp.Format(awsTimeFormatShort)
363+ serviceName := ""
364+
365+ if splitHost := strings.Split(requestHost(req), "."); len(splitHost) > 0 {
366+ serviceName = splitHost[0]
367+ }
368+ credentialScope := strings.Join([]string{dateStamp, rs.RegionName, serviceName, awsRequestType}, "/")
369+ requestString, err := canonicalRequest(req, canonicalHeaderColumns, canonicalHeaderData)
370+ if err != nil {
371+ return "", err
372+ }
373+ requestHash, err := getSha256([]byte(requestString))
374+ if err != nil {
375+ return "", err
376+ }
377+
378+ stringToSign := strings.Join([]string{awsAlgorithm, timestamp.Format(awsTimeFormatLong), credentialScope, requestHash}, "\n")
379+ signingKey := []byte("AWS4" + rs.AwsSecurityCredentials.SecretAccessKey)
380+ for _, signingInput := range []string{
381+ dateStamp, rs.RegionName, serviceName, awsRequestType, stringToSign,
382+ } {
383+ signingKey, err = getHmacSha256(signingKey, []byte(signingInput))
384+ if err != nil {
385+ return "", err
386+ }
387+ }
388+
389+ return fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", awsAlgorithm, rs.AwsSecurityCredentials.AccessKeyID, credentialScope, canonicalHeaderColumns, hex.EncodeToString(signingKey)), nil
390+}
391+
392+func getSha256(input []byte) (string, error) {
393+ hash := sha256.New()
394+ if _, err := hash.Write(input); err != nil {
395+ return "", err
396+ }
397+ return hex.EncodeToString(hash.Sum(nil)), nil
398+}
399+
400+func getHmacSha256(key, input []byte) ([]byte, error) {
401+ hash := hmac.New(sha256.New, key)
402+ if _, err := hash.Write(input); err != nil {
403+ return nil, err
404+ }
405+ return hash.Sum(nil), nil
406+}
407+
408+func cloneRequest(r *http.Request) *http.Request {
409+ r2 := new(http.Request)
410+ *r2 = *r
411+ if r.Header != nil {
412+ r2.Header = make(http.Header, len(r.Header))
413+
414+ // Find total number of values.
415+ headerCount := 0
416+ for _, headerValues := range r.Header {
417+ headerCount += len(headerValues)
418+ }
419+ copiedHeaders := make([]string, headerCount) // shared backing array for headers' values
420+
421+ for headerKey, headerValues := range r.Header {
422+ headerCount = copy(copiedHeaders, headerValues)
423+ r2.Header[headerKey] = copiedHeaders[:headerCount:headerCount]
424+ copiedHeaders = copiedHeaders[headerCount:]
425+ }
426+ }
427+ return r2
428+}
429+
430+func canonicalPath(req *http.Request) string {
431+ result := req.URL.EscapedPath()
432+ if result == "" {
433+ return "/"
434+ }
435+ return path.Clean(result)
436+}
437+
438+func canonicalQuery(req *http.Request) string {
439+ queryValues := req.URL.Query()
440+ for queryKey := range queryValues {
441+ sort.Strings(queryValues[queryKey])
442+ }
443+ return queryValues.Encode()
444+}
445+
446+func canonicalHeaders(req *http.Request) (string, string) {
447+ // Header keys need to be sorted alphabetically.
448+ var headers []string
449+ lowerCaseHeaders := make(http.Header)
450+ for k, v := range req.Header {
451+ k := strings.ToLower(k)
452+ if _, ok := lowerCaseHeaders[k]; ok {
453+ // include additional values
454+ lowerCaseHeaders[k] = append(lowerCaseHeaders[k], v...)
455+ } else {
456+ headers = append(headers, k)
457+ lowerCaseHeaders[k] = v
458+ }
459+ }
460+ sort.Strings(headers)
461+
462+ var fullHeaders bytes.Buffer
463+ for _, header := range headers {
464+ headerValue := strings.Join(lowerCaseHeaders[header], ",")
465+ fullHeaders.WriteString(header)
466+ fullHeaders.WriteRune(':')
467+ fullHeaders.WriteString(headerValue)
468+ fullHeaders.WriteRune('\n')
469+ }
470+
471+ return strings.Join(headers, ";"), fullHeaders.String()
472+}
473+
474+func requestDataHash(req *http.Request) (string, error) {
475+ var requestData []byte
476+ if req.Body != nil {
477+ requestBody, err := req.GetBody()
478+ if err != nil {
479+ return "", err
480+ }
481+ defer requestBody.Close()
482+
483+ requestData, err = internal.ReadAll(requestBody)
484+ if err != nil {
485+ return "", err
486+ }
487+ }
488+
489+ return getSha256(requestData)
490+}
491+
492+func requestHost(req *http.Request) string {
493+ if req.Host != "" {
494+ return req.Host
495+ }
496+ return req.URL.Host
497+}
498+
499+func canonicalRequest(req *http.Request, canonicalHeaderColumns, canonicalHeaderData string) (string, error) {
500+ dataHash, err := requestDataHash(req)
501+ if err != nil {
502+ return "", err
503+ }
504+ return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", req.Method, canonicalPath(req), canonicalQuery(req), canonicalHeaderData, canonicalHeaderColumns, dataHash), nil
505+}
506+
507+type awsRequestHeader struct {
508+ Key string `json:"key"`
509+ Value string `json:"value"`
510+}
511+
512+type awsRequest struct {
513+ URL string `json:"url"`
514+ Method string `json:"method"`
515+ Headers []awsRequestHeader `json:"headers"`
516+}
517+
518+// The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION. Only one is
519+// required.
520+func canRetrieveRegionFromEnvironment() bool {
521+ return getenv(awsRegionEnvVar) != "" || getenv(awsDefaultRegionEnvVar) != ""
522+}
523+
524+// Check if both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are available.
525+func canRetrieveSecurityCredentialFromEnvironment() bool {
526+ return getenv(awsAccessKeyIDEnvVar) != "" && getenv(awsSecretAccessKeyEnvVar) != ""
527+}
528+
529+func (sp *awsSubjectProvider) shouldUseMetadataServer() bool {
530+ return sp.securityCredentialsProvider == nil && (!canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialFromEnvironment())
531+}
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/executable_provider.gonew file+284
@@ -0,0 +1,284 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package externalaccount
16+
17+import (
18+ "bytes"
19+ "context"
20+ "encoding/json"
21+ "errors"
22+ "fmt"
23+ "net/http"
24+ "os"
25+ "os/exec"
26+ "regexp"
27+ "strings"
28+ "time"
29+
30+ "cloud.google.com/go/auth/internal"
31+)
32+
33+const (
34+ executableSupportedMaxVersion = 1
35+ executableDefaultTimeout = 30 * time.Second
36+ executableSource = "response"
37+ executableProviderType = "executable"
38+ outputFileSource = "output file"
39+
40+ allowExecutablesEnvVar = "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES"
41+
42+ jwtTokenType = "urn:ietf:params:oauth:token-type:jwt"
43+ idTokenType = "urn:ietf:params:oauth:token-type:id_token"
44+ saml2TokenType = "urn:ietf:params:oauth:token-type:saml2"
45+)
46+
47+var (
48+ serviceAccountImpersonationRE = regexp.MustCompile(`https://iamcredentials..+/v1/projects/-/serviceAccounts/(.*@.*):generateAccessToken`)
49+)
50+
51+type nonCacheableError struct {
52+ message string
53+}
54+
55+func (nce nonCacheableError) Error() string {
56+ return nce.message
57+}
58+
59+// environment is a contract for testing
60+type environment interface {
61+ existingEnv() []string
62+ getenv(string) string
63+ run(ctx context.Context, command string, env []string) ([]byte, error)
64+ now() time.Time
65+}
66+
67+type runtimeEnvironment struct{}
68+
69+func (r runtimeEnvironment) existingEnv() []string {
70+ return os.Environ()
71+}
72+func (r runtimeEnvironment) getenv(key string) string {
73+ return os.Getenv(key)
74+}
75+func (r runtimeEnvironment) now() time.Time {
76+ return time.Now().UTC()
77+}
78+
79+func (r runtimeEnvironment) run(ctx context.Context, command string, env []string) ([]byte, error) {
80+ splitCommand := strings.Fields(command)
81+ cmd := exec.CommandContext(ctx, splitCommand[0], splitCommand[1:]...)
82+ cmd.Env = env
83+
84+ var stdout, stderr bytes.Buffer
85+ cmd.Stdout = &stdout
86+ cmd.Stderr = &stderr
87+
88+ if err := cmd.Run(); err != nil {
89+ if ctx.Err() == context.DeadlineExceeded {
90+ return nil, context.DeadlineExceeded
91+ }
92+ if exitError, ok := err.(*exec.ExitError); ok {
93+ return nil, exitCodeError(exitError)
94+ }
95+ return nil, executableError(err)
96+ }
97+
98+ bytesStdout := bytes.TrimSpace(stdout.Bytes())
99+ if len(bytesStdout) > 0 {
100+ return bytesStdout, nil
101+ }
102+ return bytes.TrimSpace(stderr.Bytes()), nil
103+}
104+
105+type executableSubjectProvider struct {
106+ Command string
107+ Timeout time.Duration
108+ OutputFile string
109+ client *http.Client
110+ opts *Options
111+ env environment
112+}
113+
114+type executableResponse struct {
115+ Version int `json:"version,omitempty"`
116+ Success *bool `json:"success,omitempty"`
117+ TokenType string `json:"token_type,omitempty"`
118+ ExpirationTime int64 `json:"expiration_time,omitempty"`
119+ IDToken string `json:"id_token,omitempty"`
120+ SamlResponse string `json:"saml_response,omitempty"`
121+ Code string `json:"code,omitempty"`
122+ Message string `json:"message,omitempty"`
123+}
124+
125+func (sp *executableSubjectProvider) parseSubjectTokenFromSource(response []byte, source string, now int64) (string, error) {
126+ var result executableResponse
127+ if err := json.Unmarshal(response, &result); err != nil {
128+ return "", jsonParsingError(source, string(response))
129+ }
130+ // Validate
131+ if result.Version == 0 {
132+ return "", missingFieldError(source, "version")
133+ }
134+ if result.Success == nil {
135+ return "", missingFieldError(source, "success")
136+ }
137+ if !*result.Success {
138+ if result.Code == "" || result.Message == "" {
139+ return "", malformedFailureError()
140+ }
141+ return "", userDefinedError(result.Code, result.Message)
142+ }
143+ if result.Version > executableSupportedMaxVersion || result.Version < 0 {
144+ return "", unsupportedVersionError(source, result.Version)
145+ }
146+ if result.ExpirationTime == 0 && sp.OutputFile != "" {
147+ return "", missingFieldError(source, "expiration_time")
148+ }
149+ if result.TokenType == "" {
150+ return "", missingFieldError(source, "token_type")
151+ }
152+ if result.ExpirationTime != 0 && result.ExpirationTime < now {
153+ return "", tokenExpiredError()
154+ }
155+
156+ switch result.TokenType {
157+ case jwtTokenType, idTokenType:
158+ if result.IDToken == "" {
159+ return "", missingFieldError(source, "id_token")
160+ }
161+ return result.IDToken, nil
162+ case saml2TokenType:
163+ if result.SamlResponse == "" {
164+ return "", missingFieldError(source, "saml_response")
165+ }
166+ return result.SamlResponse, nil
167+ default:
168+ return "", tokenTypeError(source)
169+ }
170+}
171+
172+func (sp *executableSubjectProvider) subjectToken(ctx context.Context) (string, error) {
173+ if token, err := sp.getTokenFromOutputFile(); token != "" || err != nil {
174+ return token, err
175+ }
176+ return sp.getTokenFromExecutableCommand(ctx)
177+}
178+
179+func (sp *executableSubjectProvider) providerType() string {
180+ return executableProviderType
181+}
182+
183+func (sp *executableSubjectProvider) getTokenFromOutputFile() (token string, err error) {
184+ if sp.OutputFile == "" {
185+ // This ExecutableCredentialSource doesn't use an OutputFile.
186+ return "", nil
187+ }
188+
189+ file, err := os.Open(sp.OutputFile)
190+ if err != nil {
191+ // No OutputFile found. Hasn't been created yet, so skip it.
192+ return "", nil
193+ }
194+ defer file.Close()
195+
196+ data, err := internal.ReadAll(file)
197+ if err != nil || len(data) == 0 {
198+ // Cachefile exists, but no data found. Get new credential.
199+ return "", nil
200+ }
201+
202+ token, err = sp.parseSubjectTokenFromSource(data, outputFileSource, sp.env.now().Unix())
203+ if err != nil {
204+ if _, ok := err.(nonCacheableError); ok {
205+ // If the cached token is expired we need a new token,
206+ // and if the cache contains a failure, we need to try again.
207+ return "", nil
208+ }
209+
210+ // There was an error in the cached token, and the developer should be aware of it.
211+ return "", err
212+ }
213+ // Token parsing succeeded. Use found token.
214+ return token, nil
215+}
216+
217+func (sp *executableSubjectProvider) executableEnvironment() []string {
218+ result := sp.env.existingEnv()
219+ result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=%v", sp.opts.Audience))
220+ result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=%v", sp.opts.SubjectTokenType))
221+ result = append(result, "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0")
222+ if sp.opts.ServiceAccountImpersonationURL != "" {
223+ matches := serviceAccountImpersonationRE.FindStringSubmatch(sp.opts.ServiceAccountImpersonationURL)
224+ if matches != nil {
225+ result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL=%v", matches[1]))
226+ }
227+ }
228+ if sp.OutputFile != "" {
229+ result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE=%v", sp.OutputFile))
230+ }
231+ return result
232+}
233+
234+func (sp *executableSubjectProvider) getTokenFromExecutableCommand(ctx context.Context) (string, error) {
235+ // For security reasons, we need our consumers to set this environment variable to allow executables to be run.
236+ if sp.env.getenv(allowExecutablesEnvVar) != "1" {
237+ return "", errors.New("credentials: executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run")
238+ }
239+
240+ ctx, cancel := context.WithDeadline(ctx, sp.env.now().Add(sp.Timeout))
241+ defer cancel()
242+
243+ output, err := sp.env.run(ctx, sp.Command, sp.executableEnvironment())
244+ if err != nil {
245+ return "", err
246+ }
247+ return sp.parseSubjectTokenFromSource(output, executableSource, sp.env.now().Unix())
248+}
249+
250+func missingFieldError(source, field string) error {
251+ return fmt.Errorf("credentials: %q missing %q field", source, field)
252+}
253+
254+func jsonParsingError(source, data string) error {
255+ return fmt.Errorf("credentials: unable to parse %q: %v", source, data)
256+}
257+
258+func malformedFailureError() error {
259+ return nonCacheableError{"credentials: response must include `error` and `message` fields when unsuccessful"}
260+}
261+
262+func userDefinedError(code, message string) error {
263+ return nonCacheableError{fmt.Sprintf("credentials: response contains unsuccessful response: (%v) %v", code, message)}
264+}
265+
266+func unsupportedVersionError(source string, version int) error {
267+ return fmt.Errorf("credentials: %v contains unsupported version: %v", source, version)
268+}
269+
270+func tokenExpiredError() error {
271+ return nonCacheableError{"credentials: the token returned by the executable is expired"}
272+}
273+
274+func tokenTypeError(source string) error {
275+ return fmt.Errorf("credentials: %v contains unsupported token type", source)
276+}
277+
278+func exitCodeError(err *exec.ExitError) error {
279+ return fmt.Errorf("credentials: executable command failed with exit code %v: %w", err.ExitCode(), err)
280+}
281+
282+func executableError(err error) error {
283+ return fmt.Errorf("credentials: executable command failed: %w", err)
284+}
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/externalaccount.gonew file+428
@@ -0,0 +1,428 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package externalaccount
16+
17+import (
18+ "context"
19+ "errors"
20+ "fmt"
21+ "log/slog"
22+ "net/http"
23+ "regexp"
24+ "strconv"
25+ "strings"
26+ "time"
27+
28+ "cloud.google.com/go/auth"
29+ "cloud.google.com/go/auth/credentials/internal/impersonate"
30+ "cloud.google.com/go/auth/credentials/internal/stsexchange"
31+ "cloud.google.com/go/auth/internal/credsfile"
32+ "github.com/googleapis/gax-go/v2/internallog"
33+)
34+
35+const (
36+ timeoutMinimum = 5 * time.Second
37+ timeoutMaximum = 120 * time.Second
38+
39+ universeDomainPlaceholder = "UNIVERSE_DOMAIN"
40+ defaultTokenURL = "https://sts.UNIVERSE_DOMAIN/v1/token"
41+ defaultUniverseDomain = "googleapis.com"
42+)
43+
44+var (
45+ // Now aliases time.Now for testing
46+ Now = func() time.Time {
47+ return time.Now().UTC()
48+ }
49+ validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
50+)
51+
52+// Options stores the configuration for fetching tokens with external credentials.
53+type Options struct {
54+ // Audience is the Secure Token Service (STS) audience which contains the resource name for the workload
55+ // identity pool or the workforce pool and the provider identifier in that pool.
56+ Audience string
57+ // SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec
58+ // e.g. `urn:ietf:params:oauth:token-type:jwt`.
59+ SubjectTokenType string
60+ // TokenURL is the STS token exchange endpoint.
61+ TokenURL string
62+ // TokenInfoURL is the token_info endpoint used to retrieve the account related information (
63+ // user attributes like account identifier, eg. email, username, uid, etc). This is
64+ // needed for gCloud session account identification.
65+ TokenInfoURL string
66+ // ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
67+ // required for workload identity pools when APIs to be accessed have not integrated with UberMint.
68+ ServiceAccountImpersonationURL string
69+ // ServiceAccountImpersonationLifetimeSeconds is the number of seconds the service account impersonation
70+ // token will be valid for.
71+ ServiceAccountImpersonationLifetimeSeconds int
72+ // ClientSecret is currently only required if token_info endpoint also
73+ // needs to be called with the generated GCP access token. When provided, STS will be
74+ // called with additional basic authentication using client_id as username and client_secret as password.
75+ ClientSecret string
76+ // ClientID is only required in conjunction with ClientSecret, as described above.
77+ ClientID string
78+ // CredentialSource contains the necessary information to retrieve the token itself, as well
79+ // as some environmental information.
80+ CredentialSource *credsfile.CredentialSource
81+ // QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries
82+ // will set the x-goog-user-project which overrides the project associated with the credentials.
83+ QuotaProjectID string
84+ // Scopes contains the desired scopes for the returned access token.
85+ Scopes []string
86+ // WorkforcePoolUserProject should be set when it is a workforce pool and
87+ // not a workload identity pool. The underlying principal must still have
88+ // serviceusage.services.use IAM permission to use the project for
89+ // billing/quota. Optional.
90+ WorkforcePoolUserProject string
91+ // UniverseDomain is the default service domain for a given Cloud universe.
92+ // This value will be used in the default STS token URL. The default value
93+ // is "googleapis.com". It will not be used if TokenURL is set. Optional.
94+ UniverseDomain string
95+ // SubjectTokenProvider is an optional token provider for OIDC/SAML
96+ // credentials. One of SubjectTokenProvider, AWSSecurityCredentialProvider
97+ // or CredentialSource must be provided. Optional.
98+ SubjectTokenProvider SubjectTokenProvider
99+ // AwsSecurityCredentialsProvider is an AWS Security Credential provider
100+ // for AWS credentials. One of SubjectTokenProvider,
101+ // AWSSecurityCredentialProvider or CredentialSource must be provided. Optional.
102+ AwsSecurityCredentialsProvider AwsSecurityCredentialsProvider
103+ // Client for token request.
104+ Client *http.Client
105+ // IsDefaultClient marks whether the client passed in is a default client that can be overriden.
106+ // This is important for X509 credentials which should create a new client if the default was used
107+ // but should respect a client explicitly passed in by the user.
108+ IsDefaultClient bool
109+ // Logger is used for debug logging. If provided, logging will be enabled
110+ // at the loggers configured level. By default logging is disabled unless
111+ // enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
112+ // logger will be used. Optional.
113+ Logger *slog.Logger
114+}
115+
116+// SubjectTokenProvider can be used to supply a subject token to exchange for a
117+// GCP access token.
118+type SubjectTokenProvider interface {
119+ // SubjectToken should return a valid subject token or an error.
120+ // The external account token provider does not cache the returned subject
121+ // token, so caching logic should be implemented in the provider to prevent
122+ // multiple requests for the same subject token.
123+ SubjectToken(ctx context.Context, opts *RequestOptions) (string, error)
124+}
125+
126+// RequestOptions contains information about the requested subject token or AWS
127+// security credentials from the Google external account credential.
128+type RequestOptions struct {
129+ // Audience is the requested audience for the external account credential.
130+ Audience string
131+ // Subject token type is the requested subject token type for the external
132+ // account credential. Expected values include:
133+ // “urn:ietf:params:oauth:token-type:jwt”
134+ // “urn:ietf:params:oauth:token-type:id-token”
135+ // “urn:ietf:params:oauth:token-type:saml2”
136+ // “urn:ietf:params:aws:token-type:aws4_request”
137+ SubjectTokenType string
138+}
139+
140+// AwsSecurityCredentialsProvider can be used to supply AwsSecurityCredentials
141+// and an AWS Region to exchange for a GCP access token.
142+type AwsSecurityCredentialsProvider interface {
143+ // AwsRegion should return the AWS region or an error.
144+ AwsRegion(ctx context.Context, opts *RequestOptions) (string, error)
145+ // GetAwsSecurityCredentials should return a valid set of
146+ // AwsSecurityCredentials or an error. The external account token provider
147+ // does not cache the returned security credentials, so caching logic should
148+ // be implemented in the provider to prevent multiple requests for the
149+ // same security credentials.
150+ AwsSecurityCredentials(ctx context.Context, opts *RequestOptions) (*AwsSecurityCredentials, error)
151+}
152+
153+// AwsSecurityCredentials models AWS security credentials.
154+type AwsSecurityCredentials struct {
155+ // AccessKeyId is the AWS Access Key ID - Required.
156+ AccessKeyID string `json:"AccessKeyID"`
157+ // SecretAccessKey is the AWS Secret Access Key - Required.
158+ SecretAccessKey string `json:"SecretAccessKey"`
159+ // SessionToken is the AWS Session token. This should be provided for
160+ // temporary AWS security credentials - Optional.
161+ SessionToken string `json:"Token"`
162+}
163+
164+func (o *Options) validate() error {
165+ if o.Audience == "" {
166+ return fmt.Errorf("externalaccount: Audience must be set")
167+ }
168+ if o.SubjectTokenType == "" {
169+ return fmt.Errorf("externalaccount: Subject token type must be set")
170+ }
171+ if o.WorkforcePoolUserProject != "" {
172+ if valid := validWorkforceAudiencePattern.MatchString(o.Audience); !valid {
173+ return fmt.Errorf("externalaccount: workforce_pool_user_project should not be set for non-workforce pool credentials")
174+ }
175+ }
176+ count := 0
177+ if o.CredentialSource != nil {
178+ count++
179+ }
180+ if o.SubjectTokenProvider != nil {
181+ count++
182+ }
183+ if o.AwsSecurityCredentialsProvider != nil {
184+ count++
185+ }
186+ if count == 0 {
187+ return fmt.Errorf("externalaccount: one of CredentialSource, SubjectTokenProvider, or AwsSecurityCredentialsProvider must be set")
188+ }
189+ if count > 1 {
190+ return fmt.Errorf("externalaccount: only one of CredentialSource, SubjectTokenProvider, or AwsSecurityCredentialsProvider must be set")
191+ }
192+ return nil
193+}
194+
195+// client returns the http client that should be used for the token exchange. If a non-default client
196+// is provided, then the client configured in the options will always be returned. If a default client
197+// is provided and the options are configured for X509 credentials, a new client will be created.
198+func (o *Options) client() (*http.Client, error) {
199+ // If a client was provided and no override certificate config location was provided, use the provided client.
200+ if o.CredentialSource == nil || o.CredentialSource.Certificate == nil || (!o.IsDefaultClient && o.CredentialSource.Certificate.CertificateConfigLocation == "") {
201+ return o.Client, nil
202+ }
203+
204+ // If a new client should be created, validate and use the certificate source to create a new mTLS client.
205+ cert := o.CredentialSource.Certificate
206+ if !cert.UseDefaultCertificateConfig && cert.CertificateConfigLocation == "" {
207+ return nil, errors.New("credentials: \"certificate\" object must either specify a certificate_config_location or use_default_certificate_config should be true")
208+ }
209+ if cert.UseDefaultCertificateConfig && cert.CertificateConfigLocation != "" {
210+ return nil, errors.New("credentials: \"certificate\" object cannot specify both a certificate_config_location and use_default_certificate_config=true")
211+ }
212+ return createX509Client(cert.CertificateConfigLocation)
213+}
214+
215+// resolveTokenURL sets the default STS token endpoint with the configured
216+// universe domain.
217+func (o *Options) resolveTokenURL() {
218+ if o.TokenURL != "" {
219+ return
220+ } else if o.UniverseDomain != "" {
221+ o.TokenURL = strings.Replace(defaultTokenURL, universeDomainPlaceholder, o.UniverseDomain, 1)
222+ } else {
223+ o.TokenURL = strings.Replace(defaultTokenURL, universeDomainPlaceholder, defaultUniverseDomain, 1)
224+ }
225+}
226+
227+// NewTokenProvider returns a [cloud.google.com/go/auth.TokenProvider]
228+// configured with the provided options.
229+func NewTokenProvider(opts *Options) (auth.TokenProvider, error) {
230+ if err := opts.validate(); err != nil {
231+ return nil, err
232+ }
233+ opts.resolveTokenURL()
234+ logger := internallog.New(opts.Logger)
235+ stp, err := newSubjectTokenProvider(opts)
236+ if err != nil {
237+ return nil, err
238+ }
239+
240+ client, err := opts.client()
241+ if err != nil {
242+ return nil, err
243+ }
244+
245+ tp := &tokenProvider{
246+ client: client,
247+ opts: opts,
248+ stp: stp,
249+ logger: logger,
250+ }
251+
252+ if opts.ServiceAccountImpersonationURL == "" {
253+ return auth.NewCachedTokenProvider(tp, nil), nil
254+ }
255+
256+ scopes := make([]string, len(opts.Scopes))
257+ copy(scopes, opts.Scopes)
258+ // needed for impersonation
259+ tp.opts.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
260+ imp, err := impersonate.NewTokenProvider(&impersonate.Options{
261+ Client: client,
262+ URL: opts.ServiceAccountImpersonationURL,
263+ Scopes: scopes,
264+ Tp: auth.NewCachedTokenProvider(tp, nil),
265+ TokenLifetimeSeconds: opts.ServiceAccountImpersonationLifetimeSeconds,
266+ Logger: logger,
267+ })
268+ if err != nil {
269+ return nil, err
270+ }
271+ return auth.NewCachedTokenProvider(imp, nil), nil
272+}
273+
274+type subjectTokenProvider interface {
275+ subjectToken(ctx context.Context) (string, error)
276+ providerType() string
277+}
278+
279+// tokenProvider is the provider that handles external credentials. It is used to retrieve Tokens.
280+type tokenProvider struct {
281+ client *http.Client
282+ logger *slog.Logger
283+ opts *Options
284+ stp subjectTokenProvider
285+}
286+
287+func (tp *tokenProvider) Token(ctx context.Context) (*auth.Token, error) {
288+ subjectToken, err := tp.stp.subjectToken(ctx)
289+ if err != nil {
290+ return nil, err
291+ }
292+
293+ stsRequest := &stsexchange.TokenRequest{
294+ GrantType: stsexchange.GrantType,
295+ Audience: tp.opts.Audience,
296+ Scope: tp.opts.Scopes,
297+ RequestedTokenType: stsexchange.TokenType,
298+ SubjectToken: subjectToken,
299+ SubjectTokenType: tp.opts.SubjectTokenType,
300+ }
301+ header := make(http.Header)
302+ header.Set("Content-Type", "application/x-www-form-urlencoded")
303+ header.Add("x-goog-api-client", getGoogHeaderValue(tp.opts, tp.stp))
304+ clientAuth := stsexchange.ClientAuthentication{
305+ AuthStyle: auth.StyleInHeader,
306+ ClientID: tp.opts.ClientID,
307+ ClientSecret: tp.opts.ClientSecret,
308+ }
309+ var options map[string]interface{}
310+ // Do not pass workforce_pool_user_project when client authentication is used.
311+ // The client ID is sufficient for determining the user project.
312+ if tp.opts.WorkforcePoolUserProject != "" && tp.opts.ClientID == "" {
313+ options = map[string]interface{}{
314+ "userProject": tp.opts.WorkforcePoolUserProject,
315+ }
316+ }
317+ stsResp, err := stsexchange.ExchangeToken(ctx, &stsexchange.Options{
318+ Client: tp.client,
319+ Endpoint: tp.opts.TokenURL,
320+ Request: stsRequest,
321+ Authentication: clientAuth,
322+ Headers: header,
323+ ExtraOpts: options,
324+ Logger: tp.logger,
325+ })
326+ if err != nil {
327+ return nil, err
328+ }
329+
330+ tok := &auth.Token{
331+ Value: stsResp.AccessToken,
332+ Type: stsResp.TokenType,
333+ }
334+ // The RFC8693 doesn't define the explicit 0 of "expires_in" field behavior.
335+ if stsResp.ExpiresIn <= 0 {
336+ return nil, fmt.Errorf("credentials: got invalid expiry from security token service")
337+ }
338+ tok.Expiry = Now().Add(time.Duration(stsResp.ExpiresIn) * time.Second)
339+ return tok, nil
340+}
341+
342+// newSubjectTokenProvider determines the type of credsfile.CredentialSource needed to create a
343+// subjectTokenProvider
344+func newSubjectTokenProvider(o *Options) (subjectTokenProvider, error) {
345+ logger := internallog.New(o.Logger)
346+ reqOpts := &RequestOptions{Audience: o.Audience, SubjectTokenType: o.SubjectTokenType}
347+ if o.AwsSecurityCredentialsProvider != nil {
348+ return &awsSubjectProvider{
349+ securityCredentialsProvider: o.AwsSecurityCredentialsProvider,
350+ TargetResource: o.Audience,
351+ reqOpts: reqOpts,
352+ logger: logger,
353+ }, nil
354+ } else if o.SubjectTokenProvider != nil {
355+ return &programmaticProvider{stp: o.SubjectTokenProvider, opts: reqOpts}, nil
356+ } else if len(o.CredentialSource.EnvironmentID) > 3 && o.CredentialSource.EnvironmentID[:3] == "aws" {
357+ if awsVersion, err := strconv.Atoi(o.CredentialSource.EnvironmentID[3:]); err == nil {
358+ if awsVersion != 1 {
359+ return nil, fmt.Errorf("credentials: aws version '%d' is not supported in the current build", awsVersion)
360+ }
361+
362+ awsProvider := &awsSubjectProvider{
363+ EnvironmentID: o.CredentialSource.EnvironmentID,
364+ RegionURL: o.CredentialSource.RegionURL,
365+ RegionalCredVerificationURL: o.CredentialSource.RegionalCredVerificationURL,
366+ CredVerificationURL: o.CredentialSource.URL,
367+ TargetResource: o.Audience,
368+ Client: o.Client,
369+ logger: logger,
370+ }
371+ if o.CredentialSource.IMDSv2SessionTokenURL != "" {
372+ awsProvider.IMDSv2SessionTokenURL = o.CredentialSource.IMDSv2SessionTokenURL
373+ }
374+
375+ return awsProvider, nil
376+ }
377+ } else if o.CredentialSource.File != "" {
378+ return &fileSubjectProvider{File: o.CredentialSource.File, Format: o.CredentialSource.Format}, nil
379+ } else if o.CredentialSource.URL != "" {
380+ return &urlSubjectProvider{
381+ URL: o.CredentialSource.URL,
382+ Headers: o.CredentialSource.Headers,
383+ Format: o.CredentialSource.Format,
384+ Client: o.Client,
385+ Logger: logger,
386+ }, nil
387+ } else if o.CredentialSource.Executable != nil {
388+ ec := o.CredentialSource.Executable
389+ if ec.Command == "" {
390+ return nil, errors.New("credentials: missing `command` field — executable command must be provided")
391+ }
392+
393+ execProvider := &executableSubjectProvider{}
394+ execProvider.Command = ec.Command
395+ if ec.TimeoutMillis == 0 {
396+ execProvider.Timeout = executableDefaultTimeout
397+ } else {
398+ execProvider.Timeout = time.Duration(ec.TimeoutMillis) * time.Millisecond
399+ if execProvider.Timeout < timeoutMinimum || execProvider.Timeout > timeoutMaximum {
400+ return nil, fmt.Errorf("credentials: invalid `timeout_millis` field — executable timeout must be between %v and %v seconds", timeoutMinimum.Seconds(), timeoutMaximum.Seconds())
401+ }
402+ }
403+ execProvider.OutputFile = ec.OutputFile
404+ execProvider.client = o.Client
405+ execProvider.opts = o
406+ execProvider.env = runtimeEnvironment{}
407+ return execProvider, nil
408+ } else if o.CredentialSource.Certificate != nil {
409+ cert := o.CredentialSource.Certificate
410+ if !cert.UseDefaultCertificateConfig && cert.CertificateConfigLocation == "" {
411+ return nil, errors.New("credentials: \"certificate\" object must either specify a certificate_config_location or use_default_certificate_config should be true")
412+ }
413+ if cert.UseDefaultCertificateConfig && cert.CertificateConfigLocation != "" {
414+ return nil, errors.New("credentials: \"certificate\" object cannot specify both a certificate_config_location and use_default_certificate_config=true")
415+ }
416+ return &x509Provider{}, nil
417+ }
418+ return nil, errors.New("credentials: unable to parse credential source")
419+}
420+
421+func getGoogHeaderValue(conf *Options, p subjectTokenProvider) string {
422+ return fmt.Sprintf("gl-go/%s auth/%s google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t",
423+ goVersion(),
424+ "unknown",
425+ p.providerType(),
426+ conf.ServiceAccountImpersonationURL != "",
427+ conf.ServiceAccountImpersonationLifetimeSeconds != 0)
428+}
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/file_provider.gonew file+78
@@ -0,0 +1,78 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package externalaccount
16+
17+import (
18+ "bytes"
19+ "context"
20+ "encoding/json"
21+ "errors"
22+ "fmt"
23+ "os"
24+
25+ "cloud.google.com/go/auth/internal"
26+ "cloud.google.com/go/auth/internal/credsfile"
27+)
28+
29+const (
30+ fileProviderType = "file"
31+)
32+
33+type fileSubjectProvider struct {
34+ File string
35+ Format *credsfile.Format
36+}
37+
38+func (sp *fileSubjectProvider) subjectToken(context.Context) (string, error) {
39+ tokenFile, err := os.Open(sp.File)
40+ if err != nil {
41+ return "", fmt.Errorf("credentials: failed to open credential file %q: %w", sp.File, err)
42+ }
43+ defer tokenFile.Close()
44+ tokenBytes, err := internal.ReadAll(tokenFile)
45+ if err != nil {
46+ return "", fmt.Errorf("credentials: failed to read credential file: %w", err)
47+ }
48+ tokenBytes = bytes.TrimSpace(tokenBytes)
49+
50+ if sp.Format == nil {
51+ return string(tokenBytes), nil
52+ }
53+ switch sp.Format.Type {
54+ case fileTypeJSON:
55+ jsonData := make(map[string]interface{})
56+ err = json.Unmarshal(tokenBytes, &jsonData)
57+ if err != nil {
58+ return "", fmt.Errorf("credentials: failed to unmarshal subject token file: %w", err)
59+ }
60+ val, ok := jsonData[sp.Format.SubjectTokenFieldName]
61+ if !ok {
62+ return "", errors.New("credentials: provided subject_token_field_name not found in credentials")
63+ }
64+ token, ok := val.(string)
65+ if !ok {
66+ return "", errors.New("credentials: improperly formatted subject token")
67+ }
68+ return token, nil
69+ case fileTypeText:
70+ return string(tokenBytes), nil
71+ default:
72+ return "", errors.New("credentials: invalid credential_source file format type: " + sp.Format.Type)
73+ }
74+}
75+
76+func (sp *fileSubjectProvider) providerType() string {
77+ return fileProviderType
78+}
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/info.gonew file+74
@@ -0,0 +1,74 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package externalaccount
16+
17+import (
18+ "runtime"
19+ "strings"
20+ "unicode"
21+)
22+
23+var (
24+ // version is a package internal global variable for testing purposes.
25+ version = runtime.Version
26+)
27+
28+// versionUnknown is only used when the runtime version cannot be determined.
29+const versionUnknown = "UNKNOWN"
30+
31+// goVersion returns a Go runtime version derived from the runtime environment
32+// that is modified to be suitable for reporting in a header, meaning it has no
33+// whitespace. If it is unable to determine the Go runtime version, it returns
34+// versionUnknown.
35+func goVersion() string {
36+ const develPrefix = "devel +"
37+
38+ s := version()
39+ if strings.HasPrefix(s, develPrefix) {
40+ s = s[len(develPrefix):]
41+ if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
42+ s = s[:p]
43+ }
44+ return s
45+ } else if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
46+ s = s[:p]
47+ }
48+
49+ notSemverRune := func(r rune) bool {
50+ return !strings.ContainsRune("0123456789.", r)
51+ }
52+
53+ if strings.HasPrefix(s, "go1") {
54+ s = s[2:]
55+ var prerelease string
56+ if p := strings.IndexFunc(s, notSemverRune); p >= 0 {
57+ s, prerelease = s[:p], s[p:]
58+ }
59+ if strings.HasSuffix(s, ".") {
60+ s += "0"
61+ } else if strings.Count(s, ".") < 2 {
62+ s += ".0"
63+ }
64+ if prerelease != "" {
65+ // Some release candidates already have a dash in them.
66+ if !strings.HasPrefix(prerelease, "-") {
67+ prerelease = "-" + prerelease
68+ }
69+ s += prerelease
70+ }
71+ return s
72+ }
73+ return versionUnknown
74+}
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/programmatic_provider.gonew file+30
@@ -0,0 +1,30 @@
1+// Copyright 2024 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package externalaccount
16+
17+import "context"
18+
19+type programmaticProvider struct {
20+ opts *RequestOptions
21+ stp SubjectTokenProvider
22+}
23+
24+func (pp *programmaticProvider) providerType() string {
25+ return programmaticProviderType
26+}
27+
28+func (pp *programmaticProvider) subjectToken(ctx context.Context) (string, error) {
29+ return pp.stp.SubjectToken(ctx, pp.opts)
30+}
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/url_provider.gonew file+93
@@ -0,0 +1,93 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package externalaccount
16+
17+import (
18+ "context"
19+ "encoding/json"
20+ "errors"
21+ "fmt"
22+ "log/slog"
23+ "net/http"
24+
25+ "cloud.google.com/go/auth/internal"
26+ "cloud.google.com/go/auth/internal/credsfile"
27+ "github.com/googleapis/gax-go/v2/internallog"
28+)
29+
30+const (
31+ fileTypeText = "text"
32+ fileTypeJSON = "json"
33+ urlProviderType = "url"
34+ programmaticProviderType = "programmatic"
35+ x509ProviderType = "x509"
36+)
37+
38+type urlSubjectProvider struct {
39+ URL string
40+ Headers map[string]string
41+ Format *credsfile.Format
42+ Client *http.Client
43+ Logger *slog.Logger
44+}
45+
46+func (sp *urlSubjectProvider) subjectToken(ctx context.Context) (string, error) {
47+ req, err := http.NewRequestWithContext(ctx, "GET", sp.URL, nil)
48+ if err != nil {
49+ return "", fmt.Errorf("credentials: HTTP request for URL-sourced credential failed: %w", err)
50+ }
51+
52+ for key, val := range sp.Headers {
53+ req.Header.Add(key, val)
54+ }
55+ sp.Logger.DebugContext(ctx, "url subject token request", "request", internallog.HTTPRequest(req, nil))
56+ resp, body, err := internal.DoRequest(sp.Client, req)
57+ if err != nil {
58+ return "", fmt.Errorf("credentials: invalid response when retrieving subject token: %w", err)
59+ }
60+ sp.Logger.DebugContext(ctx, "url subject token response", "response", internallog.HTTPResponse(resp, body))
61+ if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
62+ return "", fmt.Errorf("credentials: status code %d: %s", c, body)
63+ }
64+
65+ if sp.Format == nil {
66+ return string(body), nil
67+ }
68+ switch sp.Format.Type {
69+ case "json":
70+ jsonData := make(map[string]interface{})
71+ err = json.Unmarshal(body, &jsonData)
72+ if err != nil {
73+ return "", fmt.Errorf("credentials: failed to unmarshal subject token file: %w", err)
74+ }
75+ val, ok := jsonData[sp.Format.SubjectTokenFieldName]
76+ if !ok {
77+ return "", errors.New("credentials: provided subject_token_field_name not found in credentials")
78+ }
79+ token, ok := val.(string)
80+ if !ok {
81+ return "", errors.New("credentials: improperly formatted subject token")
82+ }
83+ return token, nil
84+ case fileTypeText:
85+ return string(body), nil
86+ default:
87+ return "", errors.New("credentials: invalid credential_source file format type: " + sp.Format.Type)
88+ }
89+}
90+
91+func (sp *urlSubjectProvider) providerType() string {
92+ return urlProviderType
93+}
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/x509_provider.gonew file+63
@@ -0,0 +1,63 @@
1+// Copyright 2024 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package externalaccount
16+
17+import (
18+ "context"
19+ "crypto/tls"
20+ "net/http"
21+ "time"
22+
23+ "cloud.google.com/go/auth/internal/transport/cert"
24+)
25+
26+// x509Provider implements the subjectTokenProvider type for
27+// x509 workload identity credentials. Because x509 credentials
28+// rely on an mTLS connection to represent the 3rd party identity
29+// rather than a subject token, this provider will always return
30+// an empty string when a subject token is requested by the external account
31+// token provider.
32+type x509Provider struct {
33+}
34+
35+func (xp *x509Provider) providerType() string {
36+ return x509ProviderType
37+}
38+
39+func (xp *x509Provider) subjectToken(ctx context.Context) (string, error) {
40+ return "", nil
41+}
42+
43+// createX509Client creates a new client that is configured with mTLS, using the
44+// certificate configuration specified in the credential source.
45+func createX509Client(certificateConfigLocation string) (*http.Client, error) {
46+ certProvider, err := cert.NewWorkloadX509CertProvider(certificateConfigLocation)
47+ if err != nil {
48+ return nil, err
49+ }
50+ trans := http.DefaultTransport.(*http.Transport).Clone()
51+
52+ trans.TLSClientConfig = &tls.Config{
53+ GetClientCertificate: certProvider,
54+ }
55+
56+ // Create a client with default settings plus the X509 workload cert and key.
57+ client := &http.Client{
58+ Transport: trans,
59+ Timeout: 30 * time.Second,
60+ }
61+
62+ return client, nil
63+}
vendor/cloud.google.com/go/auth/credentials/internal/externalaccountuser/externalaccountuser.gonew file+115
@@ -0,0 +1,115 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package externalaccountuser
16+
17+import (
18+ "context"
19+ "errors"
20+ "log/slog"
21+ "net/http"
22+ "time"
23+
24+ "cloud.google.com/go/auth"
25+ "cloud.google.com/go/auth/credentials/internal/stsexchange"
26+ "cloud.google.com/go/auth/internal"
27+ "github.com/googleapis/gax-go/v2/internallog"
28+)
29+
30+// Options stores the configuration for fetching tokens with external authorized
31+// user credentials.
32+type Options struct {
33+ // Audience is the Secure Token Service (STS) audience which contains the
34+ // resource name for the workforce pool and the provider identifier in that
35+ // pool.
36+ Audience string
37+ // RefreshToken is the OAuth 2.0 refresh token.
38+ RefreshToken string
39+ // TokenURL is the STS token exchange endpoint for refresh.
40+ TokenURL string
41+ // TokenInfoURL is the STS endpoint URL for token introspection. Optional.
42+ TokenInfoURL string
43+ // ClientID is only required in conjunction with ClientSecret, as described
44+ // below.
45+ ClientID string
46+ // ClientSecret is currently only required if token_info endpoint also needs
47+ // to be called with the generated a cloud access token. When provided, STS
48+ // will be called with additional basic authentication using client_id as
49+ // username and client_secret as password.
50+ ClientSecret string
51+ // Scopes contains the desired scopes for the returned access token.
52+ Scopes []string
53+
54+ // Client for token request.
55+ Client *http.Client
56+ // Logger for logging.
57+ Logger *slog.Logger
58+}
59+
60+func (c *Options) validate() bool {
61+ return c.ClientID != "" && c.ClientSecret != "" && c.RefreshToken != "" && c.TokenURL != ""
62+}
63+
64+// NewTokenProvider returns a [cloud.google.com/go/auth.TokenProvider]
65+// configured with the provided options.
66+func NewTokenProvider(opts *Options) (auth.TokenProvider, error) {
67+ if !opts.validate() {
68+ return nil, errors.New("credentials: invalid external_account_authorized_user configuration")
69+ }
70+
71+ tp := &tokenProvider{
72+ o: opts,
73+ }
74+ return auth.NewCachedTokenProvider(tp, nil), nil
75+}
76+
77+type tokenProvider struct {
78+ o *Options
79+}
80+
81+func (tp *tokenProvider) Token(ctx context.Context) (*auth.Token, error) {
82+ opts := tp.o
83+
84+ clientAuth := stsexchange.ClientAuthentication{
85+ AuthStyle: auth.StyleInHeader,
86+ ClientID: opts.ClientID,
87+ ClientSecret: opts.ClientSecret,
88+ }
89+ headers := make(http.Header)
90+ headers.Set("Content-Type", "application/x-www-form-urlencoded")
91+ stsResponse, err := stsexchange.RefreshAccessToken(ctx, &stsexchange.Options{
92+ Client: opts.Client,
93+ Endpoint: opts.TokenURL,
94+ RefreshToken: opts.RefreshToken,
95+ Authentication: clientAuth,
96+ Headers: headers,
97+ Logger: internallog.New(tp.o.Logger),
98+ })
99+ if err != nil {
100+ return nil, err
101+ }
102+ if stsResponse.ExpiresIn < 0 {
103+ return nil, errors.New("credentials: invalid expiry from security token service")
104+ }
105+
106+ // guarded by the wrapping with CachedTokenProvider
107+ if stsResponse.RefreshToken != "" {
108+ opts.RefreshToken = stsResponse.RefreshToken
109+ }
110+ return &auth.Token{
111+ Value: stsResponse.AccessToken,
112+ Expiry: time.Now().UTC().Add(time.Duration(stsResponse.ExpiresIn) * time.Second),
113+ Type: internal.TokenTypeBearer,
114+ }, nil
115+}
vendor/cloud.google.com/go/auth/credentials/internal/gdch/gdch.gonew file+191
@@ -0,0 +1,191 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package gdch
16+
17+import (
18+ "context"
19+ "crypto"
20+ "crypto/tls"
21+ "crypto/x509"
22+ "encoding/json"
23+ "errors"
24+ "fmt"
25+ "log/slog"
26+ "net/http"
27+ "net/url"
28+ "os"
29+ "strings"
30+ "time"
31+
32+ "cloud.google.com/go/auth"
33+ "cloud.google.com/go/auth/internal"
34+ "cloud.google.com/go/auth/internal/credsfile"
35+ "cloud.google.com/go/auth/internal/jwt"
36+ "github.com/googleapis/gax-go/v2/internallog"
37+)
38+
39+const (
40+ // GrantType is the grant type for the token request.
41+ GrantType = "urn:ietf:params:oauth:token-type:token-exchange"
42+ requestTokenType = "urn:ietf:params:oauth:token-type:access_token"
43+ subjectTokenType = "urn:k8s:params:oauth:token-type:serviceaccount"
44+)
45+
46+var (
47+ gdchSupportFormatVersions map[string]bool = map[string]bool{
48+ "1": true,
49+ }
50+)
51+
52+// Options for [NewTokenProvider].
53+type Options struct {
54+ STSAudience string
55+ Client *http.Client
56+ Logger *slog.Logger
57+}
58+
59+// NewTokenProvider returns a [cloud.google.com/go/auth.TokenProvider] from a
60+// GDCH cred file.
61+func NewTokenProvider(f *credsfile.GDCHServiceAccountFile, o *Options) (auth.TokenProvider, error) {
62+ if !gdchSupportFormatVersions[f.FormatVersion] {
63+ return nil, fmt.Errorf("credentials: unsupported gdch_service_account format %q", f.FormatVersion)
64+ }
65+ if o.STSAudience == "" {
66+ return nil, errors.New("credentials: STSAudience must be set for the GDCH auth flows")
67+ }
68+ signer, err := internal.ParseKey([]byte(f.PrivateKey))
69+ if err != nil {
70+ return nil, err
71+ }
72+ certPool, err := loadCertPool(f.CertPath)
73+ if err != nil {
74+ return nil, err
75+ }
76+
77+ tp := gdchProvider{
78+ serviceIdentity: fmt.Sprintf("system:serviceaccount:%s:%s", f.Project, f.Name),
79+ tokenURL: f.TokenURL,
80+ aud: o.STSAudience,
81+ signer: signer,
82+ pkID: f.PrivateKeyID,
83+ certPool: certPool,
84+ client: o.Client,
85+ logger: internallog.New(o.Logger),
86+ }
87+ return tp, nil
88+}
89+
90+func loadCertPool(path string) (*x509.CertPool, error) {
91+ pool := x509.NewCertPool()
92+ pem, err := os.ReadFile(path)
93+ if err != nil {
94+ return nil, fmt.Errorf("credentials: failed to read certificate: %w", err)
95+ }
96+ pool.AppendCertsFromPEM(pem)
97+ return pool, nil
98+}
99+
100+type gdchProvider struct {
101+ serviceIdentity string
102+ tokenURL string
103+ aud string
104+ signer crypto.Signer
105+ pkID string
106+ certPool *x509.CertPool
107+
108+ client *http.Client
109+ logger *slog.Logger
110+}
111+
112+func (g gdchProvider) Token(ctx context.Context) (*auth.Token, error) {
113+ addCertToTransport(g.client, g.certPool)
114+ iat := time.Now()
115+ exp := iat.Add(time.Hour)
116+ claims := jwt.Claims{
117+ Iss: g.serviceIdentity,
118+ Sub: g.serviceIdentity,
119+ Aud: g.tokenURL,
120+ Iat: iat.Unix(),
121+ Exp: exp.Unix(),
122+ }
123+ h := jwt.Header{
124+ Algorithm: jwt.HeaderAlgRSA256,
125+ Type: jwt.HeaderType,
126+ KeyID: string(g.pkID),
127+ }
128+ payload, err := jwt.EncodeJWS(&h, &claims, g.signer)
129+ if err != nil {
130+ return nil, err
131+ }
132+ v := url.Values{}
133+ v.Set("grant_type", GrantType)
134+ v.Set("audience", g.aud)
135+ v.Set("requested_token_type", requestTokenType)
136+ v.Set("subject_token", payload)
137+ v.Set("subject_token_type", subjectTokenType)
138+
139+ req, err := http.NewRequestWithContext(ctx, "POST", g.tokenURL, strings.NewReader(v.Encode()))
140+ if err != nil {
141+ return nil, err
142+ }
143+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
144+ g.logger.DebugContext(ctx, "gdch token request", "request", internallog.HTTPRequest(req, []byte(v.Encode())))
145+ resp, body, err := internal.DoRequest(g.client, req)
146+ if err != nil {
147+ return nil, fmt.Errorf("credentials: cannot fetch token: %w", err)
148+ }
149+ g.logger.DebugContext(ctx, "gdch token response", "response", internallog.HTTPResponse(resp, body))
150+ if c := resp.StatusCode; c < http.StatusOK || c > http.StatusMultipleChoices {
151+ return nil, &auth.Error{
152+ Response: resp,
153+ Body: body,
154+ }
155+ }
156+
157+ var tokenRes struct {
158+ AccessToken string `json:"access_token"`
159+ TokenType string `json:"token_type"`
160+ ExpiresIn int64 `json:"expires_in"` // relative seconds from now
161+ }
162+ if err := json.Unmarshal(body, &tokenRes); err != nil {
163+ return nil, fmt.Errorf("credentials: cannot fetch token: %w", err)
164+ }
165+ token := &auth.Token{
166+ Value: tokenRes.AccessToken,
167+ Type: tokenRes.TokenType,
168+ }
169+ raw := make(map[string]interface{})
170+ json.Unmarshal(body, &raw) // no error checks for optional fields
171+ token.Metadata = raw
172+
173+ if secs := tokenRes.ExpiresIn; secs > 0 {
174+ token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
175+ }
176+ return token, nil
177+}
178+
179+// addCertToTransport makes a best effort attempt at adding in the cert info to
180+// the client. It tries to keep all configured transport settings if the
181+// underlying transport is an http.Transport. Or else it overwrites the
182+// transport with defaults adding in the certs.
183+func addCertToTransport(hc *http.Client, certPool *x509.CertPool) {
184+ trans, ok := hc.Transport.(*http.Transport)
185+ if !ok {
186+ trans = http.DefaultTransport.(*http.Transport).Clone()
187+ }
188+ trans.TLSClientConfig = &tls.Config{
189+ RootCAs: certPool,
190+ }
191+}
vendor/cloud.google.com/go/auth/credentials/internal/impersonate/impersonate.gonew file+156
@@ -0,0 +1,156 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package impersonate
16+
17+import (
18+ "bytes"
19+ "context"
20+ "encoding/json"
21+ "errors"
22+ "fmt"
23+ "log/slog"
24+ "net/http"
25+ "time"
26+
27+ "cloud.google.com/go/auth"
28+ "cloud.google.com/go/auth/internal"
29+ "github.com/googleapis/gax-go/v2/internallog"
30+)
31+
32+const (
33+ defaultTokenLifetime = "3600s"
34+ authHeaderKey = "Authorization"
35+)
36+
37+// generateAccesstokenReq is used for service account impersonation
38+type generateAccessTokenReq struct {
39+ Delegates []string `json:"delegates,omitempty"`
40+ Lifetime string `json:"lifetime,omitempty"`
41+ Scope []string `json:"scope,omitempty"`
42+}
43+
44+type impersonateTokenResponse struct {
45+ AccessToken string `json:"accessToken"`
46+ ExpireTime string `json:"expireTime"`
47+}
48+
49+// NewTokenProvider uses a source credential, stored in Ts, to request an access token to the provided URL.
50+// Scopes can be defined when the access token is requested.
51+func NewTokenProvider(opts *Options) (auth.TokenProvider, error) {
52+ if err := opts.validate(); err != nil {
53+ return nil, err
54+ }
55+ return opts, nil
56+}
57+
58+// Options for [NewTokenProvider].
59+type Options struct {
60+ // Tp is the source credential used to generate a token on the
61+ // impersonated service account. Required.
62+ Tp auth.TokenProvider
63+
64+ // URL is the endpoint to call to generate a token
65+ // on behalf of the service account. Required.
66+ URL string
67+ // Scopes that the impersonated credential should have. Required.
68+ Scopes []string
69+ // Delegates are the service account email addresses in a delegation chain.
70+ // Each service account must be granted roles/iam.serviceAccountTokenCreator
71+ // on the next service account in the chain. Optional.
72+ Delegates []string
73+ // TokenLifetimeSeconds is the number of seconds the impersonation token will
74+ // be valid for. Defaults to 1 hour if unset. Optional.
75+ TokenLifetimeSeconds int
76+ // Client configures the underlying client used to make network requests
77+ // when fetching tokens. Required.
78+ Client *http.Client
79+ // Logger is used for debug logging. If provided, logging will be enabled
80+ // at the loggers configured level. By default logging is disabled unless
81+ // enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
82+ // logger will be used. Optional.
83+ Logger *slog.Logger
84+}
85+
86+func (o *Options) validate() error {
87+ if o.Tp == nil {
88+ return errors.New("credentials: missing required 'source_credentials' field in impersonated credentials")
89+ }
90+ if o.URL == "" {
91+ return errors.New("credentials: missing required 'service_account_impersonation_url' field in impersonated credentials")
92+ }
93+ return nil
94+}
95+
96+// Token performs the exchange to get a temporary service account token to allow access to GCP.
97+func (o *Options) Token(ctx context.Context) (*auth.Token, error) {
98+ logger := internallog.New(o.Logger)
99+ lifetime := defaultTokenLifetime
100+ if o.TokenLifetimeSeconds != 0 {
101+ lifetime = fmt.Sprintf("%ds", o.TokenLifetimeSeconds)
102+ }
103+ reqBody := generateAccessTokenReq{
104+ Lifetime: lifetime,
105+ Scope: o.Scopes,
106+ Delegates: o.Delegates,
107+ }
108+ b, err := json.Marshal(reqBody)
109+ if err != nil {
110+ return nil, fmt.Errorf("credentials: unable to marshal request: %w", err)
111+ }
112+ req, err := http.NewRequestWithContext(ctx, "POST", o.URL, bytes.NewReader(b))
113+ if err != nil {
114+ return nil, fmt.Errorf("credentials: unable to create impersonation request: %w", err)
115+ }
116+ req.Header.Set("Content-Type", "application/json")
117+ if err := setAuthHeader(ctx, o.Tp, req); err != nil {
118+ return nil, err
119+ }
120+ logger.DebugContext(ctx, "impersonated token request", "request", internallog.HTTPRequest(req, b))
121+ resp, body, err := internal.DoRequest(o.Client, req)
122+ if err != nil {
123+ return nil, fmt.Errorf("credentials: unable to generate access token: %w", err)
124+ }
125+ logger.DebugContext(ctx, "impersonated token response", "response", internallog.HTTPResponse(resp, body))
126+ if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
127+ return nil, fmt.Errorf("credentials: status code %d: %s", c, body)
128+ }
129+
130+ var accessTokenResp impersonateTokenResponse
131+ if err := json.Unmarshal(body, &accessTokenResp); err != nil {
132+ return nil, fmt.Errorf("credentials: unable to parse response: %w", err)
133+ }
134+ expiry, err := time.Parse(time.RFC3339, accessTokenResp.ExpireTime)
135+ if err != nil {
136+ return nil, fmt.Errorf("credentials: unable to parse expiry: %w", err)
137+ }
138+ return &auth.Token{
139+ Value: accessTokenResp.AccessToken,
140+ Expiry: expiry,
141+ Type: internal.TokenTypeBearer,
142+ }, nil
143+}
144+
145+func setAuthHeader(ctx context.Context, tp auth.TokenProvider, r *http.Request) error {
146+ t, err := tp.Token(ctx)
147+ if err != nil {
148+ return err
149+ }
150+ typ := t.Type
151+ if typ == "" {
152+ typ = internal.TokenTypeBearer
153+ }
154+ r.Header.Set(authHeaderKey, typ+" "+t.Value)
155+ return nil
156+}
vendor/cloud.google.com/go/auth/credentials/internal/stsexchange/sts_exchange.gonew file+167
@@ -0,0 +1,167 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package stsexchange
16+
17+import (
18+ "context"
19+ "encoding/base64"
20+ "encoding/json"
21+ "fmt"
22+ "log/slog"
23+ "net/http"
24+ "net/url"
25+ "strconv"
26+ "strings"
27+
28+ "cloud.google.com/go/auth"
29+ "cloud.google.com/go/auth/internal"
30+ "github.com/googleapis/gax-go/v2/internallog"
31+)
32+
33+const (
34+ // GrantType for a sts exchange.
35+ GrantType = "urn:ietf:params:oauth:grant-type:token-exchange"
36+ // TokenType for a sts exchange.
37+ TokenType = "urn:ietf:params:oauth:token-type:access_token"
38+
39+ jwtTokenType = "urn:ietf:params:oauth:token-type:jwt"
40+)
41+
42+// Options stores the configuration for making an sts exchange request.
43+type Options struct {
44+ Client *http.Client
45+ Logger *slog.Logger
46+ Endpoint string
47+ Request *TokenRequest
48+ Authentication ClientAuthentication
49+ Headers http.Header
50+ // ExtraOpts are optional fields marshalled into the `options` field of the
51+ // request body.
52+ ExtraOpts map[string]interface{}
53+ RefreshToken string
54+}
55+
56+// RefreshAccessToken performs the token exchange using a refresh token flow.
57+func RefreshAccessToken(ctx context.Context, opts *Options) (*TokenResponse, error) {
58+ data := url.Values{}
59+ data.Set("grant_type", "refresh_token")
60+ data.Set("refresh_token", opts.RefreshToken)
61+ return doRequest(ctx, opts, data)
62+}
63+
64+// ExchangeToken performs an oauth2 token exchange with the provided endpoint.
65+func ExchangeToken(ctx context.Context, opts *Options) (*TokenResponse, error) {
66+ data := url.Values{}
67+ data.Set("audience", opts.Request.Audience)
68+ data.Set("grant_type", GrantType)
69+ data.Set("requested_token_type", TokenType)
70+ data.Set("subject_token_type", opts.Request.SubjectTokenType)
71+ data.Set("subject_token", opts.Request.SubjectToken)
72+ data.Set("scope", strings.Join(opts.Request.Scope, " "))
73+ if opts.ExtraOpts != nil {
74+ opts, err := json.Marshal(opts.ExtraOpts)
75+ if err != nil {
76+ return nil, fmt.Errorf("credentials: failed to marshal additional options: %w", err)
77+ }
78+ data.Set("options", string(opts))
79+ }
80+ return doRequest(ctx, opts, data)
81+}
82+
83+func doRequest(ctx context.Context, opts *Options, data url.Values) (*TokenResponse, error) {
84+ opts.Authentication.InjectAuthentication(data, opts.Headers)
85+ encodedData := data.Encode()
86+ logger := internallog.New(opts.Logger)
87+
88+ req, err := http.NewRequestWithContext(ctx, "POST", opts.Endpoint, strings.NewReader(encodedData))
89+ if err != nil {
90+ return nil, fmt.Errorf("credentials: failed to properly build http request: %w", err)
91+
92+ }
93+ for key, list := range opts.Headers {
94+ for _, val := range list {
95+ req.Header.Add(key, val)
96+ }
97+ }
98+ req.Header.Set("Content-Length", strconv.Itoa(len(encodedData)))
99+
100+ logger.DebugContext(ctx, "sts token request", "request", internallog.HTTPRequest(req, []byte(encodedData)))
101+ resp, body, err := internal.DoRequest(opts.Client, req)
102+ if err != nil {
103+ return nil, fmt.Errorf("credentials: invalid response from Secure Token Server: %w", err)
104+ }
105+ logger.DebugContext(ctx, "sts token response", "response", internallog.HTTPResponse(resp, body))
106+ if c := resp.StatusCode; c < http.StatusOK || c > http.StatusMultipleChoices {
107+ return nil, fmt.Errorf("credentials: status code %d: %s", c, body)
108+ }
109+ var stsResp TokenResponse
110+ if err := json.Unmarshal(body, &stsResp); err != nil {
111+ return nil, fmt.Errorf("credentials: failed to unmarshal response body from Secure Token Server: %w", err)
112+ }
113+
114+ return &stsResp, nil
115+}
116+
117+// TokenRequest contains fields necessary to make an oauth2 token
118+// exchange.
119+type TokenRequest struct {
120+ ActingParty struct {
121+ ActorToken string
122+ ActorTokenType string
123+ }
124+ GrantType string
125+ Resource string
126+ Audience string
127+ Scope []string
128+ RequestedTokenType string
129+ SubjectToken string
130+ SubjectTokenType string
131+}
132+
133+// TokenResponse is used to decode the remote server response during
134+// an oauth2 token exchange.
135+type TokenResponse struct {
136+ AccessToken string `json:"access_token"`
137+ IssuedTokenType string `json:"issued_token_type"`
138+ TokenType string `json:"token_type"`
139+ ExpiresIn int `json:"expires_in"`
140+ Scope string `json:"scope"`
141+ RefreshToken string `json:"refresh_token"`
142+}
143+
144+// ClientAuthentication represents an OAuth client ID and secret and the
145+// mechanism for passing these credentials as stated in rfc6749#2.3.1.
146+type ClientAuthentication struct {
147+ AuthStyle auth.Style
148+ ClientID string
149+ ClientSecret string
150+}
151+
152+// InjectAuthentication is used to add authentication to a Secure Token Service
153+// exchange request. It modifies either the passed url.Values or http.Header
154+// depending on the desired authentication format.
155+func (c *ClientAuthentication) InjectAuthentication(values url.Values, headers http.Header) {
156+ if c.ClientID == "" || c.ClientSecret == "" || values == nil || headers == nil {
157+ return
158+ }
159+ switch c.AuthStyle {
160+ case auth.StyleInHeader:
161+ plainHeader := c.ClientID + ":" + c.ClientSecret
162+ headers.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(plainHeader)))
163+ default:
164+ values.Set("client_id", c.ClientID)
165+ values.Set("client_secret", c.ClientSecret)
166+ }
167+}
vendor/cloud.google.com/go/auth/credentials/selfsignedjwt.gonew file+89
@@ -0,0 +1,89 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package credentials
16+
17+import (
18+ "context"
19+ "crypto"
20+ "errors"
21+ "fmt"
22+ "log/slog"
23+ "strings"
24+ "time"
25+
26+ "cloud.google.com/go/auth"
27+ "cloud.google.com/go/auth/internal"
28+ "cloud.google.com/go/auth/internal/credsfile"
29+ "cloud.google.com/go/auth/internal/jwt"
30+)
31+
32+var (
33+ // for testing
34+ now func() time.Time = time.Now
35+)
36+
37+// configureSelfSignedJWT uses the private key in the service account to create
38+// a JWT without making a network call.
39+func configureSelfSignedJWT(f *credsfile.ServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
40+ if len(opts.scopes()) == 0 && opts.Audience == "" {
41+ return nil, errors.New("credentials: both scopes and audience are empty")
42+ }
43+ signer, err := internal.ParseKey([]byte(f.PrivateKey))
44+ if err != nil {
45+ return nil, fmt.Errorf("credentials: could not parse key: %w", err)
46+ }
47+ return &selfSignedTokenProvider{
48+ email: f.ClientEmail,
49+ audience: opts.Audience,
50+ scopes: opts.scopes(),
51+ signer: signer,
52+ pkID: f.PrivateKeyID,
53+ logger: opts.logger(),
54+ }, nil
55+}
56+
57+type selfSignedTokenProvider struct {
58+ email string
59+ audience string
60+ scopes []string
61+ signer crypto.Signer
62+ pkID string
63+ logger *slog.Logger
64+}
65+
66+func (tp *selfSignedTokenProvider) Token(context.Context) (*auth.Token, error) {
67+ iat := now()
68+ exp := iat.Add(time.Hour)
69+ scope := strings.Join(tp.scopes, " ")
70+ c := &jwt.Claims{
71+ Iss: tp.email,
72+ Sub: tp.email,
73+ Aud: tp.audience,
74+ Scope: scope,
75+ Iat: iat.Unix(),
76+ Exp: exp.Unix(),
77+ }
78+ h := &jwt.Header{
79+ Algorithm: jwt.HeaderAlgRSA256,
80+ Type: jwt.HeaderType,
81+ KeyID: string(tp.pkID),
82+ }
83+ tok, err := jwt.EncodeJWS(h, c, tp.signer)
84+ if err != nil {
85+ return nil, fmt.Errorf("credentials: could not encode JWT: %w", err)
86+ }
87+ tp.logger.Debug("created self-signed JWT", "token", tok)
88+ return &auth.Token{Value: tok, Type: internal.TokenTypeBearer, Expiry: exp}, nil
89+}
vendor/cloud.google.com/go/auth/httptransport/httptransport.gonew file+247
@@ -0,0 +1,247 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+// Package httptransport provides functionality for managing HTTP client
16+// connections to Google Cloud services.
17+package httptransport
18+
19+import (
20+ "crypto/tls"
21+ "errors"
22+ "fmt"
23+ "log/slog"
24+ "net/http"
25+
26+ "cloud.google.com/go/auth"
27+ detect "cloud.google.com/go/auth/credentials"
28+ "cloud.google.com/go/auth/internal"
29+ "cloud.google.com/go/auth/internal/transport"
30+ "github.com/googleapis/gax-go/v2/internallog"
31+)
32+
33+// ClientCertProvider is a function that returns a TLS client certificate to be
34+// used when opening TLS connections. It follows the same semantics as
35+// [crypto/tls.Config.GetClientCertificate].
36+type ClientCertProvider = func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
37+
38+// Options used to configure a [net/http.Client] from [NewClient].
39+type Options struct {
40+ // DisableTelemetry disables default telemetry (OpenTelemetry). An example
41+ // reason to do so would be to bind custom telemetry that overrides the
42+ // defaults.
43+ DisableTelemetry bool
44+ // DisableAuthentication specifies that no authentication should be used. It
45+ // is suitable only for testing and for accessing public resources, like
46+ // public Google Cloud Storage buckets.
47+ DisableAuthentication bool
48+ // Headers are extra HTTP headers that will be appended to every outgoing
49+ // request.
50+ Headers http.Header
51+ // BaseRoundTripper overrides the base transport used for serving requests.
52+ // If specified ClientCertProvider is ignored.
53+ BaseRoundTripper http.RoundTripper
54+ // Endpoint overrides the default endpoint to be used for a service.
55+ Endpoint string
56+ // APIKey specifies an API key to be used as the basis for authentication.
57+ // If set DetectOpts are ignored.
58+ APIKey string
59+ // Credentials used to add Authorization header to all requests. If set
60+ // DetectOpts are ignored.
61+ Credentials *auth.Credentials
62+ // ClientCertProvider is a function that returns a TLS client certificate to
63+ // be used when opening TLS connections. It follows the same semantics as
64+ // crypto/tls.Config.GetClientCertificate.
65+ ClientCertProvider ClientCertProvider
66+ // DetectOpts configures settings for detect Application Default
67+ // Credentials.
68+ DetectOpts *detect.DetectOptions
69+ // UniverseDomain is the default service domain for a given Cloud universe.
70+ // The default value is "googleapis.com". This is the universe domain
71+ // configured for the client, which will be compared to the universe domain
72+ // that is separately configured for the credentials.
73+ UniverseDomain string
74+ // Logger is used for debug logging. If provided, logging will be enabled
75+ // at the loggers configured level. By default logging is disabled unless
76+ // enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
77+ // logger will be used. Optional.
78+ Logger *slog.Logger
79+
80+ // InternalOptions are NOT meant to be set directly by consumers of this
81+ // package, they should only be set by generated client code.
82+ InternalOptions *InternalOptions
83+}
84+
85+func (o *Options) validate() error {
86+ if o == nil {
87+ return errors.New("httptransport: opts required to be non-nil")
88+ }
89+ if o.InternalOptions != nil && o.InternalOptions.SkipValidation {
90+ return nil
91+ }
92+ hasCreds := o.APIKey != "" ||
93+ o.Credentials != nil ||
94+ (o.DetectOpts != nil && len(o.DetectOpts.CredentialsJSON) > 0) ||
95+ (o.DetectOpts != nil && o.DetectOpts.CredentialsFile != "")
96+ if o.DisableAuthentication && hasCreds {
97+ return errors.New("httptransport: DisableAuthentication is incompatible with options that set or detect credentials")
98+ }
99+ return nil
100+}
101+
102+// client returns the client a user set for the detect options or nil if one was
103+// not set.
104+func (o *Options) client() *http.Client {
105+ if o.DetectOpts != nil && o.DetectOpts.Client != nil {
106+ return o.DetectOpts.Client
107+ }
108+ return nil
109+}
110+
111+func (o *Options) logger() *slog.Logger {
112+ return internallog.New(o.Logger)
113+}
114+
115+func (o *Options) resolveDetectOptions() *detect.DetectOptions {
116+ io := o.InternalOptions
117+ // soft-clone these so we are not updating a ref the user holds and may reuse
118+ do := transport.CloneDetectOptions(o.DetectOpts)
119+
120+ // If scoped JWTs are enabled user provided an aud, allow self-signed JWT.
121+ if (io != nil && io.EnableJWTWithScope) || do.Audience != "" {
122+ do.UseSelfSignedJWT = true
123+ }
124+ // Only default scopes if user did not also set an audience.
125+ if len(do.Scopes) == 0 && do.Audience == "" && io != nil && len(io.DefaultScopes) > 0 {
126+ do.Scopes = make([]string, len(io.DefaultScopes))
127+ copy(do.Scopes, io.DefaultScopes)
128+ }
129+ if len(do.Scopes) == 0 && do.Audience == "" && io != nil {
130+ do.Audience = o.InternalOptions.DefaultAudience
131+ }
132+ if o.ClientCertProvider != nil {
133+ tlsConfig := &tls.Config{
134+ GetClientCertificate: o.ClientCertProvider,
135+ }
136+ do.Client = transport.DefaultHTTPClientWithTLS(tlsConfig)
137+ do.TokenURL = detect.GoogleMTLSTokenURL
138+ }
139+ if do.Logger == nil {
140+ do.Logger = o.logger()
141+ }
142+ return do
143+}
144+
145+// InternalOptions are only meant to be set by generated client code. These are
146+// not meant to be set directly by consumers of this package. Configuration in
147+// this type is considered EXPERIMENTAL and may be removed at any time in the
148+// future without warning.
149+type InternalOptions struct {
150+ // EnableJWTWithScope specifies if scope can be used with self-signed JWT.
151+ EnableJWTWithScope bool
152+ // DefaultAudience specifies a default audience to be used as the audience
153+ // field ("aud") for the JWT token authentication.
154+ DefaultAudience string
155+ // DefaultEndpointTemplate combined with UniverseDomain specifies the
156+ // default endpoint.
157+ DefaultEndpointTemplate string
158+ // DefaultMTLSEndpoint specifies the default mTLS endpoint.
159+ DefaultMTLSEndpoint string
160+ // DefaultScopes specifies the default OAuth2 scopes to be used for a
161+ // service.
162+ DefaultScopes []string
163+ // SkipValidation bypasses validation on Options. It should only be used
164+ // internally for clients that need more control over their transport.
165+ SkipValidation bool
166+ // SkipUniverseDomainValidation skips the verification that the universe
167+ // domain configured for the client matches the universe domain configured
168+ // for the credentials. It should only be used internally for clients that
169+ // need more control over their transport. The default is false.
170+ SkipUniverseDomainValidation bool
171+}
172+
173+// AddAuthorizationMiddleware adds a middleware to the provided client's
174+// transport that sets the Authorization header with the value produced by the
175+// provided [cloud.google.com/go/auth.Credentials]. An error is returned only
176+// if client or creds is nil.
177+//
178+// This function does not support setting a universe domain value on the client.
179+func AddAuthorizationMiddleware(client *http.Client, creds *auth.Credentials) error {
180+ if client == nil || creds == nil {
181+ return fmt.Errorf("httptransport: client and tp must not be nil")
182+ }
183+ base := client.Transport
184+ if base == nil {
185+ if dt, ok := http.DefaultTransport.(*http.Transport); ok {
186+ base = dt.Clone()
187+ } else {
188+ // Directly reuse the DefaultTransport if the application has
189+ // replaced it with an implementation of RoundTripper other than
190+ // http.Transport.
191+ base = http.DefaultTransport
192+ }
193+ }
194+ client.Transport = &authTransport{
195+ creds: creds,
196+ base: base,
197+ }
198+ return nil
199+}
200+
201+// NewClient returns a [net/http.Client] that can be used to communicate with a
202+// Google cloud service, configured with the provided [Options]. It
203+// automatically appends Authorization headers to all outgoing requests.
204+func NewClient(opts *Options) (*http.Client, error) {
205+ if err := opts.validate(); err != nil {
206+ return nil, err
207+ }
208+
209+ tOpts := &transport.Options{
210+ Endpoint: opts.Endpoint,
211+ ClientCertProvider: opts.ClientCertProvider,
212+ Client: opts.client(),
213+ UniverseDomain: opts.UniverseDomain,
214+ Logger: opts.logger(),
215+ }
216+ if io := opts.InternalOptions; io != nil {
217+ tOpts.DefaultEndpointTemplate = io.DefaultEndpointTemplate
218+ tOpts.DefaultMTLSEndpoint = io.DefaultMTLSEndpoint
219+ }
220+ clientCertProvider, dialTLSContext, err := transport.GetHTTPTransportConfig(tOpts)
221+ if err != nil {
222+ return nil, err
223+ }
224+ baseRoundTripper := opts.BaseRoundTripper
225+ if baseRoundTripper == nil {
226+ baseRoundTripper = defaultBaseTransport(clientCertProvider, dialTLSContext)
227+ }
228+ // Ensure the token exchange transport uses the same ClientCertProvider as the API transport.
229+ opts.ClientCertProvider = clientCertProvider
230+ trans, err := newTransport(baseRoundTripper, opts)
231+ if err != nil {
232+ return nil, err
233+ }
234+ return &http.Client{
235+ Transport: trans,
236+ }, nil
237+}
238+
239+// SetAuthHeader uses the provided token to set the Authorization header on a
240+// request. If the token.Type is empty, the type is assumed to be Bearer.
241+func SetAuthHeader(token *auth.Token, req *http.Request) {
242+ typ := token.Type
243+ if typ == "" {
244+ typ = internal.TokenTypeBearer
245+ }
246+ req.Header.Set("Authorization", typ+" "+token.Value)
247+}
vendor/cloud.google.com/go/auth/httptransport/transport.gonew file+234
@@ -0,0 +1,234 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package httptransport
16+
17+import (
18+ "context"
19+ "crypto/tls"
20+ "net"
21+ "net/http"
22+ "os"
23+ "time"
24+
25+ "cloud.google.com/go/auth"
26+ "cloud.google.com/go/auth/credentials"
27+ "cloud.google.com/go/auth/internal"
28+ "cloud.google.com/go/auth/internal/transport"
29+ "cloud.google.com/go/auth/internal/transport/cert"
30+ "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
31+ "golang.org/x/net/http2"
32+)
33+
34+const (
35+ quotaProjectHeaderKey = "X-goog-user-project"
36+)
37+
38+func newTransport(base http.RoundTripper, opts *Options) (http.RoundTripper, error) {
39+ var headers = opts.Headers
40+ ht := &headerTransport{
41+ base: base,
42+ headers: headers,
43+ }
44+ var trans http.RoundTripper = ht
45+ trans = addOpenTelemetryTransport(trans, opts)
46+ switch {
47+ case opts.DisableAuthentication:
48+ // Do nothing.
49+ case opts.APIKey != "":
50+ qp := internal.GetQuotaProject(nil, opts.Headers.Get(quotaProjectHeaderKey))
51+ if qp != "" {
52+ if headers == nil {
53+ headers = make(map[string][]string, 1)
54+ }
55+ headers.Set(quotaProjectHeaderKey, qp)
56+ }
57+ trans = &apiKeyTransport{
58+ Transport: trans,
59+ Key: opts.APIKey,
60+ }
61+ default:
62+ var creds *auth.Credentials
63+ if opts.Credentials != nil {
64+ creds = opts.Credentials
65+ } else {
66+ var err error
67+ creds, err = credentials.DetectDefault(opts.resolveDetectOptions())
68+ if err != nil {
69+ return nil, err
70+ }
71+ }
72+ qp, err := creds.QuotaProjectID(context.Background())
73+ if err != nil {
74+ return nil, err
75+ }
76+ if qp != "" {
77+ if headers == nil {
78+ headers = make(map[string][]string, 1)
79+ }
80+ // Don't overwrite user specified quota
81+ if v := headers.Get(quotaProjectHeaderKey); v == "" {
82+ headers.Set(quotaProjectHeaderKey, qp)
83+ }
84+ }
85+ var skipUD bool
86+ if iOpts := opts.InternalOptions; iOpts != nil {
87+ skipUD = iOpts.SkipUniverseDomainValidation
88+ }
89+ creds.TokenProvider = auth.NewCachedTokenProvider(creds.TokenProvider, nil)
90+ trans = &authTransport{
91+ base: trans,
92+ creds: creds,
93+ clientUniverseDomain: opts.UniverseDomain,
94+ skipUniverseDomainValidation: skipUD,
95+ }
96+ }
97+ return trans, nil
98+}
99+
100+// defaultBaseTransport returns the base HTTP transport.
101+// On App Engine, this is urlfetch.Transport.
102+// Otherwise, use a default transport, taking most defaults from
103+// http.DefaultTransport.
104+// If TLSCertificate is available, set TLSClientConfig as well.
105+func defaultBaseTransport(clientCertSource cert.Provider, dialTLSContext func(context.Context, string, string) (net.Conn, error)) http.RoundTripper {
106+ defaultTransport, ok := http.DefaultTransport.(*http.Transport)
107+ if !ok {
108+ defaultTransport = transport.BaseTransport()
109+ }
110+ trans := defaultTransport.Clone()
111+ trans.MaxIdleConnsPerHost = 100
112+
113+ if clientCertSource != nil {
114+ trans.TLSClientConfig = &tls.Config{
115+ GetClientCertificate: clientCertSource,
116+ }
117+ }
118+ if dialTLSContext != nil {
119+ // If DialTLSContext is set, TLSClientConfig wil be ignored
120+ trans.DialTLSContext = dialTLSContext
121+ }
122+
123+ // Configures the ReadIdleTimeout HTTP/2 option for the
124+ // transport. This allows broken idle connections to be pruned more quickly,
125+ // preventing the client from attempting to re-use connections that will no
126+ // longer work.
127+ http2Trans, err := http2.ConfigureTransports(trans)
128+ if err == nil {
129+ http2Trans.ReadIdleTimeout = time.Second * 31
130+ }
131+
132+ return trans
133+}
134+
135+type apiKeyTransport struct {
136+ // Key is the API Key to set on requests.
137+ Key string
138+ // Transport is the underlying HTTP transport.
139+ // If nil, http.DefaultTransport is used.
140+ Transport http.RoundTripper
141+}
142+
143+func (t *apiKeyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
144+ newReq := *req
145+ args := newReq.URL.Query()
146+ args.Set("key", t.Key)
147+ newReq.URL.RawQuery = args.Encode()
148+ return t.Transport.RoundTrip(&newReq)
149+}
150+
151+type headerTransport struct {
152+ headers http.Header
153+ base http.RoundTripper
154+}
155+
156+func (t *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
157+ rt := t.base
158+ newReq := *req
159+ newReq.Header = make(http.Header)
160+ for k, vv := range req.Header {
161+ newReq.Header[k] = vv
162+ }
163+
164+ for k, v := range t.headers {
165+ newReq.Header[k] = v
166+ }
167+
168+ return rt.RoundTrip(&newReq)
169+}
170+
171+func addOpenTelemetryTransport(trans http.RoundTripper, opts *Options) http.RoundTripper {
172+ if opts.DisableTelemetry {
173+ return trans
174+ }
175+ return otelhttp.NewTransport(trans)
176+}
177+
178+type authTransport struct {
179+ creds *auth.Credentials
180+ base http.RoundTripper
181+ clientUniverseDomain string
182+ skipUniverseDomainValidation bool
183+}
184+
185+// getClientUniverseDomain returns the default service domain for a given Cloud
186+// universe, with the following precedence:
187+//
188+// 1. A non-empty option.WithUniverseDomain or similar client option.
189+// 2. A non-empty environment variable GOOGLE_CLOUD_UNIVERSE_DOMAIN.
190+// 3. The default value "googleapis.com".
191+//
192+// This is the universe domain configured for the client, which will be compared
193+// to the universe domain that is separately configured for the credentials.
194+func (t *authTransport) getClientUniverseDomain() string {
195+ if t.clientUniverseDomain != "" {
196+ return t.clientUniverseDomain
197+ }
198+ if envUD := os.Getenv(internal.UniverseDomainEnvVar); envUD != "" {
199+ return envUD
200+ }
201+ return internal.DefaultUniverseDomain
202+}
203+
204+// RoundTrip authorizes and authenticates the request with an
205+// access token from Transport's Source. Per the RoundTripper contract we must
206+// not modify the initial request, so we clone it, and we must close the body
207+// on any errors that happens during our token logic.
208+func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) {
209+ reqBodyClosed := false
210+ if req.Body != nil {
211+ defer func() {
212+ if !reqBodyClosed {
213+ req.Body.Close()
214+ }
215+ }()
216+ }
217+ token, err := t.creds.Token(req.Context())
218+ if err != nil {
219+ return nil, err
220+ }
221+ if !t.skipUniverseDomainValidation && token.MetadataString("auth.google.tokenSource") != "compute-metadata" {
222+ credentialsUniverseDomain, err := t.creds.UniverseDomain(req.Context())
223+ if err != nil {
224+ return nil, err
225+ }
226+ if err := transport.ValidateUniverseDomain(t.getClientUniverseDomain(), credentialsUniverseDomain); err != nil {
227+ return nil, err
228+ }
229+ }
230+ req2 := req.Clone(req.Context())
231+ SetAuthHeader(token, req2)
232+ reqBodyClosed = true
233+ return t.base.RoundTrip(req2)
234+}
vendor/cloud.google.com/go/auth/internal/credsfile/credsfile.gonew file+107
@@ -0,0 +1,107 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+// Package credsfile is meant to hide implementation details from the pubic
16+// surface of the detect package. It should not import any other packages in
17+// this module. It is located under the main internal package so other
18+// sub-packages can use these parsed types as well.
19+package credsfile
20+
21+import (
22+ "os"
23+ "os/user"
24+ "path/filepath"
25+ "runtime"
26+)
27+
28+const (
29+ // GoogleAppCredsEnvVar is the environment variable for setting the
30+ // application default credentials.
31+ GoogleAppCredsEnvVar = "GOOGLE_APPLICATION_CREDENTIALS"
32+ userCredsFilename = "application_default_credentials.json"
33+)
34+
35+// CredentialType represents different credential filetypes Google credentials
36+// can be.
37+type CredentialType int
38+
39+const (
40+ // UnknownCredType is an unidentified file type.
41+ UnknownCredType CredentialType = iota
42+ // UserCredentialsKey represents a user creds file type.
43+ UserCredentialsKey
44+ // ServiceAccountKey represents a service account file type.
45+ ServiceAccountKey
46+ // ImpersonatedServiceAccountKey represents a impersonated service account
47+ // file type.
48+ ImpersonatedServiceAccountKey
49+ // ExternalAccountKey represents a external account file type.
50+ ExternalAccountKey
51+ // GDCHServiceAccountKey represents a GDCH file type.
52+ GDCHServiceAccountKey
53+ // ExternalAccountAuthorizedUserKey represents a external account authorized
54+ // user file type.
55+ ExternalAccountAuthorizedUserKey
56+)
57+
58+// parseCredentialType returns the associated filetype based on the parsed
59+// typeString provided.
60+func parseCredentialType(typeString string) CredentialType {
61+ switch typeString {
62+ case "service_account":
63+ return ServiceAccountKey
64+ case "authorized_user":
65+ return UserCredentialsKey
66+ case "impersonated_service_account":
67+ return ImpersonatedServiceAccountKey
68+ case "external_account":
69+ return ExternalAccountKey
70+ case "external_account_authorized_user":
71+ return ExternalAccountAuthorizedUserKey
72+ case "gdch_service_account":
73+ return GDCHServiceAccountKey
74+ default:
75+ return UnknownCredType
76+ }
77+}
78+
79+// GetFileNameFromEnv returns the override if provided or detects a filename
80+// from the environment.
81+func GetFileNameFromEnv(override string) string {
82+ if override != "" {
83+ return override
84+ }
85+ return os.Getenv(GoogleAppCredsEnvVar)
86+}
87+
88+// GetWellKnownFileName tries to locate the filepath for the user credential
89+// file based on the environment.
90+func GetWellKnownFileName() string {
91+ if runtime.GOOS == "windows" {
92+ return filepath.Join(os.Getenv("APPDATA"), "gcloud", userCredsFilename)
93+ }
94+ return filepath.Join(guessUnixHomeDir(), ".config", "gcloud", userCredsFilename)
95+}
96+
97+// guessUnixHomeDir default to checking for HOME, but not all unix systems have
98+// this set, do have a fallback.
99+func guessUnixHomeDir() string {
100+ if v := os.Getenv("HOME"); v != "" {
101+ return v
102+ }
103+ if u, err := user.Current(); err == nil {
104+ return u.HomeDir
105+ }
106+ return ""
107+}
vendor/cloud.google.com/go/auth/internal/credsfile/filetype.gonew file+157
@@ -0,0 +1,157 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package credsfile
16+
17+import (
18+ "encoding/json"
19+)
20+
21+// Config3LO is the internals of a client creds file.
22+type Config3LO struct {
23+ ClientID string `json:"client_id"`
24+ ClientSecret string `json:"client_secret"`
25+ RedirectURIs []string `json:"redirect_uris"`
26+ AuthURI string `json:"auth_uri"`
27+ TokenURI string `json:"token_uri"`
28+}
29+
30+// ClientCredentialsFile representation.
31+type ClientCredentialsFile struct {
32+ Web *Config3LO `json:"web"`
33+ Installed *Config3LO `json:"installed"`
34+ UniverseDomain string `json:"universe_domain"`
35+}
36+
37+// ServiceAccountFile representation.
38+type ServiceAccountFile struct {
39+ Type string `json:"type"`
40+ ProjectID string `json:"project_id"`
41+ PrivateKeyID string `json:"private_key_id"`
42+ PrivateKey string `json:"private_key"`
43+ ClientEmail string `json:"client_email"`
44+ ClientID string `json:"client_id"`
45+ AuthURL string `json:"auth_uri"`
46+ TokenURL string `json:"token_uri"`
47+ UniverseDomain string `json:"universe_domain"`
48+}
49+
50+// UserCredentialsFile representation.
51+type UserCredentialsFile struct {
52+ Type string `json:"type"`
53+ ClientID string `json:"client_id"`
54+ ClientSecret string `json:"client_secret"`
55+ QuotaProjectID string `json:"quota_project_id"`
56+ RefreshToken string `json:"refresh_token"`
57+ UniverseDomain string `json:"universe_domain"`
58+}
59+
60+// ExternalAccountFile representation.
61+type ExternalAccountFile struct {
62+ Type string `json:"type"`
63+ ClientID string `json:"client_id"`
64+ ClientSecret string `json:"client_secret"`
65+ Audience string `json:"audience"`
66+ SubjectTokenType string `json:"subject_token_type"`
67+ ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
68+ TokenURL string `json:"token_url"`
69+ CredentialSource *CredentialSource `json:"credential_source,omitempty"`
70+ TokenInfoURL string `json:"token_info_url"`
71+ ServiceAccountImpersonation *ServiceAccountImpersonationInfo `json:"service_account_impersonation,omitempty"`
72+ QuotaProjectID string `json:"quota_project_id"`
73+ WorkforcePoolUserProject string `json:"workforce_pool_user_project"`
74+ UniverseDomain string `json:"universe_domain"`
75+}
76+
77+// ExternalAccountAuthorizedUserFile representation.
78+type ExternalAccountAuthorizedUserFile struct {
79+ Type string `json:"type"`
80+ Audience string `json:"audience"`
81+ ClientID string `json:"client_id"`
82+ ClientSecret string `json:"client_secret"`
83+ RefreshToken string `json:"refresh_token"`
84+ TokenURL string `json:"token_url"`
85+ TokenInfoURL string `json:"token_info_url"`
86+ RevokeURL string `json:"revoke_url"`
87+ QuotaProjectID string `json:"quota_project_id"`
88+ UniverseDomain string `json:"universe_domain"`
89+}
90+
91+// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
92+//
93+// One field amongst File, URL, Certificate, and Executable should be filled, depending on the kind of credential in question.
94+// The EnvironmentID should start with AWS if being used for an AWS credential.
95+type CredentialSource struct {
96+ File string `json:"file"`
97+ URL string `json:"url"`
98+ Headers map[string]string `json:"headers"`
99+ Executable *ExecutableConfig `json:"executable,omitempty"`
100+ Certificate *CertificateConfig `json:"certificate"`
101+ EnvironmentID string `json:"environment_id"` // TODO: Make type for this
102+ RegionURL string `json:"region_url"`
103+ RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
104+ CredVerificationURL string `json:"cred_verification_url"`
105+ IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"`
106+ Format *Format `json:"format,omitempty"`
107+}
108+
109+// Format describes the format of a [CredentialSource].
110+type Format struct {
111+ // Type is either "text" or "json". When not provided "text" type is assumed.
112+ Type string `json:"type"`
113+ // SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure.
114+ SubjectTokenFieldName string `json:"subject_token_field_name"`
115+}
116+
117+// ExecutableConfig represents the command to run for an executable
118+// [CredentialSource].
119+type ExecutableConfig struct {
120+ Command string `json:"command"`
121+ TimeoutMillis int `json:"timeout_millis"`
122+ OutputFile string `json:"output_file"`
123+}
124+
125+// CertificateConfig represents the options used to set up X509 based workload
126+// [CredentialSource]
127+type CertificateConfig struct {
128+ UseDefaultCertificateConfig bool `json:"use_default_certificate_config"`
129+ CertificateConfigLocation string `json:"certificate_config_location"`
130+}
131+
132+// ServiceAccountImpersonationInfo has impersonation configuration.
133+type ServiceAccountImpersonationInfo struct {
134+ TokenLifetimeSeconds int `json:"token_lifetime_seconds"`
135+}
136+
137+// ImpersonatedServiceAccountFile representation.
138+type ImpersonatedServiceAccountFile struct {
139+ Type string `json:"type"`
140+ ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
141+ Delegates []string `json:"delegates"`
142+ CredSource json.RawMessage `json:"source_credentials"`
143+ UniverseDomain string `json:"universe_domain"`
144+}
145+
146+// GDCHServiceAccountFile represents the Google Distributed Cloud Hosted (GDCH) service identity file.
147+type GDCHServiceAccountFile struct {
148+ Type string `json:"type"`
149+ FormatVersion string `json:"format_version"`
150+ Project string `json:"project"`
151+ Name string `json:"name"`
152+ CertPath string `json:"ca_cert_path"`
153+ PrivateKeyID string `json:"private_key_id"`
154+ PrivateKey string `json:"private_key"`
155+ TokenURL string `json:"token_uri"`
156+ UniverseDomain string `json:"universe_domain"`
157+}
vendor/cloud.google.com/go/auth/internal/credsfile/parse.gonew file+98
@@ -0,0 +1,98 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package credsfile
16+
17+import (
18+ "encoding/json"
19+)
20+
21+// ParseServiceAccount parses bytes into a [ServiceAccountFile].
22+func ParseServiceAccount(b []byte) (*ServiceAccountFile, error) {
23+ var f *ServiceAccountFile
24+ if err := json.Unmarshal(b, &f); err != nil {
25+ return nil, err
26+ }
27+ return f, nil
28+}
29+
30+// ParseClientCredentials parses bytes into a
31+// [credsfile.ClientCredentialsFile].
32+func ParseClientCredentials(b []byte) (*ClientCredentialsFile, error) {
33+ var f *ClientCredentialsFile
34+ if err := json.Unmarshal(b, &f); err != nil {
35+ return nil, err
36+ }
37+ return f, nil
38+}
39+
40+// ParseUserCredentials parses bytes into a [UserCredentialsFile].
41+func ParseUserCredentials(b []byte) (*UserCredentialsFile, error) {
42+ var f *UserCredentialsFile
43+ if err := json.Unmarshal(b, &f); err != nil {
44+ return nil, err
45+ }
46+ return f, nil
47+}
48+
49+// ParseExternalAccount parses bytes into a [ExternalAccountFile].
50+func ParseExternalAccount(b []byte) (*ExternalAccountFile, error) {
51+ var f *ExternalAccountFile
52+ if err := json.Unmarshal(b, &f); err != nil {
53+ return nil, err
54+ }
55+ return f, nil
56+}
57+
58+// ParseExternalAccountAuthorizedUser parses bytes into a
59+// [ExternalAccountAuthorizedUserFile].
60+func ParseExternalAccountAuthorizedUser(b []byte) (*ExternalAccountAuthorizedUserFile, error) {
61+ var f *ExternalAccountAuthorizedUserFile
62+ if err := json.Unmarshal(b, &f); err != nil {
63+ return nil, err
64+ }
65+ return f, nil
66+}
67+
68+// ParseImpersonatedServiceAccount parses bytes into a
69+// [ImpersonatedServiceAccountFile].
70+func ParseImpersonatedServiceAccount(b []byte) (*ImpersonatedServiceAccountFile, error) {
71+ var f *ImpersonatedServiceAccountFile
72+ if err := json.Unmarshal(b, &f); err != nil {
73+ return nil, err
74+ }
75+ return f, nil
76+}
77+
78+// ParseGDCHServiceAccount parses bytes into a [GDCHServiceAccountFile].
79+func ParseGDCHServiceAccount(b []byte) (*GDCHServiceAccountFile, error) {
80+ var f *GDCHServiceAccountFile
81+ if err := json.Unmarshal(b, &f); err != nil {
82+ return nil, err
83+ }
84+ return f, nil
85+}
86+
87+type fileTypeChecker struct {
88+ Type string `json:"type"`
89+}
90+
91+// ParseFileType determines the [CredentialType] based on bytes provided.
92+func ParseFileType(b []byte) (CredentialType, error) {
93+ var f fileTypeChecker
94+ if err := json.Unmarshal(b, &f); err != nil {
95+ return 0, err
96+ }
97+ return parseCredentialType(f.Type), nil
98+}
vendor/cloud.google.com/go/auth/internal/internal.gonew file+219
@@ -0,0 +1,219 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package internal
16+
17+import (
18+ "context"
19+ "crypto"
20+ "crypto/x509"
21+ "encoding/json"
22+ "encoding/pem"
23+ "errors"
24+ "fmt"
25+ "io"
26+ "net/http"
27+ "os"
28+ "sync"
29+ "time"
30+
31+ "cloud.google.com/go/compute/metadata"
32+)
33+
34+const (
35+ // TokenTypeBearer is the auth header prefix for bearer tokens.
36+ TokenTypeBearer = "Bearer"
37+
38+ // QuotaProjectEnvVar is the environment variable for setting the quota
39+ // project.
40+ QuotaProjectEnvVar = "GOOGLE_CLOUD_QUOTA_PROJECT"
41+ // UniverseDomainEnvVar is the environment variable for setting the default
42+ // service domain for a given Cloud universe.
43+ UniverseDomainEnvVar = "GOOGLE_CLOUD_UNIVERSE_DOMAIN"
44+ projectEnvVar = "GOOGLE_CLOUD_PROJECT"
45+ maxBodySize = 1 << 20
46+
47+ // DefaultUniverseDomain is the default value for universe domain.
48+ // Universe domain is the default service domain for a given Cloud universe.
49+ DefaultUniverseDomain = "googleapis.com"
50+)
51+
52+type clonableTransport interface {
53+ Clone() *http.Transport
54+}
55+
56+// DefaultClient returns an [http.Client] with some defaults set. If
57+// the current [http.DefaultTransport] is a [clonableTransport], as
58+// is the case for an [*http.Transport], the clone will be used.
59+// Otherwise the [http.DefaultTransport] is used directly.
60+func DefaultClient() *http.Client {
61+ if transport, ok := http.DefaultTransport.(clonableTransport); ok {
62+ return &http.Client{
63+ Transport: transport.Clone(),
64+ Timeout: 30 * time.Second,
65+ }
66+ }
67+
68+ return &http.Client{
69+ Transport: http.DefaultTransport,
70+ Timeout: 30 * time.Second,
71+ }
72+}
73+
74+// ParseKey converts the binary contents of a private key file
75+// to an crypto.Signer. It detects whether the private key is in a
76+// PEM container or not. If so, it extracts the the private key
77+// from PEM container before conversion. It only supports PEM
78+// containers with no passphrase.
79+func ParseKey(key []byte) (crypto.Signer, error) {
80+ block, _ := pem.Decode(key)
81+ if block != nil {
82+ key = block.Bytes
83+ }
84+ var parsedKey crypto.PrivateKey
85+ var err error
86+ parsedKey, err = x509.ParsePKCS8PrivateKey(key)
87+ if err != nil {
88+ parsedKey, err = x509.ParsePKCS1PrivateKey(key)
89+ if err != nil {
90+ return nil, fmt.Errorf("private key should be a PEM or plain PKCS1 or PKCS8: %w", err)
91+ }
92+ }
93+ parsed, ok := parsedKey.(crypto.Signer)
94+ if !ok {
95+ return nil, errors.New("private key is not a signer")
96+ }
97+ return parsed, nil
98+}
99+
100+// GetQuotaProject retrieves quota project with precedence being: override,
101+// environment variable, creds json file.
102+func GetQuotaProject(b []byte, override string) string {
103+ if override != "" {
104+ return override
105+ }
106+ if env := os.Getenv(QuotaProjectEnvVar); env != "" {
107+ return env
108+ }
109+ if b == nil {
110+ return ""
111+ }
112+ var v struct {
113+ QuotaProject string `json:"quota_project_id"`
114+ }
115+ if err := json.Unmarshal(b, &v); err != nil {
116+ return ""
117+ }
118+ return v.QuotaProject
119+}
120+
121+// GetProjectID retrieves project with precedence being: override,
122+// environment variable, creds json file.
123+func GetProjectID(b []byte, override string) string {
124+ if override != "" {
125+ return override
126+ }
127+ if env := os.Getenv(projectEnvVar); env != "" {
128+ return env
129+ }
130+ if b == nil {
131+ return ""
132+ }
133+ var v struct {
134+ ProjectID string `json:"project_id"` // standard service account key
135+ Project string `json:"project"` // gdch key
136+ }
137+ if err := json.Unmarshal(b, &v); err != nil {
138+ return ""
139+ }
140+ if v.ProjectID != "" {
141+ return v.ProjectID
142+ }
143+ return v.Project
144+}
145+
146+// DoRequest executes the provided req with the client. It reads the response
147+// body, closes it, and returns it.
148+func DoRequest(client *http.Client, req *http.Request) (*http.Response, []byte, error) {
149+ resp, err := client.Do(req)
150+ if err != nil {
151+ return nil, nil, err
152+ }
153+ defer resp.Body.Close()
154+ body, err := ReadAll(io.LimitReader(resp.Body, maxBodySize))
155+ if err != nil {
156+ return nil, nil, err
157+ }
158+ return resp, body, nil
159+}
160+
161+// ReadAll consumes the whole reader and safely reads the content of its body
162+// with some overflow protection.
163+func ReadAll(r io.Reader) ([]byte, error) {
164+ return io.ReadAll(io.LimitReader(r, maxBodySize))
165+}
166+
167+// StaticCredentialsProperty is a helper for creating static credentials
168+// properties.
169+func StaticCredentialsProperty(s string) StaticProperty {
170+ return StaticProperty(s)
171+}
172+
173+// StaticProperty always returns that value of the underlying string.
174+type StaticProperty string
175+
176+// GetProperty loads the properly value provided the given context.
177+func (p StaticProperty) GetProperty(context.Context) (string, error) {
178+ return string(p), nil
179+}
180+
181+// ComputeUniverseDomainProvider fetches the credentials universe domain from
182+// the google cloud metadata service.
183+type ComputeUniverseDomainProvider struct {
184+ MetadataClient *metadata.Client
185+ universeDomainOnce sync.Once
186+ universeDomain string
187+ universeDomainErr error
188+}
189+
190+// GetProperty fetches the credentials universe domain from the google cloud
191+// metadata service.
192+func (c *ComputeUniverseDomainProvider) GetProperty(ctx context.Context) (string, error) {
193+ c.universeDomainOnce.Do(func() {
194+ c.universeDomain, c.universeDomainErr = getMetadataUniverseDomain(ctx, c.MetadataClient)
195+ })
196+ if c.universeDomainErr != nil {
197+ return "", c.universeDomainErr
198+ }
199+ return c.universeDomain, nil
200+}
201+
202+// httpGetMetadataUniverseDomain is a package var for unit test substitution.
203+var httpGetMetadataUniverseDomain = func(ctx context.Context, client *metadata.Client) (string, error) {
204+ ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
205+ defer cancel()
206+ return client.GetWithContext(ctx, "universe/universe-domain")
207+}
208+
209+func getMetadataUniverseDomain(ctx context.Context, client *metadata.Client) (string, error) {
210+ universeDomain, err := httpGetMetadataUniverseDomain(ctx, client)
211+ if err == nil {
212+ return universeDomain, nil
213+ }
214+ if _, ok := err.(metadata.NotDefinedError); ok {
215+ // http.StatusNotFound (404)
216+ return DefaultUniverseDomain, nil
217+ }
218+ return "", err
219+}
vendor/cloud.google.com/go/auth/internal/jwt/jwt.gonew file+171
@@ -0,0 +1,171 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package jwt
16+
17+import (
18+ "bytes"
19+ "crypto"
20+ "crypto/rand"
21+ "crypto/rsa"
22+ "crypto/sha256"
23+ "encoding/base64"
24+ "encoding/json"
25+ "errors"
26+ "fmt"
27+ "strings"
28+ "time"
29+)
30+
31+const (
32+ // HeaderAlgRSA256 is the RS256 [Header.Algorithm].
33+ HeaderAlgRSA256 = "RS256"
34+ // HeaderAlgES256 is the ES256 [Header.Algorithm].
35+ HeaderAlgES256 = "ES256"
36+ // HeaderType is the standard [Header.Type].
37+ HeaderType = "JWT"
38+)
39+
40+// Header represents a JWT header.
41+type Header struct {
42+ Algorithm string `json:"alg"`
43+ Type string `json:"typ"`
44+ KeyID string `json:"kid"`
45+}
46+
47+func (h *Header) encode() (string, error) {
48+ b, err := json.Marshal(h)
49+ if err != nil {
50+ return "", err
51+ }
52+ return base64.RawURLEncoding.EncodeToString(b), nil
53+}
54+
55+// Claims represents the claims set of a JWT.
56+type Claims struct {
57+ // Iss is the issuer JWT claim.
58+ Iss string `json:"iss"`
59+ // Scope is the scope JWT claim.
60+ Scope string `json:"scope,omitempty"`
61+ // Exp is the expiry JWT claim. If unset, default is in one hour from now.
62+ Exp int64 `json:"exp"`
63+ // Iat is the subject issued at claim. If unset, default is now.
64+ Iat int64 `json:"iat"`
65+ // Aud is the audience JWT claim. Optional.
66+ Aud string `json:"aud"`
67+ // Sub is the subject JWT claim. Optional.
68+ Sub string `json:"sub,omitempty"`
69+ // AdditionalClaims contains any additional non-standard JWT claims. Optional.
70+ AdditionalClaims map[string]interface{} `json:"-"`
71+}
72+
73+func (c *Claims) encode() (string, error) {
74+ // Compensate for skew
75+ now := time.Now().Add(-10 * time.Second)
76+ if c.Iat == 0 {
77+ c.Iat = now.Unix()
78+ }
79+ if c.Exp == 0 {
80+ c.Exp = now.Add(time.Hour).Unix()
81+ }
82+ if c.Exp < c.Iat {
83+ return "", fmt.Errorf("jwt: invalid Exp = %d; must be later than Iat = %d", c.Exp, c.Iat)
84+ }
85+
86+ b, err := json.Marshal(c)
87+ if err != nil {
88+ return "", err
89+ }
90+
91+ if len(c.AdditionalClaims) == 0 {
92+ return base64.RawURLEncoding.EncodeToString(b), nil
93+ }
94+
95+ // Marshal private claim set and then append it to b.
96+ prv, err := json.Marshal(c.AdditionalClaims)
97+ if err != nil {
98+ return "", fmt.Errorf("invalid map of additional claims %v: %w", c.AdditionalClaims, err)
99+ }
100+
101+ // Concatenate public and private claim JSON objects.
102+ if !bytes.HasSuffix(b, []byte{'}'}) {
103+ return "", fmt.Errorf("invalid JSON %s", b)
104+ }
105+ if !bytes.HasPrefix(prv, []byte{'{'}) {
106+ return "", fmt.Errorf("invalid JSON %s", prv)
107+ }
108+ b[len(b)-1] = ',' // Replace closing curly brace with a comma.
109+ b = append(b, prv[1:]...) // Append private claims.
110+ return base64.RawURLEncoding.EncodeToString(b), nil
111+}
112+
113+// EncodeJWS encodes the data using the provided key as a JSON web signature.
114+func EncodeJWS(header *Header, c *Claims, signer crypto.Signer) (string, error) {
115+ head, err := header.encode()
116+ if err != nil {
117+ return "", err
118+ }
119+ claims, err := c.encode()
120+ if err != nil {
121+ return "", err
122+ }
123+ ss := fmt.Sprintf("%s.%s", head, claims)
124+ h := sha256.New()
125+ h.Write([]byte(ss))
126+ sig, err := signer.Sign(rand.Reader, h.Sum(nil), crypto.SHA256)
127+ if err != nil {
128+ return "", err
129+ }
130+ return fmt.Sprintf("%s.%s", ss, base64.RawURLEncoding.EncodeToString(sig)), nil
131+}
132+
133+// DecodeJWS decodes a claim set from a JWS payload.
134+func DecodeJWS(payload string) (*Claims, error) {
135+ // decode returned id token to get expiry
136+ s := strings.Split(payload, ".")
137+ if len(s) < 2 {
138+ return nil, errors.New("invalid token received")
139+ }
140+ decoded, err := base64.RawURLEncoding.DecodeString(s[1])
141+ if err != nil {
142+ return nil, err
143+ }
144+ c := &Claims{}
145+ if err := json.NewDecoder(bytes.NewBuffer(decoded)).Decode(c); err != nil {
146+ return nil, err
147+ }
148+ if err := json.NewDecoder(bytes.NewBuffer(decoded)).Decode(&c.AdditionalClaims); err != nil {
149+ return nil, err
150+ }
151+ return c, err
152+}
153+
154+// VerifyJWS tests whether the provided JWT token's signature was produced by
155+// the private key associated with the provided public key.
156+func VerifyJWS(token string, key *rsa.PublicKey) error {
157+ parts := strings.Split(token, ".")
158+ if len(parts) != 3 {
159+ return errors.New("jwt: invalid token received, token must have 3 parts")
160+ }
161+
162+ signedContent := parts[0] + "." + parts[1]
163+ signatureString, err := base64.RawURLEncoding.DecodeString(parts[2])
164+ if err != nil {
165+ return err
166+ }
167+
168+ h := sha256.New()
169+ h.Write([]byte(signedContent))
170+ return rsa.VerifyPKCS1v15(key, crypto.SHA256, h.Sum(nil), signatureString)
171+}
vendor/cloud.google.com/go/auth/internal/transport/cba.gonew file+368
@@ -0,0 +1,368 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package transport
16+
17+import (
18+ "context"
19+ "crypto/tls"
20+ "crypto/x509"
21+ "errors"
22+ "log"
23+ "log/slog"
24+ "net"
25+ "net/http"
26+ "net/url"
27+ "os"
28+ "strconv"
29+ "strings"
30+
31+ "cloud.google.com/go/auth/internal"
32+ "cloud.google.com/go/auth/internal/transport/cert"
33+ "github.com/google/s2a-go"
34+ "github.com/google/s2a-go/fallback"
35+ "google.golang.org/grpc/credentials"
36+)
37+
38+const (
39+ mTLSModeAlways = "always"
40+ mTLSModeNever = "never"
41+ mTLSModeAuto = "auto"
42+
43+ // Experimental: if true, the code will try MTLS with S2A as the default for transport security. Default value is false.
44+ googleAPIUseS2AEnv = "EXPERIMENTAL_GOOGLE_API_USE_S2A"
45+ googleAPIUseCertSource = "GOOGLE_API_USE_CLIENT_CERTIFICATE"
46+ googleAPIUseMTLS = "GOOGLE_API_USE_MTLS_ENDPOINT"
47+ googleAPIUseMTLSOld = "GOOGLE_API_USE_MTLS"
48+
49+ universeDomainPlaceholder = "UNIVERSE_DOMAIN"
50+
51+ mtlsMDSRoot = "/run/google-mds-mtls/root.crt"
52+ mtlsMDSKey = "/run/google-mds-mtls/client.key"
53+)
54+
55+// Options is a struct that is duplicated information from the individual
56+// transport packages in order to avoid cyclic deps. It correlates 1:1 with
57+// fields on httptransport.Options and grpctransport.Options.
58+type Options struct {
59+ Endpoint string
60+ DefaultEndpointTemplate string
61+ DefaultMTLSEndpoint string
62+ ClientCertProvider cert.Provider
63+ Client *http.Client
64+ UniverseDomain string
65+ EnableDirectPath bool
66+ EnableDirectPathXds bool
67+ Logger *slog.Logger
68+}
69+
70+// getUniverseDomain returns the default service domain for a given Cloud
71+// universe.
72+func (o *Options) getUniverseDomain() string {
73+ if o.UniverseDomain == "" {
74+ return internal.DefaultUniverseDomain
75+ }
76+ return o.UniverseDomain
77+}
78+
79+// isUniverseDomainGDU returns true if the universe domain is the default Google
80+// universe.
81+func (o *Options) isUniverseDomainGDU() bool {
82+ return o.getUniverseDomain() == internal.DefaultUniverseDomain
83+}
84+
85+// defaultEndpoint returns the DefaultEndpointTemplate merged with the
86+// universe domain if the DefaultEndpointTemplate is set, otherwise returns an
87+// empty string.
88+func (o *Options) defaultEndpoint() string {
89+ if o.DefaultEndpointTemplate == "" {
90+ return ""
91+ }
92+ return strings.Replace(o.DefaultEndpointTemplate, universeDomainPlaceholder, o.getUniverseDomain(), 1)
93+}
94+
95+// defaultMTLSEndpoint returns the DefaultMTLSEndpointTemplate merged with the
96+// universe domain if the DefaultMTLSEndpointTemplate is set, otherwise returns an
97+// empty string.
98+func (o *Options) defaultMTLSEndpoint() string {
99+ if o.DefaultMTLSEndpoint == "" {
100+ return ""
101+ }
102+ return strings.Replace(o.DefaultMTLSEndpoint, universeDomainPlaceholder, o.getUniverseDomain(), 1)
103+}
104+
105+// mergedEndpoint merges a user-provided Endpoint of format host[:port] with the
106+// default endpoint.
107+func (o *Options) mergedEndpoint() (string, error) {
108+ defaultEndpoint := o.defaultEndpoint()
109+ u, err := url.Parse(fixScheme(defaultEndpoint))
110+ if err != nil {
111+ return "", err
112+ }
113+ return strings.Replace(defaultEndpoint, u.Host, o.Endpoint, 1), nil
114+}
115+
116+func fixScheme(baseURL string) string {
117+ if !strings.Contains(baseURL, "://") {
118+ baseURL = "https://" + baseURL
119+ }
120+ return baseURL
121+}
122+
123+// GetGRPCTransportCredsAndEndpoint returns an instance of
124+// [google.golang.org/grpc/credentials.TransportCredentials], and the
125+// corresponding endpoint to use for GRPC client.
126+func GetGRPCTransportCredsAndEndpoint(opts *Options) (credentials.TransportCredentials, string, error) {
127+ config, err := getTransportConfig(opts)
128+ if err != nil {
129+ return nil, "", err
130+ }
131+
132+ defaultTransportCreds := credentials.NewTLS(&tls.Config{
133+ GetClientCertificate: config.clientCertSource,
134+ })
135+
136+ var s2aAddr string
137+ var transportCredsForS2A credentials.TransportCredentials
138+
139+ if config.mtlsS2AAddress != "" {
140+ s2aAddr = config.mtlsS2AAddress
141+ transportCredsForS2A, err = loadMTLSMDSTransportCreds(mtlsMDSRoot, mtlsMDSKey)
142+ if err != nil {
143+ log.Printf("Loading MTLS MDS credentials failed: %v", err)
144+ if config.s2aAddress != "" {
145+ s2aAddr = config.s2aAddress
146+ } else {
147+ return defaultTransportCreds, config.endpoint, nil
148+ }
149+ }
150+ } else if config.s2aAddress != "" {
151+ s2aAddr = config.s2aAddress
152+ } else {
153+ return defaultTransportCreds, config.endpoint, nil
154+ }
155+
156+ var fallbackOpts *s2a.FallbackOptions
157+ // In case of S2A failure, fall back to the endpoint that would've been used without S2A.
158+ if fallbackHandshake, err := fallback.DefaultFallbackClientHandshakeFunc(config.endpoint); err == nil {
159+ fallbackOpts = &s2a.FallbackOptions{
160+ FallbackClientHandshakeFunc: fallbackHandshake,
161+ }
162+ }
163+
164+ s2aTransportCreds, err := s2a.NewClientCreds(&s2a.ClientOptions{
165+ S2AAddress: s2aAddr,
166+ TransportCreds: transportCredsForS2A,
167+ FallbackOpts: fallbackOpts,
168+ })
169+ if err != nil {
170+ // Use default if we cannot initialize S2A client transport credentials.
171+ return defaultTransportCreds, config.endpoint, nil
172+ }
173+ return s2aTransportCreds, config.s2aMTLSEndpoint, nil
174+}
175+
176+// GetHTTPTransportConfig returns a client certificate source and a function for
177+// dialing MTLS with S2A.
178+func GetHTTPTransportConfig(opts *Options) (cert.Provider, func(context.Context, string, string) (net.Conn, error), error) {
179+ config, err := getTransportConfig(opts)
180+ if err != nil {
181+ return nil, nil, err
182+ }
183+
184+ var s2aAddr string
185+ var transportCredsForS2A credentials.TransportCredentials
186+
187+ if config.mtlsS2AAddress != "" {
188+ s2aAddr = config.mtlsS2AAddress
189+ transportCredsForS2A, err = loadMTLSMDSTransportCreds(mtlsMDSRoot, mtlsMDSKey)
190+ if err != nil {
191+ log.Printf("Loading MTLS MDS credentials failed: %v", err)
192+ if config.s2aAddress != "" {
193+ s2aAddr = config.s2aAddress
194+ } else {
195+ return config.clientCertSource, nil, nil
196+ }
197+ }
198+ } else if config.s2aAddress != "" {
199+ s2aAddr = config.s2aAddress
200+ } else {
201+ return config.clientCertSource, nil, nil
202+ }
203+
204+ var fallbackOpts *s2a.FallbackOptions
205+ // In case of S2A failure, fall back to the endpoint that would've been used without S2A.
206+ if fallbackURL, err := url.Parse(config.endpoint); err == nil {
207+ if fallbackDialer, fallbackServerAddr, err := fallback.DefaultFallbackDialerAndAddress(fallbackURL.Hostname()); err == nil {
208+ fallbackOpts = &s2a.FallbackOptions{
209+ FallbackDialer: &s2a.FallbackDialer{
210+ Dialer: fallbackDialer,
211+ ServerAddr: fallbackServerAddr,
212+ },
213+ }
214+ }
215+ }
216+
217+ dialTLSContextFunc := s2a.NewS2ADialTLSContextFunc(&s2a.ClientOptions{
218+ S2AAddress: s2aAddr,
219+ TransportCreds: transportCredsForS2A,
220+ FallbackOpts: fallbackOpts,
221+ })
222+ return nil, dialTLSContextFunc, nil
223+}
224+
225+func loadMTLSMDSTransportCreds(mtlsMDSRootFile, mtlsMDSKeyFile string) (credentials.TransportCredentials, error) {
226+ rootPEM, err := os.ReadFile(mtlsMDSRootFile)
227+ if err != nil {
228+ return nil, err
229+ }
230+ caCertPool := x509.NewCertPool()
231+ ok := caCertPool.AppendCertsFromPEM(rootPEM)
232+ if !ok {
233+ return nil, errors.New("failed to load MTLS MDS root certificate")
234+ }
235+ // The mTLS MDS credentials are formatted as the concatenation of a PEM-encoded certificate chain
236+ // followed by a PEM-encoded private key. For this reason, the concatenation is passed in to the
237+ // tls.X509KeyPair function as both the certificate chain and private key arguments.
238+ cert, err := tls.LoadX509KeyPair(mtlsMDSKeyFile, mtlsMDSKeyFile)
239+ if err != nil {
240+ return nil, err
241+ }
242+ tlsConfig := tls.Config{
243+ RootCAs: caCertPool,
244+ Certificates: []tls.Certificate{cert},
245+ MinVersion: tls.VersionTLS13,
246+ }
247+ return credentials.NewTLS(&tlsConfig), nil
248+}
249+
250+func getTransportConfig(opts *Options) (*transportConfig, error) {
251+ clientCertSource, err := GetClientCertificateProvider(opts)
252+ if err != nil {
253+ return nil, err
254+ }
255+ endpoint, err := getEndpoint(opts, clientCertSource)
256+ if err != nil {
257+ return nil, err
258+ }
259+ defaultTransportConfig := transportConfig{
260+ clientCertSource: clientCertSource,
261+ endpoint: endpoint,
262+ }
263+
264+ if !shouldUseS2A(clientCertSource, opts) {
265+ return &defaultTransportConfig, nil
266+ }
267+
268+ s2aAddress := GetS2AAddress(opts.Logger)
269+ mtlsS2AAddress := GetMTLSS2AAddress(opts.Logger)
270+ if s2aAddress == "" && mtlsS2AAddress == "" {
271+ return &defaultTransportConfig, nil
272+ }
273+ return &transportConfig{
274+ clientCertSource: clientCertSource,
275+ endpoint: endpoint,
276+ s2aAddress: s2aAddress,
277+ mtlsS2AAddress: mtlsS2AAddress,
278+ s2aMTLSEndpoint: opts.defaultMTLSEndpoint(),
279+ }, nil
280+}
281+
282+// GetClientCertificateProvider returns a default client certificate source, if
283+// not provided by the user.
284+//
285+// A nil default source can be returned if the source does not exist. Any exceptions
286+// encountered while initializing the default source will be reported as client
287+// error (ex. corrupt metadata file).
288+func GetClientCertificateProvider(opts *Options) (cert.Provider, error) {
289+ if !isClientCertificateEnabled(opts) {
290+ return nil, nil
291+ } else if opts.ClientCertProvider != nil {
292+ return opts.ClientCertProvider, nil
293+ }
294+ return cert.DefaultProvider()
295+
296+}
297+
298+// isClientCertificateEnabled returns true by default for all GDU universe domain, unless explicitly overridden by env var
299+func isClientCertificateEnabled(opts *Options) bool {
300+ if value, ok := os.LookupEnv(googleAPIUseCertSource); ok {
301+ // error as false is OK
302+ b, _ := strconv.ParseBool(value)
303+ return b
304+ }
305+ return opts.isUniverseDomainGDU()
306+}
307+
308+type transportConfig struct {
309+ // The client certificate source.
310+ clientCertSource cert.Provider
311+ // The corresponding endpoint to use based on client certificate source.
312+ endpoint string
313+ // The plaintext S2A address if it can be used, otherwise an empty string.
314+ s2aAddress string
315+ // The MTLS S2A address if it can be used, otherwise an empty string.
316+ mtlsS2AAddress string
317+ // The MTLS endpoint to use with S2A.
318+ s2aMTLSEndpoint string
319+}
320+
321+// getEndpoint returns the endpoint for the service, taking into account the
322+// user-provided endpoint override "settings.Endpoint".
323+//
324+// If no endpoint override is specified, we will either return the default
325+// endpoint or the default mTLS endpoint if a client certificate is available.
326+//
327+// You can override the default endpoint choice (mTLS vs. regular) by setting
328+// the GOOGLE_API_USE_MTLS_ENDPOINT environment variable.
329+//
330+// If the endpoint override is an address (host:port) rather than full base
331+// URL (ex. https://...), then the user-provided address will be merged into
332+// the default endpoint. For example, WithEndpoint("myhost:8000") and
333+// DefaultEndpointTemplate("https://UNIVERSE_DOMAIN/bar/baz") will return
334+// "https://myhost:8080/bar/baz". Note that this does not apply to the mTLS
335+// endpoint.
336+func getEndpoint(opts *Options, clientCertSource cert.Provider) (string, error) {
337+ if opts.Endpoint == "" {
338+ mtlsMode := getMTLSMode()
339+ if mtlsMode == mTLSModeAlways || (clientCertSource != nil && mtlsMode == mTLSModeAuto) {
340+ return opts.defaultMTLSEndpoint(), nil
341+ }
342+ return opts.defaultEndpoint(), nil
343+ }
344+ if strings.Contains(opts.Endpoint, "://") {
345+ // User passed in a full URL path, use it verbatim.
346+ return opts.Endpoint, nil
347+ }
348+ if opts.defaultEndpoint() == "" {
349+ // If DefaultEndpointTemplate is not configured,
350+ // use the user provided endpoint verbatim. This allows a naked
351+ // "host[:port]" URL to be used with GRPC Direct Path.
352+ return opts.Endpoint, nil
353+ }
354+
355+ // Assume user-provided endpoint is host[:port], merge it with the default endpoint.
356+ return opts.mergedEndpoint()
357+}
358+
359+func getMTLSMode() string {
360+ mode := os.Getenv(googleAPIUseMTLS)
361+ if mode == "" {
362+ mode = os.Getenv(googleAPIUseMTLSOld) // Deprecated.
363+ }
364+ if mode == "" {
365+ return mTLSModeAuto
366+ }
367+ return strings.ToLower(mode)
368+}
vendor/cloud.google.com/go/auth/internal/transport/cert/default_cert.gonew file+65
@@ -0,0 +1,65 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package cert
16+
17+import (
18+ "crypto/tls"
19+ "errors"
20+ "sync"
21+)
22+
23+// defaultCertData holds all the variables pertaining to
24+// the default certificate provider created by [DefaultProvider].
25+//
26+// A singleton model is used to allow the provider to be reused
27+// by the transport layer. As mentioned in [DefaultProvider] (provider nil, nil)
28+// may be returned to indicate a default provider could not be found, which
29+// will skip extra tls config in the transport layer .
30+type defaultCertData struct {
31+ once sync.Once
32+ provider Provider
33+ err error
34+}
35+
36+var (
37+ defaultCert defaultCertData
38+)
39+
40+// Provider is a function that can be passed into crypto/tls.Config.GetClientCertificate.
41+type Provider func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
42+
43+// errSourceUnavailable is a sentinel error to indicate certificate source is unavailable.
44+var errSourceUnavailable = errors.New("certificate source is unavailable")
45+
46+// DefaultProvider returns a certificate source using the preferred EnterpriseCertificateProxySource.
47+// If EnterpriseCertificateProxySource is not available, fall back to the legacy SecureConnectSource.
48+//
49+// If neither source is available (due to missing configurations), a nil Source and a nil Error are
50+// returned to indicate that a default certificate source is unavailable.
51+func DefaultProvider() (Provider, error) {
52+ defaultCert.once.Do(func() {
53+ defaultCert.provider, defaultCert.err = NewWorkloadX509CertProvider("")
54+ if errors.Is(defaultCert.err, errSourceUnavailable) {
55+ defaultCert.provider, defaultCert.err = NewEnterpriseCertificateProxyProvider("")
56+ if errors.Is(defaultCert.err, errSourceUnavailable) {
57+ defaultCert.provider, defaultCert.err = NewSecureConnectProvider("")
58+ if errors.Is(defaultCert.err, errSourceUnavailable) {
59+ defaultCert.provider, defaultCert.err = nil, nil
60+ }
61+ }
62+ }
63+ })
64+ return defaultCert.provider, defaultCert.err
65+}
vendor/cloud.google.com/go/auth/internal/transport/cert/enterprise_cert.gonew file+54
@@ -0,0 +1,54 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package cert
16+
17+import (
18+ "crypto/tls"
19+
20+ "github.com/googleapis/enterprise-certificate-proxy/client"
21+)
22+
23+type ecpSource struct {
24+ key *client.Key
25+}
26+
27+// NewEnterpriseCertificateProxyProvider creates a certificate source
28+// using the Enterprise Certificate Proxy client, which delegates
29+// certifcate related operations to an OS-specific "signer binary"
30+// that communicates with the native keystore (ex. keychain on MacOS).
31+//
32+// The configFilePath points to a config file containing relevant parameters
33+// such as the certificate issuer and the location of the signer binary.
34+// If configFilePath is empty, the client will attempt to load the config from
35+// a well-known gcloud location.
36+func NewEnterpriseCertificateProxyProvider(configFilePath string) (Provider, error) {
37+ key, err := client.Cred(configFilePath)
38+ if err != nil {
39+ // TODO(codyoss): once this is fixed upstream can handle this error a
40+ // little better here. But be safe for now and assume unavailable.
41+ return nil, errSourceUnavailable
42+ }
43+
44+ return (&ecpSource{
45+ key: key,
46+ }).getClientCertificate, nil
47+}
48+
49+func (s *ecpSource) getClientCertificate(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
50+ var cert tls.Certificate
51+ cert.PrivateKey = s.key
52+ cert.Certificate = s.key.CertificateChain()
53+ return &cert, nil
54+}
vendor/cloud.google.com/go/auth/internal/transport/cert/secureconnect_cert.gonew file+124
@@ -0,0 +1,124 @@
1+// Copyright 2023 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package cert
16+
17+import (
18+ "crypto/tls"
19+ "crypto/x509"
20+ "encoding/json"
21+ "errors"
22+ "fmt"
23+ "os"
24+ "os/exec"
25+ "os/user"
26+ "path/filepath"
27+ "sync"
28+ "time"
29+)
30+
31+const (
32+ metadataPath = ".secureConnect"
33+ metadataFile = "context_aware_metadata.json"
34+)
35+
36+type secureConnectSource struct {
37+ metadata secureConnectMetadata
38+
39+ // Cache the cert to avoid executing helper command repeatedly.
40+ cachedCertMutex sync.Mutex
41+ cachedCert *tls.Certificate
42+}
43+
44+type secureConnectMetadata struct {
45+ Cmd []string `json:"cert_provider_command"`
46+}
47+
48+// NewSecureConnectProvider creates a certificate source using
49+// the Secure Connect Helper and its associated metadata file.
50+//
51+// The configFilePath points to the location of the context aware metadata file.
52+// If configFilePath is empty, use the default context aware metadata location.
53+func NewSecureConnectProvider(configFilePath string) (Provider, error) {
54+ if configFilePath == "" {
55+ user, err := user.Current()
56+ if err != nil {
57+ // Error locating the default config means Secure Connect is not supported.
58+ return nil, errSourceUnavailable
59+ }
60+ configFilePath = filepath.Join(user.HomeDir, metadataPath, metadataFile)
61+ }
62+
63+ file, err := os.ReadFile(configFilePath)
64+ if err != nil {
65+ // Config file missing means Secure Connect is not supported.
66+ // There are non-os.ErrNotExist errors that may be returned.
67+ // (e.g. if the home directory is /dev/null, *nix systems will
68+ // return ENOTDIR instead of ENOENT)
69+ return nil, errSourceUnavailable
70+ }
71+
72+ var metadata secureConnectMetadata
73+ if err := json.Unmarshal(file, &metadata); err != nil {
74+ return nil, fmt.Errorf("cert: could not parse JSON in %q: %w", configFilePath, err)
75+ }
76+ if err := validateMetadata(metadata); err != nil {
77+ return nil, fmt.Errorf("cert: invalid config in %q: %w", configFilePath, err)
78+ }
79+ return (&secureConnectSource{
80+ metadata: metadata,
81+ }).getClientCertificate, nil
82+}
83+
84+func validateMetadata(metadata secureConnectMetadata) error {
85+ if len(metadata.Cmd) == 0 {
86+ return errors.New("empty cert_provider_command")
87+ }
88+ return nil
89+}
90+
91+func (s *secureConnectSource) getClientCertificate(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
92+ s.cachedCertMutex.Lock()
93+ defer s.cachedCertMutex.Unlock()
94+ if s.cachedCert != nil && !isCertificateExpired(s.cachedCert) {
95+ return s.cachedCert, nil
96+ }
97+ // Expand OS environment variables in the cert provider command such as "$HOME".
98+ for i := 0; i < len(s.metadata.Cmd); i++ {
99+ s.metadata.Cmd[i] = os.ExpandEnv(s.metadata.Cmd[i])
100+ }
101+ command := s.metadata.Cmd
102+ data, err := exec.Command(command[0], command[1:]...).Output()
103+ if err != nil {
104+ return nil, err
105+ }
106+ cert, err := tls.X509KeyPair(data, data)
107+ if err != nil {
108+ return nil, err
109+ }
110+ s.cachedCert = &cert
111+ return &cert, nil
112+}
113+
114+// isCertificateExpired returns true if the given cert is expired or invalid.
115+func isCertificateExpired(cert *tls.Certificate) bool {
116+ if len(cert.Certificate) == 0 {
117+ return true
118+ }
119+ parsed, err := x509.ParseCertificate(cert.Certificate[0])
120+ if err != nil {
121+ return true
122+ }
123+ return time.Now().After(parsed.NotAfter)
124+}
vendor/cloud.google.com/go/auth/internal/transport/cert/workload_cert.gonew file+114
@@ -0,0 +1,114 @@
1+// Copyright 2024 Google LLC
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+
15+package cert
16+
17+import (
18+ "crypto/tls"
19+ "encoding/json"
20+ "errors"
21+ "io"
22+ "os"
23+
24+ "github.com/googleapis/enterprise-certificate-proxy/client/util"
25+)
26+
27+type certConfigs struct {
28+ Workload *workloadSource `json:"workload"`
29+}
30+
31+type workloadSource struct {
32+ CertPath string `json:"cert_path"`
33+ KeyPath string `json:"key_path"`
34+}
35+
36+type certificateConfig struct {
37+ CertConfigs certConfigs `json:"cert_configs"`
38+}
39+
40+// NewWorkloadX509CertProvider creates a certificate source
41+// that reads a certificate and private key file from the local file system.
42+// This is intended to be used for workload identity federation.
43+//
44+// The configFilePath points to a config file containing relevant parameters
45+// such as the certificate and key file paths.
46+// If configFilePath is empty, the client will attempt to load the config from
47+// a well-known gcloud location.
48+func NewWorkloadX509CertProvider(configFilePath string) (Provider, error) {
49+ if configFilePath == "" {
50+ envFilePath := util.GetConfigFilePathFromEnv()
51+ if envFilePath != "" {
52+ configFilePath = envFilePath
53+ } else {
54+ configFilePath = util.GetDefaultConfigFilePath()
55+ }
56+ }
57+
58+ certFile, keyFile, err := getCertAndKeyFiles(configFilePath)
59+ if err != nil {
60+ return nil, err
61+ }
62+
63+ source := &workloadSource{
64+ CertPath: certFile,
65+ KeyPath: keyFile,
66+ }
67+ return source.getClientCertificate, nil
68+}
69+
70+// getClientCertificate attempts to load the certificate and key from the files specified in the
71+// certificate config.
72+func (s *workloadSource) getClientCertificate(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
73+ cert, err := tls.LoadX509KeyPair(s.CertPath, s.KeyPath)
74+ if err != nil {
75+ return nil, err
76+ }
77+ return &cert, nil
78+}
79+
80+// getCertAndKeyFiles attempts to read the provided config file and return the certificate and private
81+// key file paths.
82+func getCertAndKeyFiles(configFilePath string) (string, string, error) {
83+ jsonFile, err := os.Open(configFilePath)
84+ if err != nil {
85+ return "", "", errSourceUnavailable
86+ }
87+
88+ byteValue, err := io.ReadAll(jsonFile)
89+ if err != nil {
90+ return "", "", err
91+ }
92+
93+ var config certificateConfig
94+ if err := json.Unmarshal(byteValue, &config); err != nil {
95+ return "", "", err
96+ }
97+
98+ if config.CertConfigs.Workload == nil {
99+ return "", "", errSourceUnavailable
100+ }
101+
102+ certFile := config.CertConfigs.Workload.CertPath
103+ keyFile := config.CertConfigs.Workload.KeyPath
104+
105+ if certFile == "" {
106+ return "", "", errors.New("certificate configuration is missing the certificate file location")
107+ }
108+
109+ if keyFile == "" {
110+ return "", "", errors.New("certificate configuration is missing the key file location")
111+ }
112+
113+ return certFile, keyFile, nil
114+}
vendor/cloud.google.com/go/auth/internal/transport/s2a.gonew file+138
This diff is too big to show here.
vendor/cloud.google.com/go/auth/internal/transport/transport.gonew file+106
This diff is too big to show here.
vendor/cloud.google.com/go/auth/threelegged.gonew file+382
This diff is too big to show here.
vendor/cloud.google.com/go/civil/civil.gonew file+350
This diff is too big to show here.
vendor/cloud.google.com/go/compute/metadata/CHANGES.mdnew file+66
This diff is too big to show here.
vendor/cloud.google.com/go/compute/metadata/LICENSEnew file+202
This diff is too big to show here.
vendor/cloud.google.com/go/compute/metadata/README.mdnew file+27
This diff is too big to show here.
vendor/cloud.google.com/go/compute/metadata/log.gonew file+149
This diff is too big to show here.
vendor/cloud.google.com/go/compute/metadata/metadata.gonew file+872
This diff is too big to show here.
vendor/cloud.google.com/go/compute/metadata/retry.gonew file+114
This diff is too big to show here.
vendor/cloud.google.com/go/compute/metadata/retry_linux.gonew file+31
This diff is too big to show here.
vendor/cloud.google.com/go/compute/metadata/syscheck.gonew file+26
This diff is too big to show here.
vendor/cloud.google.com/go/compute/metadata/syscheck_linux.gonew file+28
This diff is too big to show here.
vendor/cloud.google.com/go/compute/metadata/syscheck_windows.gonew file+38
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/CHANGELOG.mdnew file+849
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/LICENSE.txtnew file+21
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/README.mdnew file+39
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/internal/resource/resource_identifier.gonew file+239
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/internal/resource/resource_type.gonew file+114
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/policy/policy.gonew file+108
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/runtime/pipeline.gonew file+70
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/runtime/policy_bearer_token.gonew file+102
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/runtime/policy_register_rp.gonew file+322
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/runtime/policy_trace_namespace.gonew file+30
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/runtime/runtime.gonew file+24
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/ci.ymlnew file+29
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud/cloud.gonew file+44
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud/doc.gonew file+53
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/core.gonew file+173
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/doc.gonew file+264
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/errors.gonew file+17
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/etag.gonew file+57
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported/exported.gonew file+175
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported/pipeline.gonew file+77
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported/request.gonew file+260
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported/response_error.gonew file+201
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/log/log.gonew file+50
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/async/async.gonew file+159
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/body/body.gonew file+135
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/fake/fake.gonew file+133
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/loc/loc.gonew file+123
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/op/op.gonew file+148
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/poller.gonew file+24
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/util.gonew file+212
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared/constants.gonew file+44
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared/shared.gonew file+149
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/log/doc.gonew file+10
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/log/log.gonew file+55
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/policy/doc.gonew file+10
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/policy/policy.gonew file+198
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/doc.gonew file+10
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/errors.gonew file+27
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/pager.gonew file+138
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/pipeline.gonew file+94
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_api_version.gonew file+75
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_bearer_token.gonew file+236
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_body_download.gonew file+72
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_http_header.gonew file+40
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_http_trace.gonew file+154
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_include_response.gonew file+35
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_key_credential.gonew file+64
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_logging.gonew file+264
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_request_id.gonew file+34
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_retry.gonew file+276
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_sas_credential.gonew file+55
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/policy_telemetry.gonew file+83
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/poller.gonew file+396
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/request.gonew file+281
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/response.gonew file+109
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/transport_default_dialer_other.gonew file+15
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/transport_default_dialer_wasm.gonew file+15
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime/transport_default_http_client.gonew file+48
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming/doc.gonew file+9
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming/progress.gonew file+89
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing/constants.gonew file+41
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing/tracing.gonew file+191
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/.gitignorenew file+4
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/CHANGELOG.mdnew file+575
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/LICENSE.txtnew file+21
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/MIGRATION.mdnew file+307
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/README.mdnew file+258
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/TOKEN_CACHING.MDnew file+71
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/TROUBLESHOOTING.mdnew file+241
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/assets.jsonnew file+6
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/authentication_record.gonew file+95
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/azidentity.gonew file+190
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/azure_cli_credential.gonew file+190
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/azure_developer_cli_credential.gonew file+169
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/azure_pipelines_credential.gonew file+140
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/chained_token_credential.gonew file+138
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/ci.ymlnew file+46
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/client_assertion_credential.gonew file+85
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/client_certificate_credential.gonew file+174
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/client_secret_credential.gonew file+75
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/confidential_client.gonew file+184
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/default_azure_credential.gonew file+165
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/developer_credential_util.gonew file+38
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/device_code_credential.gonew file+138
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/environment_credential.gonew file+167
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/errors.gonew file+170
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/go.work.sumnew file+60
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/interactive_browser_credential.gonew file+118
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/internal/exported.gonew file+18
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/internal/internal.gonew file+31
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/logging.gonew file+14
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/managed-identity-matrix.jsonnew file+17
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/managed_identity_client.gonew file+501
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/managed_identity_credential.gonew file+128
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/on_behalf_of_credential.gonew file+113
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/public_client.gonew file+273
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/test-resources-post.ps1new file+112
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/test-resources-pre.ps1new file+44
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/test-resources.bicepnew file+219
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/username_password_credential.gonew file+90
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/version.gonew file+18
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/workload_identity.gonew file+131
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/LICENSE.txtnew file+21
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/diag/diag.gonew file+51
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/diag/doc.gonew file+7
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/errorinfo/doc.gonew file+7
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/errorinfo/errorinfo.gonew file+46
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/exported/exported.gonew file+129
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/log/doc.gonew file+7
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/log/log.gonew file+104
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/poller/util.gonew file+155
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/temporal/resource.gonew file+123
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/uuid/doc.gonew file+7
This diff is too big to show here.
vendor/github.com/Azure/azure-sdk-for-go/sdk/internal/uuid/uuid.gonew file+76
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/LICENSEnew file+21
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache/cache.gonew file+54
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential/confidential.gonew file+719
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/errors/error_design.mdnew file+111
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/errors/errors.gonew file+89
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/base.gonew file+477
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/internal/storage/items.gonew file+213
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/internal/storage/partitioned_storage.gonew file+442
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/internal/storage/storage.gonew file+583
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/exported/exported.gonew file+34
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/json/design.mdnew file+140
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/json/json.gonew file+184
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/json/mapslice.gonew file+333
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/json/marshal.gonew file+346
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/json/struct.gonew file+290
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/json/types/time/time.gonew file+70
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/local/server.gonew file+177
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/oauth.gonew file+354
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens/accesstokens.gonew file+457
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens/apptype_string.gonew file+25
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens/tokens.gonew file+339
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority/authority.gonew file+589
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority/authorizetype_string.gonew file+30
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/internal/comm/comm.gonew file+320
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/internal/comm/compress.gonew file+33
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/internal/grant/grant.gonew file+17
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/ops.gonew file+56
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/wstrust/defs/endpointtype_string.gonew file+25
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/wstrust/defs/mex_document_definitions.gonew file+394
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/wstrust/defs/saml_assertion_definitions.gonew file+230
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/wstrust/defs/version_string.gonew file+25
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/wstrust/defs/wstrust_endpoint.gonew file+199
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/wstrust/defs/wstrust_mex_document.gonew file+159
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/wstrust/wstrust.gonew file+136
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/resolvers.gonew file+149
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/options/options.gonew file+52
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared/shared.gonew file+72
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/version/version.gonew file+8
This diff is too big to show here.
vendor/github.com/AzureAD/microsoft-authentication-library-for-go/apps/public/public.gonew file+756
This diff is too big to show here.
vendor/github.com/JohannesKaufmann/html-to-markdown/.gitignorenew file+14
This diff is too big to show here.
vendor/github.com/JohannesKaufmann/html-to-markdown/CONTRIBUTING.mdnew file
No changes to the content.
vendor/github.com/JohannesKaufmann/html-to-markdown/LICENSEnew file+21
This diff is too big to show here.
vendor/github.com/JohannesKaufmann/html-to-markdown/README.mdnew file+242
This diff is too big to show here.
vendor/github.com/JohannesKaufmann/html-to-markdown/SECURITY.mdnew file+6
This diff is too big to show here.
vendor/github.com/JohannesKaufmann/html-to-markdown/commonmark.gonew file+393
This diff is too big to show here.
vendor/github.com/JohannesKaufmann/html-to-markdown/escape/escape.gonew file+65
This diff is too big to show here.
vendor/github.com/JohannesKaufmann/html-to-markdown/from.gonew file+464
This diff is too big to show here.
vendor/github.com/JohannesKaufmann/html-to-markdown/logo_five_years.pngnew file
Binary file not shown.
vendor/github.com/JohannesKaufmann/html-to-markdown/markdown.gonew file+212
This diff is too big to show here.
vendor/github.com/JohannesKaufmann/html-to-markdown/utils.gonew file+533
This diff is too big to show here.
vendor/github.com/MakeNowJust/heredoc/LICENSEnew file+21
This diff is too big to show here.
vendor/github.com/MakeNowJust/heredoc/README.mdnew file+52
This diff is too big to show here.
vendor/github.com/MakeNowJust/heredoc/heredoc.gonew file+105
This diff is too big to show here.
vendor/github.com/PuerkitoBio/goquery/.gitattributesnew file+1
This diff is too big to show here.
vendor/github.com/PuerkitoBio/goquery/.gitignorenew file+16
This diff is too big to show here.
vendor/github.com/PuerkitoBio/goquery/LICENSEnew file+12
This diff is too big to show here.
vendor/github.com/PuerkitoBio/goquery/README.mdnew file+202
This diff is too big to show here.
vendor/github.com/PuerkitoBio/goquery/array.gonew file+124
This diff is too big to show here.
vendor/github.com/PuerkitoBio/goquery/doc.gonew file+123
This diff is too big to show here.
vendor/github.com/PuerkitoBio/goquery/expand.gonew file+70
This diff is too big to show here.
vendor/github.com/PuerkitoBio/goquery/filter.gonew file+163
This diff is too big to show here.
vendor/github.com/PuerkitoBio/goquery/iteration.gonew file+47
This diff is too big to show here.
vendor/github.com/PuerkitoBio/goquery/manipulation.gonew file+679
This diff is too big to show here.
vendor/github.com/PuerkitoBio/goquery/property.gonew file+275
This diff is too big to show here.
vendor/github.com/PuerkitoBio/goquery/query.gonew file+49
This diff is too big to show here.
vendor/github.com/PuerkitoBio/goquery/traversal.gonew file+704
This diff is too big to show here.
vendor/github.com/PuerkitoBio/goquery/type.gonew file+203
This diff is too big to show here.
vendor/github.com/PuerkitoBio/goquery/utilities.gonew file+178
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/.editorconfignew file+17
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/.gitignorenew file+25
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/.golangci.ymlnew file+95
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/.goreleaser.ymlnew file+37
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/Bitfilenew file+24
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/COPYINGnew file+19
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/Makefilenew file+23
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/README.mdnew file+297
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/coalesce.gonew file+35
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/colour.gonew file+192
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/delegate.gonew file+152
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/doc.gonew file+7
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/emitters.gonew file+218
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/formatter.gonew file+43
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/formatters/api.gonew file+57
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/formatters/html/html.gonew file+623
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/formatters/json.gonew file+39
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/formatters/svg/font_liberation_mono.gonew file+51
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/formatters/svg/svg.gonew file+222
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/formatters/tokens.gonew file+18
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/formatters/tty_indexed.gonew file+284
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/formatters/tty_truecolour.gonew file+76
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/iterator.gonew file+76
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexer.gonew file+162
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/README.mdnew file+46
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/caddyfile.gonew file+275
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/cl.gonew file+243
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/dns.gonew file+17
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/emacs.gonew file+533
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/abap.xmlnew file+154
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/abnf.xmlnew file+66
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/actionscript.xmlnew file+68
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/actionscript_3.xmlnew file+163
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ada.xmlnew file+321
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/agda.xmlnew file+66
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/al.xmlnew file+75
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/alloy.xmlnew file+58
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/angular2.xmlnew file+108
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/antlr.xmlnew file+317
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/apacheconf.xmlnew file+74
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/apl.xmlnew file+59
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/applescript.xmlnew file+130
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/arangodb_aql.xmlnew file+174
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/arduino.xmlnew file+309
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/armasm.xmlnew file+126
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/atl.xmlnew file+165
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/autohotkey.xmlnew file+78
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/autoit.xmlnew file+70
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/awk.xmlnew file+95
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ballerina.xmlnew file+97
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/bash.xmlnew file+220
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/bash_session.xmlnew file+25
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/batchfile.xmlnew file+660
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/beef.xmlnew file+120
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/bibtex.xmlnew file+152
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/bicep.xmlnew file+84
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/blitzbasic.xmlnew file+141
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/bnf.xmlnew file+28
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/bqn.xmlnew file+83
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/brainfuck.xmlnew file+51
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/c#.xmlnew file+121
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/c++.xmlnew file+331
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/c.xmlnew file+260
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/cap_n_proto.xmlnew file+122
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/cassandra_cql.xmlnew file+137
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ceylon.xmlnew file+151
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/cfengine3.xmlnew file+197
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/cfstatement.xmlnew file+92
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/chaiscript.xmlnew file+134
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/chapel.xmlnew file+143
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/cheetah.xmlnew file+55
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/clojure.xmlnew file+71
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/cmake.xmlnew file+90
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/cobol.xmlnew file+90
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/coffeescript.xmlnew file+210
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/common_lisp.xmlnew file+184
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/coq.xmlnew file+136
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/crystal.xmlnew file+762
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/css.xmlnew file+323
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/csv.xmlnew file+53
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/cue.xmlnew file+85
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/cython.xmlnew file+372
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/d.xmlnew file+133
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/dart.xmlnew file+213
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/dax.xmlnew file+39
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/desktop_entry.xmlnew file+17
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/diff.xmlnew file+52
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/django_jinja.xmlnew file+153
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/dns.xmlnew file+44
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/docker.xmlnew file+57
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/dtd.xmlnew file+168
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/dylan.xmlnew file+176
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ebnf.xmlnew file+90
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/elixir.xmlnew file+744
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/elm.xmlnew file+119
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/emacslisp.xmlnew file+132
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/erlang.xmlnew file+166
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/factor.xmlnew file+412
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/fennel.xmlnew file+68
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/fish.xmlnew file+159
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/forth.xmlnew file+78
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/fortran.xmlnew file+102
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/fortranfixed.xmlnew file+71
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/fsharp.xmlnew file+245
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/gas.xmlnew file+150
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/gdscript.xmlnew file+259
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/gdscript3.xmlnew file+270
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/gherkin.xmlnew file+263
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/gleam.xmlnew file+117
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/glsl.xmlnew file+65
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/gnuplot.xmlnew file+289
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/go_template.xmlnew file+114
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/graphql.xmlnew file+88
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/groff.xmlnew file+90
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/groovy.xmlnew file+135
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/handlebars.xmlnew file+147
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/hare.xmlnew file+98
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/haskell.xmlnew file+275
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/hcl.xmlnew file+143
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/hexdump.xmlnew file+189
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/hlb.xmlnew file+149
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/hlsl.xmlnew file+110
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/holyc.xmlnew file+252
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/html.xmlnew file+159
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/hy.xmlnew file+104
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/idris.xmlnew file+216
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/igor.xmlnew file+47
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ini.xmlnew file+45
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/io.xmlnew file+71
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/iscdhcpd.xmlnew file+96
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/j.xmlnew file+157
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/java.xmlnew file+193
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/javascript.xmlnew file+160
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/json.xmlnew file+112
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/jsonata.xmlnew file+83
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/jsonnet.xmlnew file+138
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/julia.xmlnew file+400
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/jungle.xmlnew file+98
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/kotlin.xmlnew file+223
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/lighttpd_configuration_file.xmlnew file+42
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/llvm.xmlnew file+73
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/lua.xmlnew file+158
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/makefile.xmlnew file+131
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/mako.xmlnew file+120
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/mason.xmlnew file+89
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/materialize_sql_dialect.xmlnew file+155
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/mathematica.xmlnew file+60
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/matlab.xmlnew file+114
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/mcfunction.xmlnew file+138
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/meson.xmlnew file+85
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/metal.xmlnew file+270
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/minizinc.xmlnew file+82
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/mlir.xmlnew file+73
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/modula-2.xmlnew file+245
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/monkeyc.xmlnew file+153
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/morrowindscript.xmlnew file+90
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/myghty.xmlnew file+77
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/mysql.xmlnew file+121
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/nasm.xmlnew file+126
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/natural.xmlnew file+143
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ndisasm.xmlnew file+123
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/newspeak.xmlnew file+121
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/nginx_configuration_file.xmlnew file+98
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/nim.xmlnew file+211
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/nix.xmlnew file+258
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/nsis.xmlnew file+59
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/objective-c.xmlnew file+510
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/objectpascal.xmlnew file+145
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ocaml.xmlnew file+145
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/octave.xmlnew file+101
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/odin.xmlnew file+113
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/onesenterprise.xmlnew file+92
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/openedge_abl.xmlnew file+101
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/openscad.xmlnew file+96
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/org_mode.xmlnew file+329
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/pacmanconf.xmlnew file+37
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/perl.xmlnew file+400
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/php.xmlnew file+212
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/pig.xmlnew file+105
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/pkgconfig.xmlnew file+73
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/pl_pgsql.xmlnew file+119
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/plaintext.xmlnew file+21
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/plutus_core.xmlnew file+105
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/pony.xmlnew file+135
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/postgresql_sql_dialect.xmlnew file+155
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/postscript.xmlnew file+89
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/povray.xmlnew file+58
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/powerquery.xmlnew file+51
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/powershell.xmlnew file+230
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/prolog.xmlnew file+115
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/promela.xmlnew file+119
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/promql.xmlnew file+123
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/properties.xmlnew file+45
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/protocol_buffer.xmlnew file+118
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/prql.xmlnew file+161
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/psl.xmlnew file+213
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/puppet.xmlnew file+100
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/python.xmlnew file+593
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/python_2.xmlnew file+356
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/qbasic.xmlnew file+173
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/qml.xmlnew file+113
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/r.xmlnew file+128
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/racket.xmlnew file+260
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ragel.xmlnew file+149
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/react.xmlnew file+236
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/reasonml.xmlnew file+147
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/reg.xmlnew file+68
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/rego.xmlnew file+94
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/rexx.xmlnew file+127
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/rpm_spec.xmlnew file+58
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ruby.xmlnew file+724
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/rust.xmlnew file+375
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/sas.xmlnew file+191
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/sass.xmlnew file+362
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/scala.xmlnew file+274
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/scheme.xmlnew file+106
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/scilab.xmlnew file+98
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/scss.xmlnew file+373
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/sed.xmlnew file+28
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/sieve.xmlnew file+61
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/smali.xmlnew file+73
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/smalltalk.xmlnew file+294
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/smarty.xmlnew file+79
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/snbt.xmlnew file+58
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/snobol.xmlnew file+95
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/solidity.xmlnew file+279
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/sourcepawn.xmlnew file+59
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/sparql.xmlnew file+160
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/sql.xmlnew file+90
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/squidconf.xmlnew file+63
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/standard_ml.xmlnew file+548
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/stas.xmlnew file+85
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/stylus.xmlnew file+132
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/swift.xmlnew file+207
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/systemd.xmlnew file+63
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/systemverilog.xmlnew file+181
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/tablegen.xmlnew file+69
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/tal.xmlnew file+43
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/tasm.xmlnew file+135
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/tcl.xmlnew file+272
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/tcsh.xmlnew file+121
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/termcap.xmlnew file+75
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/terminfo.xmlnew file+84
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/terraform.xmlnew file+140
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/tex.xmlnew file+113
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/thrift.xmlnew file+154
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/toml.xmlnew file+44
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/tradingview.xmlnew file+81
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/transact-sql.xmlnew file+137
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/turing.xmlnew file+82
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/turtle.xmlnew file+170
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/twig.xmlnew file+155
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/typescript.xmlnew file+295
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/typoscript.xmlnew file+178
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/typoscriptcssdata.xmlnew file+52
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/typoscripthtmldata.xmlnew file+52
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/typst.xmlnew file+108
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/ucode.xmlnew file+147
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/v.xmlnew file+355
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/v_shell.xmlnew file+365
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/vala.xmlnew file+72
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/vb_net.xmlnew file+162
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/verilog.xmlnew file+158
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/vhdl.xmlnew file+171
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/vhs.xmlnew file+48
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/viml.xmlnew file+85
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/vue.xmlnew file+307
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/wdte.xmlnew file+43
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/webgpu_shading_language.xmlnew file+142
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/webvtt.xmlnew file+283
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/whiley.xmlnew file+57
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/xml.xmlnew file+95
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/xorg.xmlnew file+35
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/yaml.xmlnew file+122
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/yang.xmlnew file+99
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/z80_assembly.xmlnew file+74
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/zed.xmlnew file+51
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/embedded/zig.xmlnew file+112
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/genshi.gonew file+118
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/go.gonew file+81
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/haxe.gonew file+647
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/html.gonew file+8
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/http.gonew file+131
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/lexers.gonew file+79
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/markdown.gonew file+46
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/mysql.gonew file+33
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/php.gonew file+37
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/raku.gonew file+1721
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/rst.gonew file+89
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/svelte.gonew file+70
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/typoscript.gonew file+85
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/lexers/zed.gonew file+24
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/mutators.gonew file+201
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/pygments-lexers.txtnew file+322
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/quick/quick.gonew file+44
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/regexp.gonew file+489
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/registry.gonew file+210
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/remap.gonew file+94
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/renovate.json5new file+18
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/serialise.gonew file+479
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/style.gonew file+481
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/abap.xmlnew file+11
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/algol.xmlnew file+18
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/algol_nu.xmlnew file+18
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/api.gonew file+65
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/arduino.xmlnew file+18
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/autumn.xmlnew file+36
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/average.xmlnew file+74
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/base16-snazzy.xmlnew file+74
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/borland.xmlnew file+26
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/bw.xmlnew file+23
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/catppuccin-frappe.xmlnew file+83
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/catppuccin-latte.xmlnew file+83
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/catppuccin-macchiato.xmlnew file+83
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/catppuccin-mocha.xmlnew file+83
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/colorful.xmlnew file+52
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/compat.gonew file+66
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/doom-one.xmlnew file+51
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/doom-one2.xmlnew file+64
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/dracula.xmlnew file+74
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/emacs.xmlnew file+44
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/evergarden.xmlnew file+33
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/friendly.xmlnew file+44
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/fruity.xmlnew file+19
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/github-dark.xmlnew file+45
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/github.xmlnew file+39
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/gruvbox-light.xmlnew file+33
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/gruvbox.xmlnew file+33
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/hr_high_contrast.xmlnew file+12
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/hrdark.xmlnew file+10
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/igor.xmlnew file+9
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/lovelace.xmlnew file+53
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/manni.xmlnew file+44
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/modus-operandi.xmlnew file+13
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/modus-vivendi.xmlnew file+13
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/monokai.xmlnew file+29
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/monokailight.xmlnew file+26
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/murphy.xmlnew file+52
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/native.xmlnew file+35
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/nord.xmlnew file+46
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/nordic.xmlnew file+46
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/onedark.xmlnew file+25
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/onesenterprise.xmlnew file+10
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/paraiso-dark.xmlnew file+37
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/paraiso-light.xmlnew file+37
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/pastie.xmlnew file+45
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/perldoc.xmlnew file+37
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/pygments.xmlnew file+42
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/rainbow_dash.xmlnew file+40
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/rose-pine-dawn.xmlnew file+29
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/rose-pine-moon.xmlnew file+29
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/rose-pine.xmlnew file+29
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/rrt.xmlnew file+13
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/solarized-dark.xmlnew file+39
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/solarized-dark256.xmlnew file+41
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/solarized-light.xmlnew file+17
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/swapoff.xmlnew file+18
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/tango.xmlnew file+72
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/tokyonight-day.xmlnew file+83
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/tokyonight-moon.xmlnew file+83
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/tokyonight-night.xmlnew file+83
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/tokyonight-storm.xmlnew file+83
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/trac.xmlnew file+35
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/vim.xmlnew file+29
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/vs.xmlnew file+16
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/vulcan.xmlnew file+74
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/witchhazel.xmlnew file+31
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/xcode-dark.xmlnew file+31
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/styles/xcode.xmlnew file+22
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/table.pynew file+31
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/tokentype_enumer.gonew file+583
This diff is too big to show here.
vendor/github.com/alecthomas/chroma/v2/types.gonew file+343
This diff is too big to show here.
vendor/github.com/andybalholm/cascadia/.travis.ymlnew file+14
This diff is too big to show here.
vendor/github.com/andybalholm/cascadia/LICENSEnew file+24
This diff is too big to show here.
vendor/github.com/andybalholm/cascadia/README.mdnew file+144
This diff is too big to show here.
vendor/github.com/andybalholm/cascadia/parser.gonew file+889
This diff is too big to show here.
vendor/github.com/andybalholm/cascadia/pseudo_classes.gonew file+458
This diff is too big to show here.
vendor/github.com/andybalholm/cascadia/selector.gonew file+586
This diff is too big to show here.
vendor/github.com/andybalholm/cascadia/serialize.gonew file+176
This diff is too big to show here.
vendor/github.com/andybalholm/cascadia/specificity.gonew file+26
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/.gitignorenew file+4
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/.release-please-manifest.jsonnew file+3
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/.stats.ymlnew file+4
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/Brewfilenew file+1
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/CHANGELOG.mdnew file+240
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/CONTRIBUTING.mdnew file+66
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/LICENSEnew file+8
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/README.mdnew file+876
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/SECURITY.mdnew file+27
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/aliases.gonew file+50
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/api.mdnew file+329
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/bedrock/bedrock.gonew file+245
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/beta.gonew file+364
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/betafile.gonew file+270
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/betamessage.gonew file+5823
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/betamessagebatch.gonew file+879
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/betamodel.gonew file+149
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/client.gonew file+126
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/completion.gonew file+194
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/field.gonew file+45
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/apierror/apierror.gonew file+50
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/apiform/encoder.gonew file+465
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/apiform/form.gonew file+5
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/apiform/richparam.gonew file+20
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/apiform/tag.gonew file+51
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/apijson/decoder.gonew file+691
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/apijson/encoder.gonew file+392
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/apijson/enum.gonew file+145
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/apijson/field.gonew file+23
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/apijson/port.gonew file+120
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/apijson/registry.gonew file+51
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/apijson/subfield.gonew file+67
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/apijson/tag.gonew file+47
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/apijson/union.gonew file+202
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/apiquery/encoder.gonew file+415
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/apiquery/query.gonew file+55
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/apiquery/richparam.gonew file+19
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/apiquery/tag.gonew file+44
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/encoding/json/decode.gonew file+1324
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/encoding/json/encode.gonew file+1398
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/encoding/json/fold.gonew file+48
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/encoding/json/indent.gonew file+182
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/encoding/json/scanner.gonew file+610
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/encoding/json/sentinel/null.gonew file+57
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/encoding/json/shims/shims.gonew file+111
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/encoding/json/stream.gonew file+512
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/encoding/json/tables.gonew file+218
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/encoding/json/tags.gonew file+38
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/encoding/json/time.gonew file+61
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/paramutil/field.gonew file+30
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/paramutil/sentinel.gonew file+31
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/paramutil/union.gonew file+48
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/requestconfig/requestconfig.gonew file+629
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/internal/version.gonew file+5
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/message.gonew file+4562
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/messagebatch.gonew file+825
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/model.gonew file+149
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/option/requestoption.gonew file+284
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/packages/jsonl/jsonl.gonew file+57
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/packages/pagination/pagination.gonew file+134
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/packages/param/encoder.gonew file+101
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/packages/param/option.gonew file+121
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/packages/param/param.gonew file+158
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/packages/respjson/respjson.gonew file+88
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/packages/ssestream/ssestream.gonew file+198
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/release-please-config.jsonnew file+67
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/shared/constant/constants.gonew file+304
This diff is too big to show here.
vendor/github.com/anthropics/anthropic-sdk-go/shared/shared.gonew file+334
This diff is too big to show here.
vendor/github.com/atotto/clipboard/.travis.ymlnew file+22
This diff is too big to show here.
vendor/github.com/atotto/clipboard/LICENSEnew file+27
This diff is too big to show here.
vendor/github.com/atotto/clipboard/README.mdnew file+48
This diff is too big to show here.
vendor/github.com/atotto/clipboard/clipboard.gonew file+20
This diff is too big to show here.
vendor/github.com/atotto/clipboard/clipboard_darwin.gonew file+52
This diff is too big to show here.
vendor/github.com/atotto/clipboard/clipboard_plan9.gonew file+42
This diff is too big to show here.
vendor/github.com/atotto/clipboard/clipboard_unix.gonew file+149
This diff is too big to show here.
vendor/github.com/atotto/clipboard/clipboard_windows.gonew file+157
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/LICENSE.txtnew file+202
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/NOTICE.txtnew file+3
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/accountid_endpoint_mode.gonew file+18
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/config.gonew file+211
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/context.gonew file+22
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/credential_cache.gonew file+224
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/credentials.gonew file+173
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/defaults/auto.gonew file+38
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/defaults/configuration.gonew file+43
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/defaults/defaults.gonew file+50
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/defaults/doc.gonew file+2
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/defaultsmode.gonew file+95
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/doc.gonew file+62
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/endpoints.gonew file+247
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/errors.gonew file+9
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/from_ptr.gonew file+365
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/go_module_metadata.gonew file+6
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/logging.gonew file+119
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/logging_generate.gonew file+95
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/middleware/metadata.gonew file+213
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/middleware/middleware.gonew file+168
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/middleware/osname.gonew file+24
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/middleware/osname_go115.gonew file+24
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/middleware/private/metrics/metrics.gonew file+320
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/middleware/recursion_detection.gonew file+94
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/middleware/request_id.gonew file+27
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/middleware/request_id_retriever.gonew file+53
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/middleware/user_agent.gonew file+305
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/CHANGELOG.mdnew file+114
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/LICENSE.txtnew file+202
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/debug.gonew file+144
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/decode.gonew file+218
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/encode.gonew file+167
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/error.gonew file+23
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/eventstreamapi/headers.gonew file+24
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/eventstreamapi/middleware.gonew file+71
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/eventstreamapi/transport.gonew file+13
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/eventstreamapi/transport_go117.gonew file+12
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/go_module_metadata.gonew file+6
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/header.gonew file+175
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/header_value.gonew file+521
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/message.gonew file+117
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/query/array.gonew file+72
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/query/encoder.gonew file+80
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/query/map.gonew file+78
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/query/middleware.gonew file+62
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/query/object.gonew file+69
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/query/value.gonew file+115
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/restjson/decoder_util.gonew file+85
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/protocol/xml/error_utils.gonew file+48
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/ratelimit/none.gonew file+20
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/ratelimit/token_bucket.gonew file+96
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/ratelimit/token_rate_limit.gonew file+83
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/request.gonew file+25
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/retry/adaptive.gonew file+156
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/retry/adaptive_ratelimit.gonew file+158
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/retry/adaptive_token_bucket.gonew file+83
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/retry/doc.gonew file+80
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/retry/errors.gonew file+20
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/retry/jitter_backoff.gonew file+49
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/retry/metadata.gonew file+52
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/retry/middleware.gonew file+383
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/retry/retry.gonew file+90
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/retry/retryable_error.gonew file+222
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/retry/standard.gonew file+269
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/retry/throttle_error.gonew file+60
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/retry/timeout_error.gonew file+52
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/retryer.gonew file+127
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/runtime.gonew file+14
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4/cache.gonew file+115
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4/const.gonew file+40
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4/header_rules.gonew file+82
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4/headers.gonew file+70
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4/hmac.gonew file+13
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4/host.gonew file+75
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4/scope.gonew file+13
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4/time.gonew file+36
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4/util.gonew file+80
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/signer/v4/middleware.gonew file+414
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/signer/v4/presign_middleware.gonew file+127
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/signer/v4/stream.gonew file+86
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/signer/v4/v4.gonew file+559
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/to_ptr.gonew file+297
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/transport/http/client.gonew file+310
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/transport/http/content_type.gonew file+42
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/transport/http/response_error.gonew file+33
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/transport/http/response_error_middleware.gonew file+56
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/transport/http/timeout_read_closer.gonew file+104
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/types.gonew file+42
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/aws/version.gonew file+8
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/config/CHANGELOG.mdnew file+678
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/config/LICENSE.txtnew file+202
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/config/config.gonew file+222
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/config/defaultsmode.gonew file+47
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/config/doc.gonew file+20
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/config/env_config.gonew file+856
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/config/generate.gonew file+4
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/config/go_module_metadata.gonew file+6
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/config/load_options.gonew file+1141
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/config/local.gonew file+51
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/config/provider.gonew file+721
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/config/resolve.gonew file+383
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/config/resolve_bearer_token.gonew file+122
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/config/resolve_credentials.gonew file+566
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/config/shared_config.gonew file+1618
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/CHANGELOG.mdnew file+591
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/LICENSE.txtnew file+202
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/doc.gonew file+4
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds/doc.gonew file+58
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds/provider.gonew file+229
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/internal/client/auth.gonew file+48
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/internal/client/client.gonew file+165
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/internal/client/endpoints.gonew file+20
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/internal/client/middleware.gonew file+164
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/provider.gonew file+193
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/go_module_metadata.gonew file+6
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/processcreds/doc.gonew file+92
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/processcreds/provider.gonew file+285
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/ssocreds/doc.gonew file+81
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/ssocreds/sso_cached_token.gonew file+233
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/ssocreds/sso_credentials_provider.gonew file+153
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/ssocreds/sso_token_provider.gonew file+147
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/static_provider.gonew file+53
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/stscreds/assume_role_provider.gonew file+326
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/credentials/stscreds/web_identity_provider.gonew file+169
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/CHANGELOG.mdnew file+355
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/LICENSE.txtnew file+202
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/api_client.gonew file+352
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/api_op_GetDynamicData.gonew file+77
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/api_op_GetIAMInfo.gonew file+103
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/api_op_GetInstanceIdentityDocument.gonew file+110
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/api_op_GetMetadata.gonew file+77
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/api_op_GetRegion.gonew file+73
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/api_op_GetToken.gonew file+119
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/api_op_GetUserData.gonew file+61
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/auth.gonew file+48
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/doc.gonew file+12
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/endpoints.gonew file+20
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/go_module_metadata.gonew file+6
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/internal/config/resolvers.gonew file+114
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/request_middleware.gonew file+313
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/feature/ec2/imds/token_provider.gonew file+261
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/auth/auth.gonew file+45
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/auth/scheme.gonew file+191
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/auth/smithy/bearer_token_adapter.gonew file+43
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/auth/smithy/bearer_token_signer_adapter.gonew file+35
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/auth/smithy/credentials_adapter.gonew file+46
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/auth/smithy/smithy.gonew file+2
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/auth/smithy/v4signer_adapter.gonew file+57
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/CHANGELOG.mdnew file+320
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/LICENSE.txtnew file+202
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/config.gonew file+65
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/endpoints.gonew file+57
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/configsources/go_module_metadata.gonew file+6
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/context/context.gonew file+52
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn/arn.gonew file+94
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn/doc.gonew file+3
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn/generate.gonew file+7
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn/host.gonew file+51
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn/partition.gonew file+76
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn/partitions.gonew file+403
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn/partitions.jsonnew file+220
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/endpoints.gonew file+201
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/CHANGELOG.mdnew file+294
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/LICENSE.txtnew file+202
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/endpoints.gonew file+302
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2/go_module_metadata.gonew file+6
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/ini/CHANGELOG.mdnew file+271
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/ini/LICENSE.txtnew file+202
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/ini/errors.gonew file+22
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/ini/go_module_metadata.gonew file+6
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/ini/ini.gonew file+56
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/ini/parse.gonew file+109
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/ini/sections.gonew file+157
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/ini/strings.gonew file+89
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/ini/token.gonew file+32
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/ini/tokenize.gonew file+92
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/ini/value.gonew file+93
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/middleware/middleware.gonew file+42
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/rand/rand.gonew file+33
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/sdk/interfaces.gonew file+9
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/sdk/time.gonew file+74
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/sdkio/byte.gonew file+12
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/shareddefaults/shared_config.gonew file+47
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/strings/strings.gonew file+11
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight/LICENSEnew file+28
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight/docs.gonew file+7
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight/singleflight.gonew file+210
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/internal/timeconv/duration.gonew file+13
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/CHANGELOG.mdnew file+140
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/LICENSE.txtnew file+202
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/accept_encoding_gzip.gonew file+176
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/doc.gonew file+22
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding/go_module_metadata.gonew file+6
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/CHANGELOG.mdnew file+346
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/LICENSE.txtnew file+202
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/context.gonew file+56
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/doc.gonew file+3
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/go_module_metadata.gonew file+6
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url/middleware.gonew file+110
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/CHANGELOG.mdnew file+475
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/LICENSE.txtnew file+202
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/api_client.gonew file+627
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/api_op_GetRoleCredentials.gonew file+153
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/api_op_ListAccountRoles.gonew file+251
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/api_op_ListAccounts.gonew file+249
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/api_op_Logout.gonew file+152
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/auth.gonew file+308
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/deserializers.gonew file+1161
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/doc.gonew file+27
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/endpoints.gonew file+550
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/generated.jsonnew file+35
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/go_module_metadata.gonew file+6
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/internal/endpoints/endpoints.gonew file+566
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/options.gonew file+227
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/serializers.gonew file+284
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/types/errors.gonew file+115
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/types/types.gonew file+63
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sso/validators.gonew file+175
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/CHANGELOG.mdnew file+469
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/LICENSE.txtnew file+202
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/api_client.gonew file+627
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/api_op_CreateToken.gonew file+225
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/api_op_CreateTokenWithIAM.gonew file+256
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/api_op_RegisterClient.gonew file+186
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/api_op_StartDeviceAuthorization.gonew file+176
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/auth.gonew file+302
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/deserializers.gonew file+2167
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/doc.gonew file+46
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/endpoints.gonew file+550
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/generated.jsonnew file+35
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/go_module_metadata.gonew file+6
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/internal/endpoints/endpoints.gonew file+566
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/options.gonew file+227
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/serializers.gonew file+487
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/types/errors.gonew file+428
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/types/types.gonew file+9
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/ssooidc/validators.gonew file+184
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/CHANGELOG.mdnew file+493
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/LICENSE.txtnew file+202
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_client.gonew file+779
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_AssumeRole.gonew file+520
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_AssumeRoleWithSAML.gonew file+436
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_AssumeRoleWithWebIdentity.gonew file+447
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_DecodeAuthorizationMessage.gonew file+177
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_GetAccessKeyInfo.gonew file+168
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_GetCallerIdentity.gonew file+180
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_GetFederationToken.gonew file+381
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/api_op_GetSessionToken.gonew file+227
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/auth.gonew file+296
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/deserializers.gonew file+2516
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/doc.gonew file+13
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/endpoints.gonew file+1130
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/generated.jsonnew file+41
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/go_module_metadata.gonew file+6
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/internal/endpoints/endpoints.gonew file+512
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/options.gonew file+227
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/serializers.gonew file+862
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/types/errors.gonew file+248
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/types/types.gonew file+144
This diff is too big to show here.
vendor/github.com/aws/aws-sdk-go-v2/service/sts/validators.gonew file+305
This diff is too big to show here.
vendor/github.com/aws/smithy-go/.gitignorenew file+29
This diff is too big to show here.
vendor/github.com/aws/smithy-go/.travis.ymlnew file+28
This diff is too big to show here.
vendor/github.com/aws/smithy-go/CHANGELOG.mdnew file+239
This diff is too big to show here.
vendor/github.com/aws/smithy-go/CODE_OF_CONDUCT.mdnew file+4
This diff is too big to show here.
vendor/github.com/aws/smithy-go/CONTRIBUTING.mdnew file+59
This diff is too big to show here.
vendor/github.com/aws/smithy-go/LICENSEnew file+175
This diff is too big to show here.
vendor/github.com/aws/smithy-go/Makefilenew file+102
This diff is too big to show here.
vendor/github.com/aws/smithy-go/NOTICEnew file+1
This diff is too big to show here.
vendor/github.com/aws/smithy-go/README.mdnew file+27
This diff is too big to show here.
vendor/github.com/aws/smithy-go/auth/auth.gonew file+3
This diff is too big to show here.
vendor/github.com/aws/smithy-go/auth/bearer/docs.gonew file+3
This diff is too big to show here.
vendor/github.com/aws/smithy-go/auth/bearer/middleware.gonew file+104
This diff is too big to show here.
vendor/github.com/aws/smithy-go/auth/bearer/token.gonew file+50
This diff is too big to show here.
vendor/github.com/aws/smithy-go/auth/bearer/token_cache.gonew file+208
This diff is too big to show here.
vendor/github.com/aws/smithy-go/auth/identity.gonew file+47
This diff is too big to show here.
vendor/github.com/aws/smithy-go/auth/option.gonew file+25
This diff is too big to show here.
vendor/github.com/aws/smithy-go/auth/scheme_id.gonew file+20
This diff is too big to show here.
vendor/github.com/aws/smithy-go/context/suppress_expired.gonew file+81
This diff is too big to show here.
vendor/github.com/aws/smithy-go/doc.gonew file+2
This diff is too big to show here.
vendor/github.com/aws/smithy-go/document.gonew file+10
This diff is too big to show here.
vendor/github.com/aws/smithy-go/document/doc.gonew file+12
This diff is too big to show here.
vendor/github.com/aws/smithy-go/document/document.gonew file+153
This diff is too big to show here.
vendor/github.com/aws/smithy-go/document/errors.gonew file+75
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/doc.gonew file+4
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/encoding.gonew file+40
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/httpbinding/encode.gonew file+123
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/httpbinding/header.gonew file+122
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/httpbinding/path_replace.gonew file+108
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/httpbinding/query.gonew file+107
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/httpbinding/uri.gonew file+111
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/json/array.gonew file+35
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/json/constants.gonew file+15
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/json/decoder_util.gonew file+139
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/json/encoder.gonew file+30
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/json/escape.gonew file+198
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/json/object.gonew file+40
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/json/value.gonew file+149
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/xml/array.gonew file+49
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/xml/constants.gonew file+10
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/xml/doc.gonew file+49
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/xml/element.gonew file+91
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/xml/encoder.gonew file+51
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/xml/error_utils.gonew file+51
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/xml/escape.gonew file+137
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/xml/map.gonew file+53
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/xml/value.gonew file+302
This diff is too big to show here.
vendor/github.com/aws/smithy-go/encoding/xml/xml_decoder.gonew file+154
This diff is too big to show here.
vendor/github.com/aws/smithy-go/endpoints/endpoint.gonew file+23
This diff is too big to show here.
vendor/github.com/aws/smithy-go/errors.gonew file+137
This diff is too big to show here.
vendor/github.com/aws/smithy-go/go_module_metadata.gonew file+6
This diff is too big to show here.
vendor/github.com/aws/smithy-go/internal/sync/singleflight/LICENSEnew file+28
This diff is too big to show here.
vendor/github.com/aws/smithy-go/internal/sync/singleflight/docs.gonew file+8
This diff is too big to show here.
vendor/github.com/aws/smithy-go/internal/sync/singleflight/singleflight.gonew file+210
This diff is too big to show here.
vendor/github.com/aws/smithy-go/io/byte.gonew file+12
This diff is too big to show here.
vendor/github.com/aws/smithy-go/io/doc.gonew file+2
This diff is too big to show here.
vendor/github.com/aws/smithy-go/io/reader.gonew file+16
This diff is too big to show here.
vendor/github.com/aws/smithy-go/io/ringbuffer.gonew file+94
This diff is too big to show here.
vendor/github.com/aws/smithy-go/local-mod-replace.shnew file+39
This diff is too big to show here.
vendor/github.com/aws/smithy-go/logging/logger.gonew file+82
This diff is too big to show here.
vendor/github.com/aws/smithy-go/middleware/doc.gonew file+67
This diff is too big to show here.
vendor/github.com/aws/smithy-go/middleware/logging.gonew file+46
This diff is too big to show here.
vendor/github.com/aws/smithy-go/middleware/metadata.gonew file+65
This diff is too big to show here.
vendor/github.com/aws/smithy-go/middleware/middleware.gonew file+71
This diff is too big to show here.
vendor/github.com/aws/smithy-go/middleware/ordered_group.gonew file+268
This diff is too big to show here.
vendor/github.com/aws/smithy-go/middleware/stack.gonew file+209
This diff is too big to show here.
vendor/github.com/aws/smithy-go/middleware/stack_values.gonew file+100
This diff is too big to show here.
vendor/github.com/aws/smithy-go/middleware/step_build.gonew file+211
This diff is too big to show here.
vendor/github.com/aws/smithy-go/middleware/step_deserialize.gonew file+217
This diff is too big to show here.
vendor/github.com/aws/smithy-go/middleware/step_finalize.gonew file+211
This diff is too big to show here.
vendor/github.com/aws/smithy-go/middleware/step_initialize.gonew file+211
This diff is too big to show here.
vendor/github.com/aws/smithy-go/middleware/step_serialize.gonew file+219
This diff is too big to show here.
vendor/github.com/aws/smithy-go/modman.tomlnew file+10
This diff is too big to show here.
vendor/github.com/aws/smithy-go/private/requestcompression/gzip.gonew file+30
This diff is too big to show here.
vendor/github.com/aws/smithy-go/private/requestcompression/middleware_capture_request_compression.gonew file+52
This diff is too big to show here.
vendor/github.com/aws/smithy-go/private/requestcompression/request_compression.gonew file+103
This diff is too big to show here.
vendor/github.com/aws/smithy-go/properties.gonew file+62
This diff is too big to show here.
vendor/github.com/aws/smithy-go/ptr/doc.gonew file+5
This diff is too big to show here.
vendor/github.com/aws/smithy-go/ptr/from_ptr.gonew file+601
This diff is too big to show here.
vendor/github.com/aws/smithy-go/ptr/gen_scalars.gonew file+83
This diff is too big to show here.
vendor/github.com/aws/smithy-go/ptr/to_ptr.gonew file+499
This diff is too big to show here.
vendor/github.com/aws/smithy-go/rand/doc.gonew file+3
This diff is too big to show here.
vendor/github.com/aws/smithy-go/rand/rand.gonew file+31
This diff is too big to show here.
vendor/github.com/aws/smithy-go/rand/uuid.gonew file+87
This diff is too big to show here.
vendor/github.com/aws/smithy-go/time/time.gonew file+134
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/auth.gonew file+21
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/auth_schemes.gonew file+45
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/checksum_middleware.gonew file+70
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/client.gonew file+120
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/doc.gonew file+5
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/headerlist.gonew file+163
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/host.gonew file+89
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/internal/io/safe.gonew file+75
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/md5_checksum.gonew file+25
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/middleware_close_response_body.gonew file+79
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/middleware_content_length.gonew file+84
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/middleware_header_comment.gonew file+81
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/middleware_headers.gonew file+167
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/middleware_http_logging.gonew file+75
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/middleware_metadata.gonew file+51
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/middleware_min_proto.gonew file+79
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/properties.gonew file+80
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/request.gonew file+189
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/response.gonew file+34
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/time.gonew file+13
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/url.gonew file+44
This diff is too big to show here.
vendor/github.com/aws/smithy-go/transport/http/user_agent.gonew file+37
This diff is too big to show here.
vendor/github.com/aws/smithy-go/validation.gonew file+140
This diff is too big to show here.
vendor/github.com/aymanbagabas/go-osc52/v2/LICENSEnew file+21
This diff is too big to show here.
vendor/github.com/aymanbagabas/go-osc52/v2/README.mdnew file+83
This diff is too big to show here.
vendor/github.com/aymanbagabas/go-osc52/v2/osc52.gonew file+305
This diff is too big to show here.
vendor/github.com/aymanbagabas/go-udiff/LICENSE-BSDnew file+27
This diff is too big to show here.
vendor/github.com/aymanbagabas/go-udiff/LICENSE-MITnew file+21
This diff is too big to show here.
vendor/github.com/aymanbagabas/go-udiff/README.mdnew file+142
This diff is too big to show here.
vendor/github.com/aymanbagabas/go-udiff/diff.gonew file+177
This diff is too big to show here.
vendor/github.com/aymanbagabas/go-udiff/export.gonew file+16
This diff is too big to show here.
vendor/github.com/aymanbagabas/go-udiff/lcs/common.gonew file+179
This diff is too big to show here.
vendor/github.com/aymanbagabas/go-udiff/lcs/doc.gonew file+156
This diff is too big to show here.
vendor/github.com/aymanbagabas/go-udiff/lcs/git.shnew file+33
This diff is too big to show here.
vendor/github.com/aymanbagabas/go-udiff/lcs/labels.gonew file+55
This diff is too big to show here.
vendor/github.com/aymanbagabas/go-udiff/lcs/old.gonew file+478
This diff is too big to show here.
vendor/github.com/aymanbagabas/go-udiff/lcs/sequence.gonew file+113
This diff is too big to show here.
vendor/github.com/aymanbagabas/go-udiff/merge.gonew file+81
This diff is too big to show here.
vendor/github.com/aymanbagabas/go-udiff/myers/diff.gonew file+246
This diff is too big to show here.
vendor/github.com/aymanbagabas/go-udiff/ndiff.gonew file+99
This diff is too big to show here.
vendor/github.com/aymanbagabas/go-udiff/unified.gonew file+251
This diff is too big to show here.
vendor/github.com/aymerick/douceur/LICENSEnew file+22
This diff is too big to show here.
vendor/github.com/aymerick/douceur/css/declaration.gonew file+60
This diff is too big to show here.
vendor/github.com/aymerick/douceur/css/rule.gonew file+230
This diff is too big to show here.
vendor/github.com/aymerick/douceur/css/stylesheet.gonew file+25
This diff is too big to show here.
vendor/github.com/aymerick/douceur/parser/parser.gonew file+409
This diff is too big to show here.
vendor/github.com/bmatcuk/doublestar/v4/.codecov.ymlnew file+10
This diff is too big to show here.
vendor/github.com/bmatcuk/doublestar/v4/.gitignorenew file+32
This diff is too big to show here.
vendor/github.com/bmatcuk/doublestar/v4/LICENSEnew file+22
This diff is too big to show here.
vendor/github.com/bmatcuk/doublestar/v4/README.mdnew file+433
This diff is too big to show here.
vendor/github.com/bmatcuk/doublestar/v4/UPGRADING.mdnew file+63
This diff is too big to show here.
vendor/github.com/bmatcuk/doublestar/v4/doublestar.gonew file+13
This diff is too big to show here.
vendor/github.com/bmatcuk/doublestar/v4/glob.gonew file+472
This diff is too big to show here.
vendor/github.com/bmatcuk/doublestar/v4/globoptions.gonew file+144
This diff is too big to show here.
vendor/github.com/bmatcuk/doublestar/v4/globwalk.gonew file+414
This diff is too big to show here.
vendor/github.com/bmatcuk/doublestar/v4/match.gonew file+403
This diff is too big to show here.
vendor/github.com/bmatcuk/doublestar/v4/utils.gonew file+160
This diff is too big to show here.
vendor/github.com/bmatcuk/doublestar/v4/validate.gonew file+82
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/.gitignorenew file+2
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/LICENSEnew file+21
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/Makefilenew file+101
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/README.mdnew file+193
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/adapters.gonew file+102
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/dirent.gonew file+73
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/dirent_portable.gonew file+134
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/dirent_unix.gonew file+146
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/entry_filter_portable.gonew file+37
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/entry_filter_unix.gonew file+59
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/entry_filter_windows.gonew file+156
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/fastwalk.gonew file+697
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/fastwalk_darwin.gonew file+122
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/fastwalk_portable.gonew file+65
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/fastwalk_unix.gonew file+131
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/go.LICENSEnew file+27
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/internal/dirent/dirent.gonew file+109
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/internal/dirent/dirent_aix.gonew file+29
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/internal/dirent/dirent_dragonfly.gonew file+54
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/internal/dirent/dirent_freebsd.gonew file+46
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/internal/dirent/dirent_js.gonew file+27
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/internal/dirent/dirent_linux.gonew file+50
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/internal/dirent/dirent_netbsd.gonew file+46
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/internal/dirent/dirent_openbsd.gonew file+46
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/internal/dirent/dirent_solaris.gonew file+29
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/internal/dirent/doc.gonew file+2
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/internal/dirent/endian_big.gonew file+9
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/internal/dirent/endian_little.gonew file+9
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/internal/fmtdirent/fmtdirent_go120.gonew file+31
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/internal/fmtdirent/fmtdirent_go121.gonew file+15
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/zsyscall_darwin.gonew file+93
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/zsyscall_darwin_amd64.1_13.snew file+22
This diff is too big to show here.
vendor/github.com/charlievieth/fastwalk/zsyscall_darwin_arm64.1_13.snew file+22
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbles/v2/LICENSEnew file+21
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbles/v2/cursor/cursor.gonew file+241
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbles/v2/filepicker/filepicker.gonew file+526
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbles/v2/filepicker/hidden_unix.gonew file+11
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbles/v2/filepicker/hidden_windows.gonew file+21
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbles/v2/help/help.gonew file+243
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbles/v2/internal/memoization/memoization.gonew file+125
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbles/v2/internal/runeutil/runeutil.gonew file+102
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbles/v2/key/key.gonew file+140
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbles/v2/spinner/spinner.gonew file+211
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbles/v2/textarea/textarea.gonew file+1682
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbles/v2/textinput/styles.gonew file+96
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbles/v2/textinput/textinput.gonew file+968
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbles/v2/viewport/highlight.gonew file+141
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbles/v2/viewport/keymap.gonew file+58
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbles/v2/viewport/viewport.gonew file+754
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/.gitattributesnew file+1
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/.gitignorenew file+23
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/.golangci.ymlnew file+41
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/.goreleaser.ymlnew file+5
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/LICENSEnew file+21
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/README.mdnew file+412
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/Taskfile.yamlnew file+14
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/clipboard.gonew file+69
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/color.gonew file+121
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/commands.gonew file+186
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/cursed_renderer.gonew file+450
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/cursor.gonew file+26
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/environ.gonew file+34
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/exec.gonew file+129
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/focus.gonew file+9
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/input.gonew file+67
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/key.gonew file+371
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/keyboard.gonew file+160
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/logging.gonew file+53
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/mod.gonew file+27
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/mode.gonew file+8
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/mouse.gonew file+144
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/nil_renderer.gonew file+70
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/options.gonew file+302
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/paste.gonew file+13
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/profile.gonew file+15
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/raw.gonew file+37
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/renderer.gonew file+124
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/screen.gonew file+154
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/signals_unix.gonew file+33
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/signals_windows.gonew file+10
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/standard_renderer.gonew file+384
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/tea.gonew file+1474
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/termcap.gonew file+41
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/termios_bsd.gonew file+13
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/termios_other.gonew file+8
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/termios_unix.gonew file+14
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/tty.gonew file+179
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/tty_unix.gonew file+54
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/tty_windows.gonew file+74
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/util.gonew file+8
This diff is too big to show here.
vendor/github.com/charmbracelet/bubbletea/v2/xterm.gonew file+14
This diff is too big to show here.
vendor/github.com/charmbracelet/colorprofile/.golangci.ymlnew file+41
This diff is too big to show here.
vendor/github.com/charmbracelet/colorprofile/.goreleaser.ymlnew file+6
This diff is too big to show here.
vendor/github.com/charmbracelet/colorprofile/LICENSEnew file+21
This diff is too big to show here.
vendor/github.com/charmbracelet/colorprofile/README.mdnew file+103
This diff is too big to show here.
vendor/github.com/charmbracelet/colorprofile/doc.gonew file+4
This diff is too big to show here.
vendor/github.com/charmbracelet/colorprofile/env.gonew file+293
This diff is too big to show here.
vendor/github.com/charmbracelet/colorprofile/env_other.gonew file+8
This diff is too big to show here.
vendor/github.com/charmbracelet/colorprofile/env_windows.gonew file+45
This diff is too big to show here.
vendor/github.com/charmbracelet/colorprofile/profile.go+399
This diff is too big to show here.
vendor/github.com/charmbracelet/colorprofile/writer.go+169
This diff is too big to show here.
vendor/github.com/charmbracelet/fang/.gitignore+2
This diff is too big to show here.
vendor/github.com/charmbracelet/fang/.golangci.yml+40
This diff is too big to show here.
vendor/github.com/charmbracelet/fang/.goreleaser.yml+5
This diff is too big to show here.
vendor/github.com/charmbracelet/fang/LICENSE.md+21
This diff is too big to show here.
vendor/github.com/charmbracelet/fang/README.md+91
This diff is too big to show here.
vendor/github.com/charmbracelet/fang/Taskfile.yml+7
This diff is too big to show here.
vendor/github.com/charmbracelet/fang/fang.go+149
This diff is too big to show here.
vendor/github.com/charmbracelet/fang/help.go+340
This diff is too big to show here.
vendor/github.com/charmbracelet/fang/theme.go+182
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/.editorconfig+18
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/.gitattributes+2
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/.gitignore+2
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/.golangci.yml+49
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/.goreleaser.yml+6
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/CONTRIBUTING.md+23
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/LICENSE+21
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/README.md+96
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/Taskfile.yml+25
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/ansi.go+3
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/baseelement.go+157
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/blockelement.go+65
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/blockstack.go+95
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/codeblock.go+150
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/codespan.go+15
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/context.go+38
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/elements.go+479
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/emphasis.go+47
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/heading.go+79
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/image.go+57
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/link.go+112
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/listitem.go+29
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/margin.go+189
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/paragraph.go+62
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/renderer.go+166
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/style.go+252
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/table.go+205
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/table_links.go+234
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/task.go+27
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/ansi/templatehelper.go+86
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/examples.sh+19
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/gallery.sh+23
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/glamour.go+294
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/internal/autolink/autolink.go+55
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/styles/README.md+565
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/styles/ascii.json+83
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/styles/dark.json+191
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/styles/dracula.go+216
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/styles/dracula.json+188
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/styles/light.json+190
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/styles/notty.json+83
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/styles/pink.json+90
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/styles/styles.go+680
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/styles/tokyo-night.go+209
This diff is too big to show here.
vendor/github.com/charmbracelet/glamour/v2/styles/tokyo_night.json+182
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/.editorconfig+18
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/.gitattributes+1
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/.gitignore+2
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/.golangci.yml+49
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/.goreleaser.yml+5
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/LICENSE+21
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/README.md+792
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/Taskfile.yaml+19
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/align.go+82
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/ansi_unix.go+9
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/ansi_windows.go+31
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/borders.go+489
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/canvas.go+267
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/color.go+227
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/get.go+571
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/join.go+175
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/lipgloss.go+3
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/position.go+134
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/query.go+89
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/ranges.go+48
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/runes.go+43
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/set.go+795
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/size.go+40
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/style.go+571
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/table/resizing.go+481
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/table/rows.go+130
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/table/table.go+561
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/table/util.go+44
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/terminal.go+114
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/tree/children.go+100
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/tree/enumerator.go+74
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/tree/renderer.go+142
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/tree/tree.go+369
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/unset.go+331
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/whitespace.go+69
This diff is too big to show here.
vendor/github.com/charmbracelet/lipgloss/v2/writer.go+160
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/.gitignore+17
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/.golangci-soft.yml+40
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/.golangci.yml+28
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/.goreleaser.yml+3
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/LICENSE+21
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/README.md+367
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/context.go+23
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/formatter.go+27
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/json.go+151
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/level.go+65
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/logfmt.go+32
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/logger.go+411
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/logger_121.go+71
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/logger_no121.go+68
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/options.go+61
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/pkg.go+248
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/stdlog.go+67
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/styles.go+83
This diff is too big to show here.
vendor/github.com/charmbracelet/log/v2/text.go+272
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/.gitattributes+1
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/.golangci.yml+40
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/.goreleaser.yml+5
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/LICENSE+5
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/README.md+25
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/TUTORIAL.md+207
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/border.go+231
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/buffer.go+612
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/cancelreader_other.go+14
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/cancelreader_windows.go+135
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/cell.go+519
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/doc.go+6
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/environ.go+32
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/event.go+443
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/input.go+61
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/key.go+766
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/key_table.go+664
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/mouse.go+86
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/parse.go+1766
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/styled.go+292
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/tabstop.go+142
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/terminal.go+1090
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/terminal_other.go+17
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/terminal_reader.go+357
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/terminal_reader_other.go+18
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/terminal_reader_windows.go+624
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/terminal_renderer.go+1584
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/terminal_renderer_hardscroll.go+275
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/terminal_renderer_hashmap.go+294
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/terminal_unix.go+72
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/terminal_windows.go+93
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/tty.go+18
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/tty_other.go+14
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/tty_unix.go+28
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/tty_windows.go+27
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/utils.go+15
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/uv.go+52
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/winch.go+29
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/winch_other.go+15
This diff is too big to show here.
vendor/github.com/charmbracelet/ultraviolet/winch_unix.go+86
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/LICENSE+21
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/ansi.go+11
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/ascii.go+8
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/background.go+178
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/c0.go+79
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/c1.go+72
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/charset.go+55
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/clipboard.go+75
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/color.go+784
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/ctrl.go+156
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/cursor.go+635
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/cwd.go+26
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/doc.go+7
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/finalterm.go+67
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/focus.go+9
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/graphics.go+237
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/hyperlink.go+28
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/iterm2.go+18
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/keypad.go+28
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/kitty.go+90
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/kitty/decoder.go+85
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/kitty/encoder.go+64
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/kitty/graphics.go+414
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/kitty/options.go+367
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/method.go+172
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/mode.go+778
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/modes.go+65
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/mouse.go+172
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/notification.go+13
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/parser.go+417
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/parser/const.go+78
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/parser/seq.go+136
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/parser/transition_table.go+273
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/parser_decode.go+524
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/parser_handler.go+60
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/parser_sync.go+29
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/passthrough.go+60
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/paste.go+7
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/reset.go+11
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/screen.go+410
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/sgr.go+79
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/status.go+144
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/style.go+673
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/termcap.go+41
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/title.go+48
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/truncate.go+299
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/util.go+106
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/width.go+113
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/winop.go+53
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/wrap.go+474
This diff is too big to show here.
vendor/github.com/charmbracelet/x/ansi/xterm.go+138
This diff is too big to show here.
vendor/github.com/charmbracelet/x/cellbuf/LICENSE+21
This diff is too big to show here.
vendor/github.com/charmbracelet/x/cellbuf/buffer.go+473
This diff is too big to show here.
vendor/github.com/charmbracelet/x/cellbuf/cell.go+508
This diff is too big to show here.
vendor/github.com/charmbracelet/x/cellbuf/errors.go+6
This diff is too big to show here.
vendor/github.com/charmbracelet/x/cellbuf/geom.go+21
This diff is too big to show here.
vendor/github.com/charmbracelet/x/cellbuf/hardscroll.go+273
This diff is too big to show here.
vendor/github.com/charmbracelet/x/cellbuf/hashmap.go+301
This diff is too big to show here.
vendor/github.com/charmbracelet/x/cellbuf/link.go+14
This diff is too big to show here.
vendor/github.com/charmbracelet/x/cellbuf/pen.go+88
This diff is too big to show here.
vendor/github.com/charmbracelet/x/cellbuf/screen.go+1506
This diff is too big to show here.
vendor/github.com/charmbracelet/x/cellbuf/sequence.go+131
This diff is too big to show here.
vendor/github.com/charmbracelet/x/cellbuf/style.go+31
This diff is too big to show here.
vendor/github.com/charmbracelet/x/cellbuf/tabstop.go+137
This diff is too big to show here.
vendor/github.com/charmbracelet/x/cellbuf/utils.go+38
This diff is too big to show here.
vendor/github.com/charmbracelet/x/cellbuf/wrap.go+194
This diff is too big to show here.
vendor/github.com/charmbracelet/x/cellbuf/writer.go+339
This diff is too big to show here.
vendor/github.com/charmbracelet/x/exp/charmtone/LICENSE+21
This diff is too big to show here.
vendor/github.com/charmbracelet/x/exp/charmtone/charmtone.go+362
This diff is too big to show here.
vendor/github.com/charmbracelet/x/exp/golden/LICENSE+21
This diff is too big to show here.
vendor/github.com/charmbracelet/x/exp/golden/golden.go+82
This diff is too big to show here.
vendor/github.com/charmbracelet/x/exp/slice/LICENSE+21
This diff is too big to show here.
vendor/github.com/charmbracelet/x/exp/slice/slice.go+120
This diff is too big to show here.
vendor/github.com/charmbracelet/x/term/LICENSE+21
This diff is too big to show here.
vendor/github.com/charmbracelet/x/term/term.go+49
This diff is too big to show here.
vendor/github.com/charmbracelet/x/term/term_other.go+39
This diff is too big to show here.
vendor/github.com/charmbracelet/x/term/term_unix.go+96
This diff is too big to show here.
vendor/github.com/charmbracelet/x/term/term_unix_bsd.go+11
This diff is too big to show here.
vendor/github.com/charmbracelet/x/term/term_unix_other.go+11
This diff is too big to show here.
vendor/github.com/charmbracelet/x/term/term_windows.go+87
This diff is too big to show here.
vendor/github.com/charmbracelet/x/term/terminal.go+12
This diff is too big to show here.
vendor/github.com/charmbracelet/x/term/util.go+47
This diff is too big to show here.
vendor/github.com/charmbracelet/x/termios/LICENSE+21
This diff is too big to show here.
vendor/github.com/charmbracelet/x/termios/bit_bsd.go+7
This diff is too big to show here.
vendor/github.com/charmbracelet/x/termios/bit_darwin.go+7
This diff is too big to show here.
vendor/github.com/charmbracelet/x/termios/bit_other.go+7
This diff is too big to show here.
vendor/github.com/charmbracelet/x/termios/syscalls_bsd.go+11
This diff is too big to show here.
vendor/github.com/charmbracelet/x/termios/syscalls_darwin.go+10
This diff is too big to show here.
vendor/github.com/charmbracelet/x/termios/syscalls_linux.go+14
This diff is too big to show here.
vendor/github.com/charmbracelet/x/termios/termios.go+249
This diff is too big to show here.
vendor/github.com/charmbracelet/x/termios/termios_bsd.go+13
This diff is too big to show here.
vendor/github.com/charmbracelet/x/termios/termios_linux.go+13
This diff is too big to show here.
vendor/github.com/charmbracelet/x/termios/termios_other.go+15
This diff is too big to show here.
vendor/github.com/charmbracelet/x/termios/termios_solaris.go+26
This diff is too big to show here.
vendor/github.com/charmbracelet/x/windows/LICENSE+21
This diff is too big to show here.
vendor/github.com/charmbracelet/x/windows/doc.go+3
This diff is too big to show here.
vendor/github.com/charmbracelet/x/windows/syscall_windows.go+12
This diff is too big to show here.
vendor/github.com/charmbracelet/x/windows/types_windows.go+351
This diff is too big to show here.
vendor/github.com/charmbracelet/x/windows/zsyscall_windows.go+77
This diff is too big to show here.
vendor/github.com/davecgh/go-spew/LICENSE+15
This diff is too big to show here.
vendor/github.com/davecgh/go-spew/spew/bypass.go+145
This diff is too big to show here.
vendor/github.com/davecgh/go-spew/spew/bypasssafe.go+38
This diff is too big to show here.
vendor/github.com/davecgh/go-spew/spew/common.go+341
This diff is too big to show here.
vendor/github.com/davecgh/go-spew/spew/config.go+306
This diff is too big to show here.
vendor/github.com/davecgh/go-spew/spew/doc.go+211
This diff is too big to show here.
vendor/github.com/davecgh/go-spew/spew/dump.go+509
This diff is too big to show here.
vendor/github.com/davecgh/go-spew/spew/format.go+419
This diff is too big to show here.
vendor/github.com/davecgh/go-spew/spew/spew.go+148
This diff is too big to show here.
vendor/github.com/disintegration/gift/.travis.yml+17
This diff is too big to show here.
vendor/github.com/disintegration/gift/LICENSE+21
This diff is too big to show here.
vendor/github.com/disintegration/gift/README.md+251
This diff is too big to show here.
vendor/github.com/disintegration/gift/colors.go+511
This diff is too big to show here.
vendor/github.com/disintegration/gift/convolution.go+578
This diff is too big to show here.
vendor/github.com/disintegration/gift/effects.go+87
This diff is too big to show here.
vendor/github.com/disintegration/gift/gift.go+219
This diff is too big to show here.
vendor/github.com/disintegration/gift/pixels.go+493
This diff is too big to show here.
vendor/github.com/disintegration/gift/rank.go+223
This diff is too big to show here.
vendor/github.com/disintegration/gift/resize.go+462
This diff is too big to show here.
vendor/github.com/disintegration/gift/transform.go+498
This diff is too big to show here.
vendor/github.com/disintegration/gift/utils.go+242
This diff is too big to show here.
vendor/github.com/disintegration/imageorient/LICENSE+21
This diff is too big to show here.
vendor/github.com/disintegration/imageorient/README.md+61
This diff is too big to show here.
vendor/github.com/disintegration/imageorient/imageorient.go+209
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/.gitignore+27
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/.travis.yml+7
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/ATTRIB+133
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/LICENSE+21
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/README.md+174
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/fastclock.go+129
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/match.go+349
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/regexp.go+395
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/replace.go+177
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/runner.go+1613
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/syntax/charclass.go+865
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/syntax/code.go+274
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/syntax/escape.go+94
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/syntax/fuzz.go+20
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/syntax/parser.go+2262
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/syntax/prefix.go+896
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/syntax/replacerdata.go+87
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/syntax/tree.go+654
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/syntax/writer.go+500
This diff is too big to show here.
vendor/github.com/dlclark/regexp2/testoutput1+7061
This diff is too big to show here.
vendor/github.com/dustin/go-humanize/.travis.yml+21
This diff is too big to show here.
vendor/github.com/dustin/go-humanize/LICENSE+21
This diff is too big to show here.
vendor/github.com/dustin/go-humanize/README.markdown+124
This diff is too big to show here.
vendor/github.com/dustin/go-humanize/big.go+31
This diff is too big to show here.
vendor/github.com/dustin/go-humanize/bigbytes.go+189
This diff is too big to show here.
vendor/github.com/dustin/go-humanize/bytes.go+143
This diff is too big to show here.
vendor/github.com/dustin/go-humanize/comma.go+116
This diff is too big to show here.
vendor/github.com/dustin/go-humanize/commaf.go+41
This diff is too big to show here.
vendor/github.com/dustin/go-humanize/ftoa.go+49
This diff is too big to show here.
vendor/github.com/dustin/go-humanize/humanize.go+8
This diff is too big to show here.
vendor/github.com/dustin/go-humanize/number.go+192
This diff is too big to show here.
vendor/github.com/dustin/go-humanize/ordinals.go+25
This diff is too big to show here.
vendor/github.com/dustin/go-humanize/si.go+127
This diff is too big to show here.
vendor/github.com/dustin/go-humanize/times.go+117
This diff is too big to show here.
vendor/github.com/felixge/httpsnoop/.gitignore
This diff is too big to show here.
vendor/github.com/felixge/httpsnoop/LICENSE.txt+19
This diff is too big to show here.
vendor/github.com/felixge/httpsnoop/Makefile+10
This diff is too big to show here.
vendor/github.com/felixge/httpsnoop/README.md+95
This diff is too big to show here.
vendor/github.com/felixge/httpsnoop/capture_metrics.go+86
This diff is too big to show here.
vendor/github.com/felixge/httpsnoop/docs.go+10
This diff is too big to show here.
vendor/github.com/felixge/httpsnoop/wrap_generated_gteq_1.8.go+436
This diff is too big to show here.
vendor/github.com/felixge/httpsnoop/wrap_generated_lt_1.8.go+278
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/.cirrus.yml+14
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/.gitignore+10
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/.mailmap+2
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/CHANGELOG.md+569
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/CONTRIBUTING.md+144
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/LICENSE+25
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/README.md+184
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/backend_fen.go+484
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/backend_inotify.go+658
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/backend_kqueue.go+733
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/backend_other.go+23
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/backend_windows.go+682
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/fsnotify.go+494
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/internal/darwin.go+39
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/internal/debug_darwin.go+57
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/internal/debug_dragonfly.go+33
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/internal/debug_freebsd.go+42
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/internal/debug_kqueue.go+32
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/internal/debug_linux.go+56
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/internal/debug_netbsd.go+25
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/internal/debug_openbsd.go+28
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/internal/debug_solaris.go+45
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/internal/debug_windows.go+40
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/internal/freebsd.go+31
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/internal/internal.go+2
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/internal/unix.go+31
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/internal/unix2.go+7
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/internal/windows.go+41
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/system_bsd.go+7
This diff is too big to show here.
vendor/github.com/fsnotify/fsnotify/system_darwin.go+8
This diff is too big to show here.
vendor/github.com/go-logfmt/logfmt/.gitignore+1
This diff is too big to show here.
vendor/github.com/go-logfmt/logfmt/CHANGELOG.md+82
This diff is too big to show here.
vendor/github.com/go-logfmt/logfmt/LICENSE+22
This diff is too big to show here.
vendor/github.com/go-logfmt/logfmt/README.md+41
This diff is too big to show here.
vendor/github.com/go-logfmt/logfmt/decode.go+254
This diff is too big to show here.
vendor/github.com/go-logfmt/logfmt/doc.go+6
This diff is too big to show here.
vendor/github.com/go-logfmt/logfmt/encode.go+322
This diff is too big to show here.
vendor/github.com/go-logfmt/logfmt/jsonstring.go+277
This diff is too big to show here.
vendor/github.com/go-logr/logr/.golangci.yaml+26
This diff is too big to show here.
vendor/github.com/go-logr/logr/CHANGELOG.md+6
This diff is too big to show here.
vendor/github.com/go-logr/logr/CONTRIBUTING.md+17
This diff is too big to show here.
vendor/github.com/go-logr/logr/LICENSE+201
This diff is too big to show here.
vendor/github.com/go-logr/logr/README.md+407
This diff is too big to show here.
vendor/github.com/go-logr/logr/SECURITY.md+18
This diff is too big to show here.
vendor/github.com/go-logr/logr/context.go+33
This diff is too big to show here.
vendor/github.com/go-logr/logr/context_noslog.go+49
This diff is too big to show here.
vendor/github.com/go-logr/logr/context_slog.go+83
This diff is too big to show here.
vendor/github.com/go-logr/logr/discard.go+24
This diff is too big to show here.
vendor/github.com/go-logr/logr/funcr/funcr.go+914
This diff is too big to show here.
vendor/github.com/go-logr/logr/funcr/slogsink.go+105
This diff is too big to show here.
vendor/github.com/go-logr/logr/logr.go+520
This diff is too big to show here.
vendor/github.com/go-logr/logr/sloghandler.go+192
This diff is too big to show here.
vendor/github.com/go-logr/logr/slogr.go+100
This diff is too big to show here.
vendor/github.com/go-logr/logr/slogsink.go+120
This diff is too big to show here.
vendor/github.com/go-logr/stdr/LICENSE+201
This diff is too big to show here.
vendor/github.com/go-logr/stdr/README.md+6
This diff is too big to show here.
vendor/github.com/go-logr/stdr/stdr.go+170
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/.gitignore+4
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/LICENSE+9
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/MIGRATION_GUIDE.md+195
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/README.md+167
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/SECURITY.md+19
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/VERSION_HISTORY.md+137
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/claims.go+16
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/doc.go+4
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/ecdsa.go+134
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/ecdsa_utils.go+69
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/ed25519.go+79
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/ed25519_utils.go+64
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/errors.go+49
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/errors_go1_20.go+47
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/errors_go_other.go+78
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/hmac.go+104
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/map_claims.go+109
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/none.go+50
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/parser.go+268
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/parser_option.go+128
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/registered_claims.go+63
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/rsa.go+93
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/rsa_pss.go+135
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/rsa_utils.go+107
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/signing_method.go+49
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/staticcheck.conf+1
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/token.go+100
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/token_option.go+5
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/types.go+149
This diff is too big to show here.
vendor/github.com/golang-jwt/jwt/v5/validator.go+316
This diff is too big to show here.
vendor/github.com/google/go-cmp/LICENSE+27
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/compare.go+671
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/export.go+31
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go+18
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go+123
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go+402
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/internal/flags/flags.go+9
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/internal/function/func.go+106
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/internal/value/name.go+164
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/internal/value/pointer.go+34
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/internal/value/sort.go+106
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/options.go+562
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/path.go+390
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/report.go+54
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/report_compare.go+433
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/report_references.go+264
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/report_reflect.go+414
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/report_slices.go+614
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/report_text.go+432
This diff is too big to show here.
vendor/github.com/google/go-cmp/cmp/report_value.go+121
This diff is too big to show here.
vendor/github.com/google/s2a-go/.gitignore+6
This diff is too big to show here.
vendor/github.com/google/s2a-go/CODE_OF_CONDUCT.md+93
This diff is too big to show here.
vendor/github.com/google/s2a-go/CONTRIBUTING.md+29
This diff is too big to show here.
vendor/github.com/google/s2a-go/LICENSE.md+202
This diff is too big to show here.
vendor/github.com/google/s2a-go/README.md+14
This diff is too big to show here.
vendor/github.com/google/s2a-go/fallback/s2a_fallback.go+167
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/authinfo/authinfo.go+119
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/handshaker/handshaker.go+438
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/handshaker/service/service.go+66
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/proto/common_go_proto/common.pb.go+388
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/proto/s2a_context_go_proto/s2a_context.pb.go+267
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/proto/s2a_go_proto/s2a.pb.go+1377
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/proto/s2a_go_proto/s2a_grpc.pb.go+174
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/proto/v2/common_go_proto/common.pb.go+549
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/proto/v2/s2a_context_go_proto/s2a_context.pb.go+249
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/proto/v2/s2a_go_proto/s2a.pb.go+2512
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/proto/v2/s2a_go_proto/s2a_grpc.pb.go+160
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/record/internal/aeadcrypter/aeadcrypter.go+34
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/record/internal/aeadcrypter/aesgcm.go+70
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/record/internal/aeadcrypter/chachapoly.go+67
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/record/internal/aeadcrypter/common.go+92
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/record/internal/halfconn/ciphersuite.go+98
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/record/internal/halfconn/counter.go+60
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/record/internal/halfconn/expander.go+59
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/record/internal/halfconn/halfconn.go+193
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/record/record.go+729
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/record/ticketsender.go+178
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/tokenmanager/tokenmanager.go+79
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/v2/README.md+1
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/v2/certverifier/certverifier.go+122
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/v2/remotesigner/remotesigner.go+186
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/v2/s2av2.go+379
This diff is too big to show here.
vendor/github.com/google/s2a-go/internal/v2/tlsconfigstore/tlsconfigstore.go+403
This diff is too big to show here.
vendor/github.com/google/s2a-go/retry/retry.go+144
This diff is too big to show here.
vendor/github.com/google/s2a-go/s2a.go+442
This diff is too big to show here.
vendor/github.com/google/s2a-go/s2a_options.go+235
This diff is too big to show here.
vendor/github.com/google/s2a-go/s2a_utils.go+79
This diff is too big to show here.
vendor/github.com/google/s2a-go/stream/s2a_stream.go+34
This diff is too big to show here.
vendor/github.com/google/uuid/CHANGELOG.md+41
This diff is too big to show here.
vendor/github.com/google/uuid/CONTRIBUTING.md+26
This diff is too big to show here.
vendor/github.com/google/uuid/CONTRIBUTORS+9
This diff is too big to show here.
vendor/github.com/google/uuid/LICENSE+27
This diff is too big to show here.
vendor/github.com/google/uuid/README.md+21
This diff is too big to show here.
vendor/github.com/google/uuid/dce.go+80
This diff is too big to show here.
vendor/github.com/google/uuid/doc.go+12
This diff is too big to show here.
vendor/github.com/google/uuid/hash.go+59
This diff is too big to show here.
vendor/github.com/google/uuid/marshal.go+38
This diff is too big to show here.
vendor/github.com/google/uuid/node.go+90
This diff is too big to show here.
vendor/github.com/google/uuid/node_js.go+12
This diff is too big to show here.
vendor/github.com/google/uuid/node_net.go+33
This diff is too big to show here.
vendor/github.com/google/uuid/null.go+118
This diff is too big to show here.
vendor/github.com/google/uuid/sql.go+59
This diff is too big to show here.
vendor/github.com/google/uuid/time.go+134
This diff is too big to show here.
vendor/github.com/google/uuid/util.go+43
This diff is too big to show here.
vendor/github.com/google/uuid/uuid.go+365
This diff is too big to show here.
vendor/github.com/google/uuid/version1.go+44
This diff is too big to show here.
vendor/github.com/google/uuid/version4.go+76
This diff is too big to show here.
vendor/github.com/google/uuid/version6.go+56
This diff is too big to show here.
vendor/github.com/google/uuid/version7.go+104
This diff is too big to show here.
vendor/github.com/googleapis/enterprise-certificate-proxy/LICENSE+202
This diff is too big to show here.
vendor/github.com/googleapis/enterprise-certificate-proxy/client/client.go+219
This diff is too big to show here.
vendor/github.com/googleapis/enterprise-certificate-proxy/client/util/util.go+100
This diff is too big to show here.
vendor/github.com/googleapis/gax-go/v2/LICENSE+27
This diff is too big to show here.
vendor/github.com/googleapis/gax-go/v2/internallog/internal/internal.go+134
This diff is too big to show here.
vendor/github.com/googleapis/gax-go/v2/internallog/internallog.go+154
This diff is too big to show here.
vendor/github.com/gorilla/css/LICENSE+28
This diff is too big to show here.
vendor/github.com/gorilla/css/scanner/doc.go+33
This diff is too big to show here.
vendor/github.com/gorilla/css/scanner/scanner.go+360
This diff is too big to show here.
vendor/github.com/gorilla/websocket/.gitignore+25
This diff is too big to show here.
vendor/github.com/gorilla/websocket/AUTHORS+9
This diff is too big to show here.
vendor/github.com/gorilla/websocket/LICENSE+22
This diff is too big to show here.
vendor/github.com/gorilla/websocket/README.md+33
This diff is too big to show here.
vendor/github.com/gorilla/websocket/client.go+434
This diff is too big to show here.
vendor/github.com/gorilla/websocket/compression.go+148
This diff is too big to show here.
vendor/github.com/gorilla/websocket/conn.go+1238
This diff is too big to show here.
vendor/github.com/gorilla/websocket/doc.go+227
This diff is too big to show here.
vendor/github.com/gorilla/websocket/join.go+42
This diff is too big to show here.
vendor/github.com/gorilla/websocket/json.go+60
This diff is too big to show here.
vendor/github.com/gorilla/websocket/mask.go+55
This diff is too big to show here.
vendor/github.com/gorilla/websocket/mask_safe.go+16
This diff is too big to show here.
vendor/github.com/gorilla/websocket/prepared.go+102
This diff is too big to show here.
vendor/github.com/gorilla/websocket/proxy.go+77
This diff is too big to show here.
vendor/github.com/gorilla/websocket/server.go+365
This diff is too big to show here.
vendor/github.com/gorilla/websocket/tls_handshake.go+21
This diff is too big to show here.
vendor/github.com/gorilla/websocket/tls_handshake_116.go+21
This diff is too big to show here.
vendor/github.com/gorilla/websocket/util.go+298
This diff is too big to show here.
vendor/github.com/gorilla/websocket/x_net_proxy.go+473
This diff is too big to show here.
vendor/github.com/inconshreveable/mousetrap/LICENSE+201
This diff is too big to show here.
vendor/github.com/inconshreveable/mousetrap/README.md+23
This diff is too big to show here.
vendor/github.com/inconshreveable/mousetrap/trap_others.go+16
This diff is too big to show here.
vendor/github.com/inconshreveable/mousetrap/trap_windows.go+42
This diff is too big to show here.
vendor/github.com/joho/godotenv/.gitignore+1
This diff is too big to show here.
vendor/github.com/joho/godotenv/LICENCE+23
This diff is too big to show here.
vendor/github.com/joho/godotenv/README.md+202
This diff is too big to show here.
vendor/github.com/joho/godotenv/autoload/autoload.go+15
This diff is too big to show here.
vendor/github.com/joho/godotenv/godotenv.go+228
This diff is too big to show here.
vendor/github.com/joho/godotenv/parser.go+271
This diff is too big to show here.
vendor/github.com/kylelemons/godebug/LICENSE+202
This diff is too big to show here.
vendor/github.com/kylelemons/godebug/diff/diff.go+186
This diff is too big to show here.
vendor/github.com/kylelemons/godebug/pretty/.gitignore+5
This diff is too big to show here.
vendor/github.com/kylelemons/godebug/pretty/doc.go+25
This diff is too big to show here.
vendor/github.com/kylelemons/godebug/pretty/public.go+188
This diff is too big to show here.
vendor/github.com/kylelemons/godebug/pretty/reflect.go+241
This diff is too big to show here.
vendor/github.com/kylelemons/godebug/pretty/structure.go+223
This diff is too big to show here.
vendor/github.com/lucasb-eyer/go-colorful/.gitignore+101
This diff is too big to show here.
vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md+42
This diff is too big to show here.
vendor/github.com/lucasb-eyer/go-colorful/LICENSE+7
This diff is too big to show here.
vendor/github.com/lucasb-eyer/go-colorful/README.md+482
This diff is too big to show here.
vendor/github.com/lucasb-eyer/go-colorful/colorgens.go+55
This diff is too big to show here.
vendor/github.com/lucasb-eyer/go-colorful/colors.go+979
This diff is too big to show here.
vendor/github.com/lucasb-eyer/go-colorful/happy_palettegen.go+25
This diff is too big to show here.
vendor/github.com/lucasb-eyer/go-colorful/hexcolor.go+67
This diff is too big to show here.
vendor/github.com/lucasb-eyer/go-colorful/hsluv-snapshot-rev4.json+1
This diff is too big to show here.
vendor/github.com/lucasb-eyer/go-colorful/hsluv.go+207
This diff is too big to show here.
vendor/github.com/lucasb-eyer/go-colorful/soft_palettegen.go+185
This diff is too big to show here.
vendor/github.com/lucasb-eyer/go-colorful/warm_palettegen.go+25
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/LICENSE+21
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/client/client.go+434
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/client/http.go+17
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/client/inprocess.go+12
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/client/interface.go+109
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/client/oauth.go+76
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/client/sse.go+42
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/client/stdio.go+43
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/client/transport/inprocess.go+70
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/client/transport/interface.go+50
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/client/transport/oauth.go+650
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/client/transport/oauth_utils.go+68
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/client/transport/sse.go+522
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/client/transport/stdio.go+288
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/client/transport/streamable_http.go+515
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/mcp/prompts.go+170
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/mcp/resources.go+99
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/mcp/tools.go+974
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/mcp/typed_tools.go+20
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/mcp/types.go+1044
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/mcp/utils.go+798
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/server/errors.go+34
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/server/hooks.go+532
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/server/http_transport_options.go+11
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/server/request_handler.go+320
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/server/server.go+1079
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/server/session.go+380
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/server/sse.go+736
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/server/stdio.go+314
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/server/streamable_http.go+653
This diff is too big to show here.
vendor/github.com/mark3labs/mcp-go/util/logger.go+33
This diff is too big to show here.
vendor/github.com/mattn/go-isatty/LICENSE+9
This diff is too big to show here.
vendor/github.com/mattn/go-isatty/README.md+50
This diff is too big to show here.
vendor/github.com/mattn/go-isatty/doc.go+2
This diff is too big to show here.
vendor/github.com/mattn/go-isatty/go.test.sh+12
This diff is too big to show here.
vendor/github.com/mattn/go-isatty/isatty_bsd.go+20
This diff is too big to show here.
vendor/github.com/mattn/go-isatty/isatty_others.go+17
This diff is too big to show here.
vendor/github.com/mattn/go-isatty/isatty_plan9.go+23
This diff is too big to show here.
vendor/github.com/mattn/go-isatty/isatty_solaris.go+21
This diff is too big to show here.
vendor/github.com/mattn/go-isatty/isatty_tcgets.go+20
This diff is too big to show here.
vendor/github.com/mattn/go-isatty/isatty_windows.go+125
This diff is too big to show here.
vendor/github.com/mattn/go-runewidth/LICENSE+21
This diff is too big to show here.
vendor/github.com/mattn/go-runewidth/README.md+27
This diff is too big to show here.
vendor/github.com/mattn/go-runewidth/runewidth.go+358
This diff is too big to show here.
vendor/github.com/mattn/go-runewidth/runewidth_appengine.go+9
This diff is too big to show here.
vendor/github.com/mattn/go-runewidth/runewidth_js.go+9
This diff is too big to show here.
vendor/github.com/mattn/go-runewidth/runewidth_posix.go+81
This diff is too big to show here.
vendor/github.com/mattn/go-runewidth/runewidth_table.go+450
This diff is too big to show here.
vendor/github.com/mattn/go-runewidth/runewidth_windows.go+28
This diff is too big to show here.
vendor/github.com/mfridman/interpolate/LICENSE.txt+24
This diff is too big to show here.
vendor/github.com/mfridman/interpolate/README.md+85
This diff is too big to show here.
vendor/github.com/mfridman/interpolate/env.go+53
This diff is too big to show here.
vendor/github.com/mfridman/interpolate/interpolate.go+213
This diff is too big to show here.
vendor/github.com/mfridman/interpolate/parser.go+281
This diff is too big to show here.
vendor/github.com/microcosm-cc/bluemonday/CONTRIBUTING.md+50
This diff is too big to show here.
vendor/github.com/microcosm-cc/bluemonday/CREDITS.md+8
This diff is too big to show here.
vendor/github.com/microcosm-cc/bluemonday/LICENSE.md+28
This diff is too big to show here.
vendor/github.com/microcosm-cc/bluemonday/README.md+386
This diff is too big to show here.
vendor/github.com/microcosm-cc/bluemonday/SECURITY.md+13
This diff is too big to show here.
vendor/github.com/microcosm-cc/bluemonday/css/handlers.go+2016
This diff is too big to show here.
vendor/github.com/microcosm-cc/bluemonday/doc.go+104
This diff is too big to show here.
vendor/github.com/microcosm-cc/bluemonday/helpers.go+300
This diff is too big to show here.
vendor/github.com/microcosm-cc/bluemonday/policies.go+253
This diff is too big to show here.
vendor/github.com/microcosm-cc/bluemonday/policy.go+990
This diff is too big to show here.
vendor/github.com/microcosm-cc/bluemonday/sanitize.go+1096
This diff is too big to show here.
vendor/github.com/muesli/cancelreader/.gitignore+15
This diff is too big to show here.
vendor/github.com/muesli/cancelreader/.golangci-soft.yml+47
This diff is too big to show here.
vendor/github.com/muesli/cancelreader/.golangci.yml+29
This diff is too big to show here.
vendor/github.com/muesli/cancelreader/LICENSE+21
This diff is too big to show here.
vendor/github.com/muesli/cancelreader/README.md+64
This diff is too big to show here.
vendor/github.com/muesli/cancelreader/cancelreader.go+93
This diff is too big to show here.
vendor/github.com/muesli/cancelreader/cancelreader_bsd.go+146
This diff is too big to show here.
vendor/github.com/muesli/cancelreader/cancelreader_default.go+12
This diff is too big to show here.
vendor/github.com/muesli/cancelreader/cancelreader_linux.go+154
This diff is too big to show here.
vendor/github.com/muesli/cancelreader/cancelreader_select.go+136
This diff is too big to show here.
vendor/github.com/muesli/cancelreader/cancelreader_unix.go+18
This diff is too big to show here.
vendor/github.com/muesli/cancelreader/cancelreader_windows.go+244
This diff is too big to show here.
vendor/github.com/muesli/mango-cobra/.gitignore+17
This diff is too big to show here.
vendor/github.com/muesli/mango-cobra/.golangci-soft.yml+47
This diff is too big to show here.
vendor/github.com/muesli/mango-cobra/.golangci.yml+29
This diff is too big to show here.
vendor/github.com/muesli/mango-cobra/LICENSE+21
This diff is too big to show here.
vendor/github.com/muesli/mango-cobra/README.md+39
This diff is too big to show here.
vendor/github.com/muesli/mango-cobra/mcobra.go+61
This diff is too big to show here.
vendor/github.com/muesli/mango-pflag/.gitignore+17
This diff is too big to show here.
vendor/github.com/muesli/mango-pflag/.golangci-soft.yml+47
This diff is too big to show here.
vendor/github.com/muesli/mango-pflag/.golangci.yml+29
This diff is too big to show here.
vendor/github.com/muesli/mango-pflag/LICENSE+21
This diff is too big to show here.
vendor/github.com/muesli/mango-pflag/README.md+33
This diff is too big to show here.
vendor/github.com/muesli/mango-pflag/mpflag.go+30
This diff is too big to show here.
vendor/github.com/muesli/mango/.gitignore+19
This diff is too big to show here.
vendor/github.com/muesli/mango/.golangci-soft.yml+47
This diff is too big to show here.
vendor/github.com/muesli/mango/.golangci.yml+29
This diff is too big to show here.
vendor/github.com/muesli/mango/LICENSE+21
This diff is too big to show here.
vendor/github.com/muesli/mango/README.md+150
This diff is too big to show here.
vendor/github.com/muesli/mango/builder.go+21
This diff is too big to show here.
vendor/github.com/muesli/mango/mango.go+221
This diff is too big to show here.
vendor/github.com/muesli/mango/mango.png
This diff is too big to show here.
vendor/github.com/muesli/roff/.gitignore+15
This diff is too big to show here.
vendor/github.com/muesli/roff/.golangci-soft.yml+47
This diff is too big to show here.
vendor/github.com/muesli/roff/.golangci.yml+29
This diff is too big to show here.
vendor/github.com/muesli/roff/LICENSE+21
This diff is too big to show here.
vendor/github.com/muesli/roff/README.md+65
This diff is too big to show here.
vendor/github.com/muesli/roff/roff.go+163
This diff is too big to show here.
vendor/github.com/muesli/termenv/.gitignore+15
This diff is too big to show here.
vendor/github.com/muesli/termenv/.golangci-soft.yml+43
This diff is too big to show here.
vendor/github.com/muesli/termenv/.golangci.yml+28
This diff is too big to show here.
vendor/github.com/muesli/termenv/LICENSE+21
This diff is too big to show here.
vendor/github.com/muesli/termenv/README.md+431
This diff is too big to show here.
vendor/github.com/muesli/termenv/ansi_compat.md+65
This diff is too big to show here.
vendor/github.com/muesli/termenv/ansicolors.go+281
This diff is too big to show here.
vendor/github.com/muesli/termenv/color.go+205
This diff is too big to show here.
vendor/github.com/muesli/termenv/constants_linux.go+8
This diff is too big to show here.
vendor/github.com/muesli/termenv/constants_solaris.go+8
This diff is too big to show here.
vendor/github.com/muesli/termenv/constants_unix.go+13
This diff is too big to show here.
vendor/github.com/muesli/termenv/constants_zos.go+8
This diff is too big to show here.
vendor/github.com/muesli/termenv/copy.go+37
This diff is too big to show here.
vendor/github.com/muesli/termenv/hyperlink.go+11
This diff is too big to show here.
vendor/github.com/muesli/termenv/notification.go+11
This diff is too big to show here.
vendor/github.com/muesli/termenv/output.go+205
This diff is too big to show here.
vendor/github.com/muesli/termenv/profile.go+112
This diff is too big to show here.
vendor/github.com/muesli/termenv/screen.go+590
This diff is too big to show here.
vendor/github.com/muesli/termenv/style.go+126
This diff is too big to show here.
vendor/github.com/muesli/termenv/templatehelper.go+88
This diff is too big to show here.
vendor/github.com/muesli/termenv/termenv.go+115
This diff is too big to show here.
vendor/github.com/muesli/termenv/termenv_other.go+30
This diff is too big to show here.
vendor/github.com/muesli/termenv/termenv_posix.go+17
This diff is too big to show here.
vendor/github.com/muesli/termenv/termenv_solaris.go+22
This diff is too big to show here.
vendor/github.com/muesli/termenv/termenv_unix.go+301
This diff is too big to show here.
vendor/github.com/muesli/termenv/termenv_windows.go+140
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/.gitignore+23
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/LICENSE+21
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/README.md+121
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/backup.go+134
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/blob.go+266
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/config.go+397
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/conn.go+496
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/const.go+425
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/context.go+234
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/driver/driver.go+821
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/driver/savepoint.go+40
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/driver/time.go+32
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/driver/util.go+74
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/embed/README.md+41
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/embed/build.sh+33
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/embed/exports.txt+141
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/embed/init.go+21
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/embed/sqlite3.wasm
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/error.go+164
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/func.go+319
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_other.go+24
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_unix.go+67
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/alloc/alloc_windows.go+74
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/dotlk/dotlk.go+29
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/dotlk/dotlk_other.go+13
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/dotlk/dotlk_unix.go+50
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/util/const.go+117
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/util/error.go+116
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/util/func.go+202
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/util/handle.go+70
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/util/json.go+50
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/util/math.go+29
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/util/mem.go+153
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/util/mmap_other.go+5
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/util/mmap_unix.go+87
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/util/mmap_windows.go+51
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/util/module.go+25
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/util/pointer.go+11
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/util/reflect.go+10
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/internal/util/set.go+12
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/json.go+12
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/pointer.go+13
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/quote.go+140
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/registry.go+30
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/sqlite.go+344
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/stmt.go+756
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/time.go+357
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/txn.go+301
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/util/sql3util/README.md+9
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/util/sql3util/arg.go+65
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/util/sql3util/const.go+61
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/util/sql3util/parse.go+210
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/util/sql3util/sql3util.go+9
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/util/sql3util/wasm/sql3parse_table.wasm
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/value.go+241
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/README.md+124
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/api.go+216
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/cksm.go+168
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/const.go+251
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/file.go+247
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/filename.go+179
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/lock.go+128
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/lock_other.go+23
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/os_bsd.go+110
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/os_darwin.go+107
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/os_dotlk.go+143
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/os_f2fs_linux.go+37
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/os_linux.go+86
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/os_ofd.go+59
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/os_std.go+50
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/os_std_alloc.go+19
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/os_std_atomic.go+9
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/os_std_rw.go+13
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/os_std_sync.go+9
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/os_unix.go+103
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/os_windows.go+179
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/registry.go+48
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/shm.go+26
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/shm_bsd.go+249
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/shm_copy.go+87
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/shm_dotlk.go+178
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/shm_memlk.go+53
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/shm_ofd.go+176
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/shm_other.go+21
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/shm_windows.go+175
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vfs/vfs.go+462
This diff is too big to show here.
vendor/github.com/ncruces/go-sqlite3/vtab.go+689
This diff is too big to show here.
vendor/github.com/ncruces/julianday/.gitignore+15
This diff is too big to show here.
vendor/github.com/ncruces/julianday/LICENSE+21
This diff is too big to show here.
vendor/github.com/ncruces/julianday/README.md+9
This diff is too big to show here.
vendor/github.com/ncruces/julianday/julianday.go+124
This diff is too big to show here.
vendor/github.com/nfnt/resize/.travis.yml+7
This diff is too big to show here.
vendor/github.com/nfnt/resize/LICENSE+13
This diff is too big to show here.
vendor/github.com/nfnt/resize/README.md+151
This diff is too big to show here.
vendor/github.com/nfnt/resize/converter.go+438
This diff is too big to show here.
vendor/github.com/nfnt/resize/filters.go+143
This diff is too big to show here.
vendor/github.com/nfnt/resize/nearest.go+318
This diff is too big to show here.
vendor/github.com/nfnt/resize/resize.go+620
This diff is too big to show here.
vendor/github.com/nfnt/resize/thumbnail.go+55
This diff is too big to show here.
vendor/github.com/nfnt/resize/ycc.go+387
This diff is too big to show here.
vendor/github.com/nxadm/tail/.cirrus.yml+8
This diff is too big to show here.
vendor/github.com/nxadm/tail/.gitignore+2
This diff is too big to show here.
vendor/github.com/nxadm/tail/CHANGES.md+64
This diff is too big to show here.
vendor/github.com/nxadm/tail/CONTRIBUTING.md+5
This diff is too big to show here.
vendor/github.com/nxadm/tail/Dockerfile+19
This diff is too big to show here.
vendor/github.com/nxadm/tail/LICENSE+21
This diff is too big to show here.
vendor/github.com/nxadm/tail/README.md+45
This diff is too big to show here.
vendor/github.com/nxadm/tail/ratelimiter/Licence+7
This diff is too big to show here.
vendor/github.com/nxadm/tail/ratelimiter/leakybucket.go+97
This diff is too big to show here.
vendor/github.com/nxadm/tail/ratelimiter/memory.go+60
This diff is too big to show here.
vendor/github.com/nxadm/tail/ratelimiter/storage.go+6
This diff is too big to show here.
vendor/github.com/nxadm/tail/tail.go+481
This diff is too big to show here.
vendor/github.com/nxadm/tail/tail_posix.go+17
This diff is too big to show here.
vendor/github.com/nxadm/tail/tail_windows.go+19
This diff is too big to show here.
vendor/github.com/nxadm/tail/util/util.go+49
This diff is too big to show here.
vendor/github.com/nxadm/tail/watch/filechanges.go+37
This diff is too big to show here.
vendor/github.com/nxadm/tail/watch/inotify.go+136
This diff is too big to show here.
vendor/github.com/nxadm/tail/watch/inotify_tracker.go+249
This diff is too big to show here.
vendor/github.com/nxadm/tail/watch/polling.go+119
This diff is too big to show here.
vendor/github.com/nxadm/tail/watch/watch.go+21
This diff is too big to show here.
vendor/github.com/nxadm/tail/winfile/winfile.go+93
This diff is too big to show here.
vendor/github.com/openai/openai-go/.gitignore+4
This diff is too big to show here.
vendor/github.com/openai/openai-go/.release-please-manifest.json+3
This diff is too big to show here.
vendor/github.com/openai/openai-go/.stats.yml+4
This diff is too big to show here.
vendor/github.com/openai/openai-go/Brewfile+1
This diff is too big to show here.
vendor/github.com/openai/openai-go/CHANGELOG.md+392
This diff is too big to show here.
vendor/github.com/openai/openai-go/CONTRIBUTING.md+66
This diff is too big to show here.
vendor/github.com/openai/openai-go/LICENSE+201
This diff is too big to show here.
vendor/github.com/openai/openai-go/MIGRATION.md+284
This diff is too big to show here.
vendor/github.com/openai/openai-go/README.md+948
This diff is too big to show here.
vendor/github.com/openai/openai-go/SECURITY.md+29
This diff is too big to show here.
vendor/github.com/openai/openai-go/aliases.go+440
This diff is too big to show here.
vendor/github.com/openai/openai-go/api.md+785
This diff is too big to show here.
vendor/github.com/openai/openai-go/audio.go+53
This diff is too big to show here.
vendor/github.com/openai/openai-go/audiospeech.go+129
This diff is too big to show here.
vendor/github.com/openai/openai-go/audiotranscription.go+654
This diff is too big to show here.
vendor/github.com/openai/openai-go/audiotranslation.go+117
This diff is too big to show here.
vendor/github.com/openai/openai-go/azure/azure.go+237
This diff is too big to show here.
vendor/github.com/openai/openai-go/batch.go+343
This diff is too big to show here.
vendor/github.com/openai/openai-go/beta.go+31
This diff is too big to show here.
vendor/github.com/openai/openai-go/betaassistant.go+2246
This diff is too big to show here.
vendor/github.com/openai/openai-go/betathread.go+1564
This diff is too big to show here.
vendor/github.com/openai/openai-go/betathreadmessage.go+1712
This diff is too big to show here.
vendor/github.com/openai/openai-go/betathreadrun.go+960
This diff is too big to show here.
vendor/github.com/openai/openai-go/betathreadrunstep.go+1393
This diff is too big to show here.
vendor/github.com/openai/openai-go/chat.go+28
This diff is too big to show here.
vendor/github.com/openai/openai-go/chatcompletion.go+2592
This diff is too big to show here.
vendor/github.com/openai/openai-go/chatcompletionmessage.go+96
This diff is too big to show here.
vendor/github.com/openai/openai-go/client.go+161
This diff is too big to show here.
vendor/github.com/openai/openai-go/completion.go+426
This diff is too big to show here.
vendor/github.com/openai/openai-go/container.go+352
This diff is too big to show here.
vendor/github.com/openai/openai-go/containerfile.go+286
This diff is too big to show here.
vendor/github.com/openai/openai-go/containerfilecontent.go+49
This diff is too big to show here.
vendor/github.com/openai/openai-go/embedding.go+203
This diff is too big to show here.
vendor/github.com/openai/openai-go/field.go+45
This diff is too big to show here.
vendor/github.com/openai/openai-go/file.go+313
This diff is too big to show here.
vendor/github.com/openai/openai-go/finetuning.go+34
This diff is too big to show here.
vendor/github.com/openai/openai-go/finetuningalpha.go+28
This diff is too big to show here.
vendor/github.com/openai/openai-go/finetuningalphagrader.go+672
This diff is too big to show here.
vendor/github.com/openai/openai-go/finetuningcheckpoint.go+28
This diff is too big to show here.
vendor/github.com/openai/openai-go/finetuningcheckpointpermission.go+254
This diff is too big to show here.
vendor/github.com/openai/openai-go/finetuningjob.go+880
This diff is too big to show here.
vendor/github.com/openai/openai-go/finetuningjobcheckpoint.go+149
This diff is too big to show here.
vendor/github.com/openai/openai-go/finetuningmethod.go+1487
This diff is too big to show here.
vendor/github.com/openai/openai-go/grader.go+28
This diff is too big to show here.
vendor/github.com/openai/openai-go/gradergradermodel.go+1179
This diff is too big to show here.
vendor/github.com/openai/openai-go/image.go+645
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/apierror/apierror.go+58
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/apiform/encoder.go+465
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/apiform/form.go+5
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/apiform/richparam.go+20
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/apiform/tag.go+51
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/apijson/decoder.go+691
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/apijson/encoder.go+392
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/apijson/enum.go+145
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/apijson/field.go+23
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/apijson/port.go+120
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/apijson/registry.go+51
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/apijson/subfield.go+67
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/apijson/tag.go+47
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/apijson/union.go+202
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/apiquery/encoder.go+415
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/apiquery/query.go+55
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/apiquery/richparam.go+20
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/apiquery/tag.go+44
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/encoding/json/decode.go+1324
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/encoding/json/encode.go+1391
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/encoding/json/fold.go+48
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/encoding/json/indent.go+182
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/encoding/json/scanner.go+610
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/encoding/json/sentinel/null.go+46
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/encoding/json/shims/shims.go+111
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/encoding/json/stream.go+512
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/encoding/json/tables.go+218
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/encoding/json/tags.go+38
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/encoding/json/time.go+61
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/paramutil/field.go+30
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/paramutil/union.go+48
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/requestconfig/requestconfig.go+635
This diff is too big to show here.
vendor/github.com/openai/openai-go/internal/version.go+5
This diff is too big to show here.
vendor/github.com/openai/openai-go/model.go+134
This diff is too big to show here.
vendor/github.com/openai/openai-go/moderation.go+485
This diff is too big to show here.
vendor/github.com/openai/openai-go/option/middleware.go+38
This diff is too big to show here.
vendor/github.com/openai/openai-go/option/requestoption.go+298
This diff is too big to show here.
vendor/github.com/openai/openai-go/packages/pagination/pagination.go+223
This diff is too big to show here.
vendor/github.com/openai/openai-go/packages/param/encoder.go+101
This diff is too big to show here.
vendor/github.com/openai/openai-go/packages/param/null.go+19
This diff is too big to show here.
vendor/github.com/openai/openai-go/packages/param/option.go+121
This diff is too big to show here.
vendor/github.com/openai/openai-go/packages/param/param.go+168
This diff is too big to show here.
vendor/github.com/openai/openai-go/packages/respjson/respjson.go+88
This diff is too big to show here.
vendor/github.com/openai/openai-go/packages/ssestream/ssestream.go+216
This diff is too big to show here.
vendor/github.com/openai/openai-go/polling.go+95
This diff is too big to show here.
vendor/github.com/openai/openai-go/release-please-config.json+67
This diff is too big to show here.
vendor/github.com/openai/openai-go/responses/aliases.go+440
This diff is too big to show here.
vendor/github.com/openai/openai-go/responses/inputitem.go+136
This diff is too big to show here.
vendor/github.com/openai/openai-go/responses/response.go+12897
This diff is too big to show here.
vendor/github.com/openai/openai-go/shared/constant/constants.go+721
This diff is too big to show here.
vendor/github.com/openai/openai-go/shared/shared.go+796
This diff is too big to show here.
vendor/github.com/openai/openai-go/streamaccumulator.go+195
This diff is too big to show here.
vendor/github.com/openai/openai-go/upload.go+201
This diff is too big to show here.
vendor/github.com/openai/openai-go/uploadpart.go+112
This diff is too big to show here.
vendor/github.com/openai/openai-go/vectorstore.go+905
This diff is too big to show here.
vendor/github.com/openai/openai-go/vectorstorefile.go+534
This diff is too big to show here.
vendor/github.com/openai/openai-go/vectorstorefilebatch.go+364
This diff is too big to show here.
vendor/github.com/openai/openai-go/webhooks/aliases.go+440
This diff is too big to show here.
vendor/github.com/openai/openai-go/webhooks/webhook.go+1208
This diff is too big to show here.
vendor/github.com/pkg/browser/LICENSE+23
This diff is too big to show here.
vendor/github.com/pkg/browser/README.md+55
This diff is too big to show here.
vendor/github.com/pkg/browser/browser.go+57
This diff is too big to show here.
vendor/github.com/pkg/browser/browser_darwin.go+5
This diff is too big to show here.
vendor/github.com/pkg/browser/browser_freebsd.go+14
This diff is too big to show here.
vendor/github.com/pkg/browser/browser_linux.go+21
This diff is too big to show here.
vendor/github.com/pkg/browser/browser_netbsd.go+14
This diff is too big to show here.
vendor/github.com/pkg/browser/browser_openbsd.go+14
This diff is too big to show here.
vendor/github.com/pkg/browser/browser_unsupported.go+12
This diff is too big to show here.
vendor/github.com/pkg/browser/browser_windows.go+7
This diff is too big to show here.
vendor/github.com/pmezard/go-difflib/LICENSE+27
This diff is too big to show here.
vendor/github.com/pmezard/go-difflib/difflib/difflib.go+772
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/.gitignore+18
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/.golangci.yaml+31
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/.goreleaser.yaml+36
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/CHANGELOG.md+291
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/LICENSE+22
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/Makefile+144
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/README.md+493
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/create.go+120
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/database/dialect.go+172
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/database/doc.go+14
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/database/sql_extended.go+23
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/database/store.go+66
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/database/store_extended.go+33
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/db.go+46
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/dialect.go+64
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/down.go+107
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/fix.go+54
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/globals.go+104
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/goose.go+141
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/helpers.go+84
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/install.sh+37
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/internal/controller/controller.go+37
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/internal/dialect/dialectquery/clickhouse.go+44
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/internal/dialect/dialectquery/dialectquery.go+59
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/internal/dialect/dialectquery/mysql.go+53
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/internal/dialect/dialectquery/postgres.go+54
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/internal/dialect/dialectquery/redshift.go+43
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/internal/dialect/dialectquery/sqlite3.go+42
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/internal/dialect/dialectquery/sqlserver.go+42
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/internal/dialect/dialectquery/starrocks.go+45
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/internal/dialect/dialectquery/tidb.go+43
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/internal/dialect/dialectquery/turso.go+7
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/internal/dialect/dialectquery/vertica.go+43
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/internal/dialect/dialectquery/ydb.go+53
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/internal/dialect/dialects.go+18
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/internal/dialect/store.go+169
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/internal/gooseutil/resolve.go+124
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/internal/sqlparser/parse.go+59
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/internal/sqlparser/parser.go+394
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/lock/postgres.go+118
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/lock/session_locker.go+23
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/lock/session_locker_options.go+98
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/log.go+36
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/migrate.go+378
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/migration.go+398
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/migration_sql.go+115
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/osfs.go+28
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/provider.go+650
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/provider_collect.go+196
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/provider_errors.go+48
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/provider_options.go+203
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/provider_run.go+453
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/provider_types.go+91
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/redo.go+51
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/register.go+133
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/reset.go+64
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/status.go+64
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/up.go+221
This diff is too big to show here.
vendor/github.com/pressly/goose/v3/version.go+52
This diff is too big to show here.
vendor/github.com/qjebbs/go-jsons/.gitignore+86
This diff is too big to show here.
vendor/github.com/qjebbs/go-jsons/LICENSE+21
This diff is too big to show here.
vendor/github.com/qjebbs/go-jsons/Makefile+9
This diff is too big to show here.
vendor/github.com/qjebbs/go-jsons/common.go+19
This diff is too big to show here.
vendor/github.com/qjebbs/go-jsons/formats.go+10
This diff is too big to show here.
vendor/github.com/qjebbs/go-jsons/jsons.go+32
This diff is too big to show here.
vendor/github.com/qjebbs/go-jsons/loader.go+101
This diff is too big to show here.
vendor/github.com/qjebbs/go-jsons/merge/map.go+55
This diff is too big to show here.
vendor/github.com/qjebbs/go-jsons/merger.go+170
This diff is too big to show here.
vendor/github.com/qjebbs/go-jsons/merger_extensions.go+27
This diff is too big to show here.
vendor/github.com/qjebbs/go-jsons/merger_loader.go+28
This diff is too big to show here.
vendor/github.com/qjebbs/go-jsons/readme.md+152
This diff is too big to show here.
vendor/github.com/qjebbs/go-jsons/rule/field.go+57
This diff is too big to show here.
vendor/github.com/qjebbs/go-jsons/rule/merge.go+75
This diff is too big to show here.
vendor/github.com/qjebbs/go-jsons/rule/rule.go+93
This diff is too big to show here.
vendor/github.com/qjebbs/go-jsons/rule/sort.go+95
This diff is too big to show here.
vendor/github.com/rivo/uniseg/LICENSE.txt+21
This diff is too big to show here.
vendor/github.com/rivo/uniseg/README.md+137
This diff is too big to show here.
vendor/github.com/rivo/uniseg/doc.go+108
This diff is too big to show here.
vendor/github.com/rivo/uniseg/eastasianwidth.go+2588
This diff is too big to show here.
vendor/github.com/rivo/uniseg/emojipresentation.go+295
This diff is too big to show here.
vendor/github.com/rivo/uniseg/gen_breaktest.go+215
This diff is too big to show here.
vendor/github.com/rivo/uniseg/gen_properties.go+261
This diff is too big to show here.
vendor/github.com/rivo/uniseg/grapheme.go+331
This diff is too big to show here.
vendor/github.com/rivo/uniseg/graphemeproperties.go+1915
This diff is too big to show here.
vendor/github.com/rivo/uniseg/graphemerules.go+176
This diff is too big to show here.
vendor/github.com/rivo/uniseg/line.go+134
This diff is too big to show here.
vendor/github.com/rivo/uniseg/lineproperties.go+3554
This diff is too big to show here.
vendor/github.com/rivo/uniseg/linerules.go+626
This diff is too big to show here.
vendor/github.com/rivo/uniseg/properties.go+208
This diff is too big to show here.
vendor/github.com/rivo/uniseg/sentence.go+90
This diff is too big to show here.
vendor/github.com/rivo/uniseg/sentenceproperties.go+2845
This diff is too big to show here.
vendor/github.com/rivo/uniseg/sentencerules.go+276
This diff is too big to show here.
vendor/github.com/rivo/uniseg/step.go+242
This diff is too big to show here.
vendor/github.com/rivo/uniseg/width.go+61
This diff is too big to show here.
vendor/github.com/rivo/uniseg/word.go+89
This diff is too big to show here.
vendor/github.com/rivo/uniseg/wordproperties.go+1883
This diff is too big to show here.
vendor/github.com/rivo/uniseg/wordrules.go+282
This diff is too big to show here.
vendor/github.com/sabhiram/go-gitignore/.gitignore+28
This diff is too big to show here.
vendor/github.com/sabhiram/go-gitignore/LICENSE+22
This diff is too big to show here.
vendor/github.com/sabhiram/go-gitignore/README.md+15
This diff is too big to show here.
vendor/github.com/sabhiram/go-gitignore/ignore.go+239
This diff is too big to show here.
vendor/github.com/sabhiram/go-gitignore/version_gen.go+12
This diff is too big to show here.
vendor/github.com/sahilm/fuzzy/.editorconfig+18
This diff is too big to show here.
vendor/github.com/sahilm/fuzzy/.gitignore+2
This diff is too big to show here.
vendor/github.com/sahilm/fuzzy/.travis.yml+8
This diff is too big to show here.
vendor/github.com/sahilm/fuzzy/CONTRIBUTING.md+1
This diff is too big to show here.
vendor/github.com/sahilm/fuzzy/Gopkg.lock+20
This diff is too big to show here.
vendor/github.com/sahilm/fuzzy/Gopkg.toml+4
This diff is too big to show here.
vendor/github.com/sahilm/fuzzy/LICENSE+21
This diff is too big to show here.
vendor/github.com/sahilm/fuzzy/Makefile+57
This diff is too big to show here.
vendor/github.com/sahilm/fuzzy/README.md+186
This diff is too big to show here.
vendor/github.com/sahilm/fuzzy/fuzzy.go+254
This diff is too big to show here.
vendor/github.com/sethvargo/go-retry/LICENSE+202
This diff is too big to show here.
vendor/github.com/sethvargo/go-retry/README.md+185
This diff is too big to show here.
vendor/github.com/sethvargo/go-retry/backoff.go+134
This diff is too big to show here.
vendor/github.com/sethvargo/go-retry/backoff_constant.go+25
This diff is too big to show here.
vendor/github.com/sethvargo/go-retry/backoff_exponential.go+47
This diff is too big to show here.
vendor/github.com/sethvargo/go-retry/backoff_fibonacci.go+56
This diff is too big to show here.
vendor/github.com/sethvargo/go-retry/rand.go+54
This diff is too big to show here.
vendor/github.com/sethvargo/go-retry/retry.go+104
This diff is too big to show here.
vendor/github.com/spf13/cast/.gitignore+25
This diff is too big to show here.
vendor/github.com/spf13/cast/LICENSE+21
This diff is too big to show here.
vendor/github.com/spf13/cast/Makefile+40
This diff is too big to show here.
vendor/github.com/spf13/cast/README.md+75
This diff is too big to show here.
vendor/github.com/spf13/cast/cast.go+176
This diff is too big to show here.
vendor/github.com/spf13/cast/caste.go+1510
This diff is too big to show here.
vendor/github.com/spf13/cast/timeformattype_string.go+27
This diff is too big to show here.
vendor/github.com/spf13/cobra/.gitignore+39
This diff is too big to show here.
vendor/github.com/spf13/cobra/.golangci.yml+57
This diff is too big to show here.
vendor/github.com/spf13/cobra/.mailmap+3
This diff is too big to show here.
vendor/github.com/spf13/cobra/CONDUCT.md+37
This diff is too big to show here.
vendor/github.com/spf13/cobra/CONTRIBUTING.md+50
This diff is too big to show here.
vendor/github.com/spf13/cobra/LICENSE.txt+174
This diff is too big to show here.
vendor/github.com/spf13/cobra/MAINTAINERS+13
This diff is too big to show here.
vendor/github.com/spf13/cobra/Makefile+35
This diff is too big to show here.
vendor/github.com/spf13/cobra/README.md+113
This diff is too big to show here.
vendor/github.com/spf13/cobra/active_help.go+60
This diff is too big to show here.
vendor/github.com/spf13/cobra/args.go+131
This diff is too big to show here.
vendor/github.com/spf13/cobra/bash_completions.go+709
This diff is too big to show here.
vendor/github.com/spf13/cobra/bash_completionsV2.go+484
This diff is too big to show here.
vendor/github.com/spf13/cobra/cobra.go+246
This diff is too big to show here.
vendor/github.com/spf13/cobra/command.go+2067
This diff is too big to show here.
vendor/github.com/spf13/cobra/command_notwin.go+20
This diff is too big to show here.
vendor/github.com/spf13/cobra/command_win.go+41
This diff is too big to show here.
vendor/github.com/spf13/cobra/completions.go+1005
This diff is too big to show here.
vendor/github.com/spf13/cobra/fish_completions.go+292
This diff is too big to show here.
vendor/github.com/spf13/cobra/flag_groups.go+290
This diff is too big to show here.
vendor/github.com/spf13/cobra/powershell_completions.go+350
This diff is too big to show here.
vendor/github.com/spf13/cobra/shell_completions.go+98
This diff is too big to show here.
vendor/github.com/spf13/cobra/zsh_completions.go+308
This diff is too big to show here.
vendor/github.com/spf13/pflag/.editorconfig+12
This diff is too big to show here.
vendor/github.com/spf13/pflag/.gitignore+2
This diff is too big to show here.
vendor/github.com/spf13/pflag/.golangci.yaml+4
This diff is too big to show here.
vendor/github.com/spf13/pflag/.travis.yml+22
This diff is too big to show here.
vendor/github.com/spf13/pflag/LICENSE+28
This diff is too big to show here.
vendor/github.com/spf13/pflag/README.md+296
This diff is too big to show here.
vendor/github.com/spf13/pflag/bool.go+94
This diff is too big to show here.
vendor/github.com/spf13/pflag/bool_slice.go+185
This diff is too big to show here.
vendor/github.com/spf13/pflag/bytes.go+209
This diff is too big to show here.
vendor/github.com/spf13/pflag/count.go+96
This diff is too big to show here.
vendor/github.com/spf13/pflag/duration.go+86
This diff is too big to show here.
vendor/github.com/spf13/pflag/duration_slice.go+166
This diff is too big to show here.
vendor/github.com/spf13/pflag/flag.go+1246
This diff is too big to show here.
vendor/github.com/spf13/pflag/float32.go+88
This diff is too big to show here.
vendor/github.com/spf13/pflag/float32_slice.go+174
This diff is too big to show here.
vendor/github.com/spf13/pflag/float64.go+84
This diff is too big to show here.
vendor/github.com/spf13/pflag/float64_slice.go+166
This diff is too big to show here.
vendor/github.com/spf13/pflag/golangflag.go+105
This diff is too big to show here.
vendor/github.com/spf13/pflag/int.go+84
This diff is too big to show here.
vendor/github.com/spf13/pflag/int16.go+88
This diff is too big to show here.
vendor/github.com/spf13/pflag/int32.go+88
This diff is too big to show here.
vendor/github.com/spf13/pflag/int32_slice.go+174
This diff is too big to show here.
vendor/github.com/spf13/pflag/int64.go+84
This diff is too big to show here.
vendor/github.com/spf13/pflag/int64_slice.go+166
This diff is too big to show here.
vendor/github.com/spf13/pflag/int8.go+88
This diff is too big to show here.
vendor/github.com/spf13/pflag/int_slice.go+158
This diff is too big to show here.
vendor/github.com/spf13/pflag/ip.go+97
This diff is too big to show here.
vendor/github.com/spf13/pflag/ip_slice.go+186
This diff is too big to show here.
vendor/github.com/spf13/pflag/ipmask.go+122
This diff is too big to show here.
vendor/github.com/spf13/pflag/ipnet.go+98
This diff is too big to show here.
vendor/github.com/spf13/pflag/ipnet_slice.go+147
This diff is too big to show here.
vendor/github.com/spf13/pflag/string.go+80
This diff is too big to show here.
vendor/github.com/spf13/pflag/string_array.go+125
This diff is too big to show here.
vendor/github.com/spf13/pflag/string_slice.go+163
This diff is too big to show here.
vendor/github.com/spf13/pflag/string_to_int.go+149
This diff is too big to show here.
vendor/github.com/spf13/pflag/string_to_int64.go+149
This diff is too big to show here.
vendor/github.com/spf13/pflag/string_to_string.go+160
This diff is too big to show here.
vendor/github.com/spf13/pflag/uint.go+88
This diff is too big to show here.
vendor/github.com/spf13/pflag/uint16.go+88
This diff is too big to show here.
vendor/github.com/spf13/pflag/uint32.go+88
This diff is too big to show here.
vendor/github.com/spf13/pflag/uint64.go+88
This diff is too big to show here.
vendor/github.com/spf13/pflag/uint8.go+88
This diff is too big to show here.
vendor/github.com/spf13/pflag/uint_slice.go+168
This diff is too big to show here.
vendor/github.com/srwiley/oksvg/.gitignore+2
This diff is too big to show here.
vendor/github.com/srwiley/oksvg/LICENSE+29
This diff is too big to show here.
vendor/github.com/srwiley/oksvg/README.md+49
This diff is too big to show here.
vendor/github.com/srwiley/oksvg/definitions.go+16
This diff is too big to show here.
vendor/github.com/srwiley/oksvg/draw.go+392
This diff is too big to show here.
vendor/github.com/srwiley/oksvg/icon_cursor.go+391
This diff is too big to show here.
vendor/github.com/srwiley/oksvg/path_cursor.go+397
This diff is too big to show here.
vendor/github.com/srwiley/oksvg/path_style.go+35
This diff is too big to show here.
vendor/github.com/srwiley/oksvg/public.go+287
This diff is too big to show here.
vendor/github.com/srwiley/oksvg/svg_icon.go+37
This diff is too big to show here.
vendor/github.com/srwiley/oksvg/svg_path.go+109
This diff is too big to show here.
vendor/github.com/srwiley/oksvg/utils.go+172
This diff is too big to show here.
vendor/github.com/srwiley/rasterx/LICENSE+29
This diff is too big to show here.
vendor/github.com/srwiley/rasterx/README.md+84
This diff is too big to show here.
vendor/github.com/srwiley/rasterx/dash.go+202
This diff is too big to show here.
vendor/github.com/srwiley/rasterx/fill.go+241
This diff is too big to show here.
vendor/github.com/srwiley/rasterx/geomx.go+306
This diff is too big to show here.
vendor/github.com/srwiley/rasterx/gradient.go+310
This diff is too big to show here.
vendor/github.com/srwiley/rasterx/matrix.go+191
This diff is too big to show here.
vendor/github.com/srwiley/rasterx/scan.go+181
This diff is too big to show here.
vendor/github.com/srwiley/rasterx/shapes.go+221
This diff is too big to show here.
vendor/github.com/srwiley/rasterx/stroke.go+677
This diff is too big to show here.
vendor/github.com/stretchr/testify/LICENSE+21
This diff is too big to show here.
vendor/github.com/stretchr/testify/assert/assertion_compare.go+489
This diff is too big to show here.
vendor/github.com/stretchr/testify/assert/assertion_format.go+841
This diff is too big to show here.
vendor/github.com/stretchr/testify/assert/assertion_format.go.tmpl+5
This diff is too big to show here.
vendor/github.com/stretchr/testify/assert/assertion_forward.go+1673
This diff is too big to show here.
vendor/github.com/stretchr/testify/assert/assertion_forward.go.tmpl+5
This diff is too big to show here.
vendor/github.com/stretchr/testify/assert/assertion_order.go+81
This diff is too big to show here.
vendor/github.com/stretchr/testify/assert/assertions.go+2184
This diff is too big to show here.
vendor/github.com/stretchr/testify/assert/doc.go+46
This diff is too big to show here.
vendor/github.com/stretchr/testify/assert/errors.go+10
This diff is too big to show here.
vendor/github.com/stretchr/testify/assert/forward_assertions.go+16
This diff is too big to show here.
vendor/github.com/stretchr/testify/assert/http_assertions.go+165
This diff is too big to show here.
vendor/github.com/stretchr/testify/assert/yaml/yaml_custom.go+25
This diff is too big to show here.
vendor/github.com/stretchr/testify/assert/yaml/yaml_default.go+37
This diff is too big to show here.
vendor/github.com/stretchr/testify/assert/yaml/yaml_fail.go+18
This diff is too big to show here.
vendor/github.com/stretchr/testify/require/doc.go+29
This diff is too big to show here.
vendor/github.com/stretchr/testify/require/forward_requirements.go+16
This diff is too big to show here.
vendor/github.com/stretchr/testify/require/require.go+2124
This diff is too big to show here.
vendor/github.com/stretchr/testify/require/require.go.tmpl+6
This diff is too big to show here.
vendor/github.com/stretchr/testify/require/require_forward.go+1674
This diff is too big to show here.
vendor/github.com/stretchr/testify/require/require_forward.go.tmpl+5
This diff is too big to show here.
vendor/github.com/stretchr/testify/require/requirements.go+29
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/.editorconfig+7
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/.gitattributes+2
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/.gitignore+46
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/.gitmodules+3
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/CONTRIBUTING.md+75
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/LICENSE+201
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/Makefile+364
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/NOTICE+2
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/RATIONALE.md+1587
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/README.md+139
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/api/features.go+214
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/api/wasm.go+766
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/builder.go+367
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/cache.go+123
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/codecov.yml+9
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/config.go+899
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/checkpoint.go+35
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/close.go+63
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/experimental.go+41
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/features.go+15
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/importresolver.go+19
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/listener.go+324
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/memory.go+52
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/sys/dir.go+92
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/sys/errno.go+98
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/sys/error.go+45
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/sys/file.go+316
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/sys/fs.go+292
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/sys/oflag.go+70
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/sys/syscall_errno.go+106
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/sys/syscall_errno_notwindows.go+13
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/sys/syscall_errno_unsupported.go+7
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/sys/syscall_errno_windows.go+66
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/sys/time.go+10
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/experimental/sys/unimplemented.go+160
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/fsconfig.go+213
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/descriptor/table.go+149
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/interpreter/compiler.go+3632
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/interpreter/format.go+22
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/interpreter/interpreter.go+4596
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/interpreter/operations.go+2812
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/interpreter/signature.go+767
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/abi.go+170
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/backend.go+3
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/compiler.go+399
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/compiler_lower.go+226
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/go_call.go+33
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/abi.go+186
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/abi_entry_amd64.go+9
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/abi_entry_amd64.s+29
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/abi_entry_preamble.go+248
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/abi_go_call.go+440
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/cond.go+168
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/ext.go+35
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/instr.go+2447
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/instr_encoding.go+1683
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/lower_constant.go+71
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/lower_mem.go+187
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/machine.go+3733
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/machine_pro_epi_logue.go+303
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/machine_regalloc.go+352
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/machine_vec.go+992
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/operands.go+336
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/reg.go+181
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/stack.go+130
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/abi.go+333
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/abi_entry_arm64.go+9
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/abi_entry_arm64.s+29
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/abi_entry_preamble.go+233
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/abi_go_call.go+430
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/cond.go+215
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/instr.go+2534
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/instr_encoding.go+2351
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/lower_constant.go+301
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/lower_instr.go+2224
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/lower_instr_operands.go+340
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/lower_mem.go+451
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/machine.go+631
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/machine_pro_epi_logue.go+467
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/machine_regalloc.go+351
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/machine_relocation.go+122
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/reg.go+397
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/unwind_stack.go+84
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/machine.go+119
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/regalloc/api.go+124
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/regalloc/reg.go+123
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/regalloc/regalloc.go+1189
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/regalloc/regset.go+96
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/vdef.go+19
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/call_engine.go+708
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/engine.go+843
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/engine_cache.go+303
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/entrypoint_amd64.go+15
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/entrypoint_arm64.go+15
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/entrypoint_others.go+15
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/frontend/frontend.go+567
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/frontend/lower.go+4261
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/frontend/misc.go+10
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/frontend/sort_id.go+13
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/hostmodule.go+88
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/isa_amd64.go+30
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/isa_arm64.go+32
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/isa_other.go+29
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/memmove.go+11
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/module_engine.go+331
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/ssa/basic_block.go+379
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/ssa/basic_block_sort.go+32
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/ssa/builder.go+790
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/ssa/cmp.go+107
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/ssa/funcref.go+12
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/ssa/instructions.go+2976
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/ssa/pass.go+393
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/ssa/pass_blk_layouts.go+334
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/ssa/pass_cfg.go+313
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/ssa/signature.go+49
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/ssa/ssa.go+14
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/ssa/type.go+115
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/ssa/vs.go+114
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi/debug_options.go+196
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi/exitcode.go+109
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi/offsetdata.go+216
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi/perfmap.go+96
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi/perfmap_disabled.go+5
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi/perfmap_enabled.go+5
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi/pool.go+215
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi/ptr.go+15
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi/queue.go+26
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi/resetmap.go+11
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/expctxkeys/checkpoint.go+10
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/expctxkeys/close.go+5
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/expctxkeys/expctxkeys.go+2
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/expctxkeys/importresolver.go+6
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/expctxkeys/listener.go+7
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/expctxkeys/memory.go+4
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/filecache/compilationcache.go+42
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/filecache/file_cache.go+76
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/fsapi/file.go+69
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/fsapi/poll.go+20
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/fsapi/unimplemented.go+27
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/ieee754/ieee754.go+29
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/internalapi/internal.go+9
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/leb128/leb128.go+285
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/moremath/moremath.go+271
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/cpuid.go+35
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_amd64.go+80
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_amd64.s+16
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_arm64.go+71
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_arm64.s+21
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/cpuid_unsupported.go+17
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/crypto.go+17
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/mmap_linux.go+76
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/mmap_other.go+18
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/mmap_unix.go+32
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/mmap_unsupported.go+26
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/mmap_windows.go+95
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/mprotect_bsd.go+22
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/mprotect_syscall.go+10
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/mprotect_unsupported.go+9
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/path.go+6
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/path_windows.go+17
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/platform.go+56
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/time.go+76
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/time_cgo.go+11
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/time_notcgo.go+7
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/platform/time_windows.go+40
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/sock/sock.go+89
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/sock/sock_supported.go+11
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/sock/sock_unsupported.go+10
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/sys/fs.go+457
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/sys/lazy.go+151
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/sys/stdio.go+128
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/sys/sys.go+228
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/sysfs/adapter.go+105
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/sysfs/datasync_linux.go+14
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/sysfs/datasync_tinygo.go+13
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/sysfs/datasync_unsupported.go+14
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/sysfs/dir.go+24
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/sysfs/dirfs.go+99
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/sysfs/dirfs_supported.go+48
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/sysfs/dirfs_unsupported.go+34
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/sysfs/file.go+523
This diff is too big to show here.
vendor/github.com/tetratelabs/wazero/internal/sysfs/file_unix.go+39
This diff is too big to show here.