Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,8 @@ Configuration is stored in the platform-specific config directory:
- **macOS**: `~/Library/Application Support/dispatch/config.json`
- **Windows**: `%APPDATA%\dispatch\config.json`

Set `DISPATCH_CONFIG` to an absolute file path to use a different config file, for example to keep separate work and personal profiles. `config path`, `config get`/`set`/`edit`, and `doctor` all follow the override. A relative or UNC value is ignored and the default location is used.

### From the command line

Read and change settings without opening the TUI or editing JSON by hand:
Expand Down Expand Up @@ -625,6 +627,7 @@ Unknown flags print an error message with usage help and exit with code 1.

| Variable | Description |
|---|---|
| `DISPATCH_CONFIG` | Override the path to the config file. Must be an absolute, non-UNC path; a relative or UNC value is ignored and the default location is used |
| `DISPATCH_DB` | Override the path to the Copilot CLI session store database |
| `DISPATCH_LOG` | Path to a log file (enables debug logging) |
| `DISPATCH_NO_UPDATE_CHECK` | Skip the background release check when set to `1`, `true`, `yes`, or `on` |
Expand Down
25 changes: 24 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,15 +531,38 @@ func Default() *Config {
}
}

// configPath returns the full path to the configuration file.
// configPath returns the full path to the configuration file. When the
// DISPATCH_CONFIG environment variable holds an absolute, non-UNC path, that
// path is used instead of the default OS config location. A relative or UNC
// value is ignored so a bad override falls back to the default.
func configPath() (string, error) {
if override := configPathOverride(); override != "" {
return override, nil
}
dir, err := platform.ConfigDir()
if err != nil {
return "", err
}
return filepath.Join(dir, configFileName), nil
}

// configPathOverride returns a validated config path from the DISPATCH_CONFIG
// environment variable, or an empty string when the variable is unset or names
// a relative or UNC path. This mirrors the guard used for the CLI log file so
// the override cannot point at a network location or a path relative to the
// current directory.
func configPathOverride() string {
raw := os.Getenv("DISPATCH_CONFIG")
if raw == "" {
return ""
}
cleaned := filepath.Clean(raw)
if !filepath.IsAbs(cleaned) || strings.HasPrefix(cleaned, `\\`) {
return ""
}
return cleaned
}

// ConfigPath returns the full path to the configuration file.
func ConfigPath() (string, error) {
return configPath()
Expand Down
91 changes: 91 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,97 @@ func TestConfigPathFormat(t *testing.T) {
}
}

// ---------------------------------------------------------------------------
// DISPATCH_CONFIG path override
// ---------------------------------------------------------------------------

func TestConfigPathHonorsDispatchConfigOverride(t *testing.T) {
withTempConfigDir(t)
custom := filepath.Join(t.TempDir(), "profile.json")
t.Setenv("DISPATCH_CONFIG", custom)

path, err := configPath()
if err != nil {
t.Fatalf("configPath: %v", err)
}
if path != custom {
t.Errorf("configPath = %q, want override %q", path, custom)
}
pub, err := ConfigPath()
if err != nil {
t.Fatalf("ConfigPath: %v", err)
}
if pub != custom {
t.Errorf("ConfigPath = %q, want override %q", pub, custom)
}
}

func TestSaveAndLoadUseDispatchConfigOverride(t *testing.T) {
withTempConfigDir(t)
custom := filepath.Join(t.TempDir(), "profile.json")
t.Setenv("DISPATCH_CONFIG", custom)

if err := Save(&Config{Agent: "reviewer", Model: "claude-3"}); err != nil {
t.Fatalf("Save: %v", err)
}
if _, err := os.Stat(custom); err != nil {
t.Fatalf("override file should exist after Save: %v", err)
}

loaded, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if loaded.Agent != "reviewer" {
t.Errorf("Agent = %q, want 'reviewer'", loaded.Agent)
}
if loaded.Model != "claude-3" {
t.Errorf("Model = %q, want 'claude-3'", loaded.Model)
}
}

func TestConfigPathIgnoresUnusableDispatchConfig(t *testing.T) {
for _, tc := range []struct {
name string
value string
}{
{name: "relative", value: filepath.Join("nested", "profile.json")},
{name: "unc", value: `\\server\share\profile.json`},
} {
t.Run(tc.name, func(t *testing.T) {
withTempConfigDir(t)
t.Setenv("DISPATCH_CONFIG", tc.value)

path, err := configPath()
if err != nil {
t.Fatalf("configPath: %v", err)
}
if filepath.Base(path) != configFileName {
t.Errorf("config file name = %q, want %q", filepath.Base(path), configFileName)
}
if filepath.Base(filepath.Dir(path)) != "dispatch" {
t.Errorf("config dir = %q, want default 'dispatch' parent", filepath.Dir(path))
}
})
}
}

func TestConfigPathUnsetDispatchConfigUsesDefault(t *testing.T) {
withTempConfigDir(t)
t.Setenv("DISPATCH_CONFIG", "")

path, err := configPath()
if err != nil {
t.Fatalf("configPath: %v", err)
}
if filepath.Base(path) != configFileName {
t.Errorf("config file name = %q, want %q", filepath.Base(path), configFileName)
}
if filepath.Base(filepath.Dir(path)) != "dispatch" {
t.Errorf("config dir = %q, want default 'dispatch' parent", filepath.Dir(path))
}
}

func TestDefaultFieldTypes(t *testing.T) {
t.Parallel()
cfg := Default()
Expand Down