Skip to content

Commit fdb30c3

Browse files
neongreenclaude
andcommitted
conf: Add tilde (~) expansion support for config_path
Users can now use portable tilde notation in config files: config_path = '~/.config/jj/config.toml' Instead of hardcoded absolute paths: config_path = '/Users/username/.config/jj/config.toml' Implementation: - Added ExpandPath() function to expand tilde to home directory - GetTool() now automatically expands tilde in config_path - DefaultConfig() uses tilde notation for portability - Load() expands tilde when loading config from file Testing: - Added comprehensive tests for ExpandPath() - Added TestLoadExpandsTildePaths integration test - All package tests pass (config, editors, schemas, tools) Benefits: - Config files are portable across users/systems - No need to update paths when home directory changes - Cleaner, more readable configuration files Closes: issues-36 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent fb02788 commit fdb30c3

3 files changed

Lines changed: 205 additions & 14 deletions

File tree

.beads/issues.jsonl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,8 @@
433433
{"id":"issues-32","title":"Add comprehensive unit tests for declarative state management","description":"The new declarative state management system lacks unit tests for critical functionality.\n\nMissing test coverage:\n1. Config.SetToolValue/GetToolValue/UnsetToolValue methods\n2. Config.SetShim/GetShim/UnsetShim methods \n3. applyTool() function with various scenarios\n4. showToolStatus() drift detection logic\n5. Integration between state recording and target file application\n\nTest scenarios needed:\n- State recording and retrieval\n- Drift detection (in sync, drift, missing values)\n- Apply operations (dry-run vs actual)\n- Error handling for invalid tools/paths\n- Edge cases (empty state, missing files)\n\nPriority 1 since this is core functionality without test coverage.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-21T23:20:16.142583+02:00","updated_at":"2025-10-21T23:22:53.660666+02:00","closed_at":"2025-10-21T23:22:53.660666+02:00"}
434434
{"id":"issues-33","title":"Add unit tests for tools/jj, tools/shims, tools/starship packages","description":"Three tool packages have no unit test coverage.\n\nMissing tests for:\n1. pkg/tools/jj/ - JJ tool operations, schema validation, config handling\n2. pkg/tools/shims/ - Shim creation, listing, removal, validation \n3. pkg/tools/starship/ - Starship config operations, path validation\n\nEach package needs tests for:\n- Tool initialization (NewTool functions)\n- SetConfig/GetConfig operations\n- Path validation logic\n- Error handling\n- Dry-run modes\n- Config file handling\n\nPriority 2 since basic functionality works but lacks test safety net.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-21T23:20:16.195189+02:00","updated_at":"2025-10-22T08:28:48.581102+02:00","closed_at":"2025-10-22T08:28:48.581102+02:00"}
435435
{"id":"issues-34","title":"Fix JJ schema compilation warning and refactor duplicated switch statements","description":"Technical debt cleanup for better maintainability.\n\n1. JJ Schema Issue:\nWarning: jj schema compilation had issues: duplicate enum values at indexes 10 and 11\nNeed to fix jj.json schema file to remove duplicate enum entries\n\n2. Code Duplication:\napplyToolValue() and getActualValue() have identical switch statements\nShould extract into a single tool factory/registry pattern\n\nRefactor to:\n- toolRegistry map[string]ToolFactory\n- Single function to get tool instance by name\n- Cleaner, more maintainable code\n\n3. Error Handling:\nSome edge cases not properly handled in new commands\nAdd proper validation and error messages\n\nPriority 2 for code quality and maintainability.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-21T23:20:16.239878+02:00","updated_at":"2025-10-22T07:54:36.939519+02:00","closed_at":"2025-10-22T07:54:36.939519+02:00"}
436+
{"id":"issues-35","title":"conf: Make configuration management LLM-friendly","description":"Design and implement features to make conf a natural choice for LLM-driven configuration management:\n\nGoals:\n- Natural language to config operations\n- Clear, parseable status output for LLMs\n- Schema-based validation and suggestions\n- Atomic operations with clear success/failure\n- Readable diffs and previews\n\nAreas to explore:\n- JSON output modes for programmatic access\n- Structured error messages\n- Enhanced schema documentation\n- API/library mode for direct integration\n- Clear operation semantics\n\nWill break down into specific tasks after initial design review.","status":"open","priority":2,"issue_type":"epic","created_at":"2025-10-22T10:54:43.862155+02:00","updated_at":"2025-10-22T10:54:43.862155+02:00"}
437+
{"id":"issues-36","title":"conf: Support tilde (~) resolution in config_path","description":"Currently config_path in conf's config.toml requires full absolute paths like:\n config_path = '/Users/artyom/.config/jj/config.toml'\n\nShould support tilde notation for better portability:\n config_path = '~/.config/jj/config.toml'\n\nImplement tilde expansion when reading config_path values so configs are more portable across systems and users.","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-22T13:29:40.593353+02:00","updated_at":"2025-10-22T13:29:40.593353+02:00"}
436438
{"id":"issues-5","title":"Create robust mise task for building preprocessor and example book","description":"Build system currently has no validation - preprocessor may or may not exist, book builds with stale JS files, no error checking.\n\nRequirements:\n- mise task that builds preprocessor if needed (check if Cargo project exists)\n- Verifies preprocessor is installed/available before building book\n- Builds TypeScript/JS assets\n- Builds example book with mdbook\n- Fails loudly if anything is missing or out of date\n- Should be the ONLY way to build for testing\n\nCurrent problems:\n- mdbook-comments preprocessor not found but book still builds\n- HTML has inlined JS from old builds\n- No way to ensure fresh, correct builds\n- Manual copying of files (bad!)\n\nThis task should replace all manual build steps and be used exclusively for testing.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-21T10:49:03.911981+02:00","updated_at":"2025-10-21T10:56:08.81806+02:00","closed_at":"2025-10-21T10:56:08.81806+02:00"}
437439
{"id":"issues-6","title":"Clean up test task to kill servers and reset database","description":"The test:e2e task doesn't properly clean up before running:\n- Doesn't kill existing json-server or mdbook servers\n- Doesn't clean /tmp/test-db.json\n- Can lead to tests using stale data/code\n\nFix mise test:e2e task to:\n1. Kill any running json-server on port 54322\n2. Kill any running mdbook serve on port 3300 \n3. Remove /tmp/test-db.json\n4. Then run build:all\n5. Then run Playwright tests\n\nThis ensures fresh, clean state for every test run.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-21T10:53:25.66892+02:00","updated_at":"2025-10-21T12:03:15.464857+02:00","closed_at":"2025-10-21T12:03:15.464857+02:00"}
438440
{"id":"issues-7","title":"Fix Preact reactivity for reply updates in Comment components","description":"The Comment component isn't re-rendering when replies are added because:\n\n1. buildReplyStructure() creates new comment objects, but handleUpdate doesn't properly pass updated references to Preact\n2. The key={`${comment.id}-replies-${comment.replies?.length || 0}`} strategy works in theory but the comment objects themselves aren't being updated in the component props\n3. Need to ensure that when handleUpdate re-renders the CommentSection, it gets the freshly built comment objects with updated replies arrays\n\nThis is causing test #6 'should reply to a comment' to fail - the reply saves to database but UI doesn't refresh.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-10-21T12:03:43.819407+02:00","updated_at":"2025-10-21T12:05:11.808144+02:00","closed_at":"2025-10-21T12:05:11.808144+02:00"}

conf/pkg/config/config.go

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,21 @@ type ToolConfig struct {
2424

2525
// DefaultConfig returns a new Config with default settings
2626
func DefaultConfig() *Config {
27-
homeDir, err := os.UserHomeDir()
28-
if err != nil {
29-
homeDir = "~"
30-
}
31-
3227
return &Config{
3328
Tools: map[string]ToolConfig{
3429
"jj": {
3530
Name: "jj",
36-
ConfigPath: filepath.Join(homeDir, ".config/jj/config.toml"),
31+
ConfigPath: "~/.config/jj/config.toml",
3732
SchemaPath: "embedded://jj.json",
3833
},
3934
"mise": {
4035
Name: "mise",
41-
ConfigPath: filepath.Join(homeDir, ".config", "mise", "config.toml"),
36+
ConfigPath: "~/.config/mise/config.toml",
4237
SchemaPath: "embedded://mise.toml",
4338
},
4439
"starship": {
4540
Name: "starship",
46-
ConfigPath: filepath.Join(homeDir, ".config", "starship.toml"),
41+
ConfigPath: "~/.config/starship.toml",
4742
},
4843
},
4944
}
@@ -70,6 +65,31 @@ func ConfigPath() (string, error) {
7065
return filepath.Join(configDir, "config.toml"), nil
7166
}
7267

68+
// ExpandPath expands tilde (~) in paths to the user's home directory
69+
func ExpandPath(path string) (string, error) {
70+
if path == "" {
71+
return path, nil
72+
}
73+
74+
// Only expand if path starts with ~
75+
if path[0] != '~' {
76+
return path, nil
77+
}
78+
79+
homeDir, err := os.UserHomeDir()
80+
if err != nil {
81+
return "", fmt.Errorf("failed to get user home directory: %w", err)
82+
}
83+
84+
// Handle ~/ or just ~
85+
if len(path) == 1 || path[1] == '/' {
86+
return filepath.Join(homeDir, path[1:]), nil
87+
}
88+
89+
// Don't support ~user syntax for now
90+
return path, nil
91+
}
92+
7393
// Load loads the configuration from the config file, creating it if it doesn't exist
7494
func Load() (*Config, error) {
7595
configPath, err := ConfigPath()
@@ -97,6 +117,14 @@ func Load() (*Config, error) {
97117
return nil, fmt.Errorf("failed to parse config file: %w", err)
98118
}
99119

120+
// Expand tilde in config paths
121+
for name, tool := range config.Tools {
122+
if expandedPath, err := ExpandPath(tool.ConfigPath); err == nil {
123+
tool.ConfigPath = expandedPath
124+
config.Tools[name] = tool
125+
}
126+
}
127+
100128
return &config, nil
101129
}
102130

@@ -127,10 +155,19 @@ func (c *Config) Save() error {
127155
return nil
128156
}
129157

130-
// GetTool returns the configuration for a specific tool
158+
// GetTool returns the configuration for a specific tool with expanded paths
131159
func (c *Config) GetTool(name string) (ToolConfig, bool) {
132160
tool, exists := c.Tools[name]
133-
return tool, exists
161+
if !exists {
162+
return tool, false
163+
}
164+
165+
// Expand tilde in config path
166+
if expandedPath, err := ExpandPath(tool.ConfigPath); err == nil {
167+
tool.ConfigPath = expandedPath
168+
}
169+
170+
return tool, true
134171
}
135172

136173
// SetTool sets the configuration for a specific tool

conf/pkg/config/config_test.go

Lines changed: 156 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ func TestDefaultConfig(t *testing.T) {
1313
t.Fatal("Default config should not be nil")
1414
}
1515

16-
if len(config.Tools) != 2 {
17-
t.Errorf("Default config should have 2 tools, got %d", len(config.Tools))
16+
if len(config.Tools) != 3 {
17+
t.Errorf("Default config should have 3 tools, got %d", len(config.Tools))
1818
}
1919

2020
// Check jj tool config
@@ -25,9 +25,13 @@ func TestDefaultConfig(t *testing.T) {
2525
if jjTool.Name != "jj" {
2626
t.Errorf("jj tool name should be 'jj', got '%s'", jjTool.Name)
2727
}
28+
// GetTool() expands tilde, so check for expanded path
2829
if !strings.Contains(jjTool.ConfigPath, ".config/jj/config.toml") {
2930
t.Errorf("jj config path should contain '.config/jj/config.toml', got '%s'", jjTool.ConfigPath)
3031
}
32+
if strings.HasPrefix(jjTool.ConfigPath, "~") {
33+
t.Errorf("jj config path should be expanded, got '%s'", jjTool.ConfigPath)
34+
}
3135

3236
// Check mise tool config
3337
miseTool, exists := config.GetTool("mise")
@@ -37,8 +41,92 @@ func TestDefaultConfig(t *testing.T) {
3741
if miseTool.Name != "mise" {
3842
t.Errorf("mise tool name should be 'mise', got '%s'", miseTool.Name)
3943
}
40-
if !strings.Contains(miseTool.ConfigPath, "mise/config.toml") {
41-
t.Errorf("mise config path should contain 'mise/config.toml', got '%s'", miseTool.ConfigPath)
44+
// GetTool() expands tilde, so check for expanded path
45+
if !strings.Contains(miseTool.ConfigPath, ".config/mise/config.toml") {
46+
t.Errorf("mise config path should contain '.config/mise/config.toml', got '%s'", miseTool.ConfigPath)
47+
}
48+
if strings.HasPrefix(miseTool.ConfigPath, "~") {
49+
t.Errorf("mise config path should be expanded, got '%s'", miseTool.ConfigPath)
50+
}
51+
52+
// Check starship tool config
53+
starshipTool, exists := config.GetTool("starship")
54+
if !exists {
55+
t.Error("Default config should include starship tool")
56+
}
57+
if starshipTool.Name != "starship" {
58+
t.Errorf("starship tool name should be 'starship', got '%s'", starshipTool.Name)
59+
}
60+
// GetTool() expands tilde, so check for expanded path
61+
if !strings.Contains(starshipTool.ConfigPath, ".config/starship.toml") {
62+
t.Errorf("starship config path should contain '.config/starship.toml', got '%s'", starshipTool.ConfigPath)
63+
}
64+
if strings.HasPrefix(starshipTool.ConfigPath, "~") {
65+
t.Errorf("starship config path should be expanded, got '%s'", starshipTool.ConfigPath)
66+
}
67+
}
68+
69+
func TestExpandPath(t *testing.T) {
70+
homeDir, err := os.UserHomeDir()
71+
if err != nil {
72+
t.Fatalf("Failed to get home dir: %v", err)
73+
}
74+
75+
tests := []struct {
76+
name string
77+
input string
78+
expected string
79+
wantErr bool
80+
}{
81+
{
82+
name: "expand tilde with slash",
83+
input: "~/.config/jj/config.toml",
84+
expected: homeDir + "/.config/jj/config.toml",
85+
wantErr: false,
86+
},
87+
{
88+
name: "expand just tilde",
89+
input: "~",
90+
expected: homeDir,
91+
wantErr: false,
92+
},
93+
{
94+
name: "no tilde - absolute path",
95+
input: "/etc/config.toml",
96+
expected: "/etc/config.toml",
97+
wantErr: false,
98+
},
99+
{
100+
name: "no tilde - relative path",
101+
input: "./config.toml",
102+
expected: "./config.toml",
103+
wantErr: false,
104+
},
105+
{
106+
name: "empty string",
107+
input: "",
108+
expected: "",
109+
wantErr: false,
110+
},
111+
{
112+
name: "tilde in middle - no expansion",
113+
input: "/path/~/config.toml",
114+
expected: "/path/~/config.toml",
115+
wantErr: false,
116+
},
117+
}
118+
119+
for _, tt := range tests {
120+
t.Run(tt.name, func(t *testing.T) {
121+
result, err := ExpandPath(tt.input)
122+
if (err != nil) != tt.wantErr {
123+
t.Errorf("ExpandPath() error = %v, wantErr %v", err, tt.wantErr)
124+
return
125+
}
126+
if result != tt.expected {
127+
t.Errorf("ExpandPath() = %v, want %v", result, tt.expected)
128+
}
129+
})
42130
}
43131
}
44132

@@ -135,3 +223,67 @@ func TestSaveAndLoad(t *testing.T) {
135223
t.Errorf("Test tool name should be 'test', got '%s'", testTool.Name)
136224
}
137225
}
226+
227+
func TestLoadExpandsTildePaths(t *testing.T) {
228+
// Create a temporary directory for testing
229+
tempDir, err := os.MkdirTemp("", "conf-test")
230+
if err != nil {
231+
t.Fatalf("Failed to create temp dir: %v", err)
232+
}
233+
defer os.RemoveAll(tempDir)
234+
235+
// Override HOME for testing
236+
originalHome := os.Getenv("HOME")
237+
os.Setenv("HOME", tempDir)
238+
defer os.Setenv("HOME", originalHome)
239+
240+
// Create config with tilde paths
241+
config := &Config{
242+
Tools: map[string]ToolConfig{
243+
"jj": {
244+
Name: "jj",
245+
ConfigPath: "~/.config/jj/config.toml",
246+
SchemaPath: "embedded://jj.json",
247+
},
248+
"mise": {
249+
Name: "mise",
250+
ConfigPath: "~/.config/mise/config.toml",
251+
SchemaPath: "embedded://mise.toml",
252+
},
253+
},
254+
}
255+
256+
// Save config
257+
err = config.Save()
258+
if err != nil {
259+
t.Fatalf("Failed to save config: %v", err)
260+
}
261+
262+
// Load config - should expand tilde paths
263+
loadedConfig, err := Load()
264+
if err != nil {
265+
t.Fatalf("Failed to load config: %v", err)
266+
}
267+
268+
// Verify jj path was expanded
269+
jjTool, exists := loadedConfig.GetTool("jj")
270+
if !exists {
271+
t.Fatal("Loaded config should contain jj tool")
272+
}
273+
274+
expectedJJPath := tempDir + "/.config/jj/config.toml"
275+
if jjTool.ConfigPath != expectedJJPath {
276+
t.Errorf("jj ConfigPath should be expanded to '%s', got '%s'", expectedJJPath, jjTool.ConfigPath)
277+
}
278+
279+
// Verify mise path was expanded
280+
miseTool, exists := loadedConfig.GetTool("mise")
281+
if !exists {
282+
t.Fatal("Loaded config should contain mise tool")
283+
}
284+
285+
expectedMisePath := tempDir + "/.config/mise/config.toml"
286+
if miseTool.ConfigPath != expectedMisePath {
287+
t.Errorf("mise ConfigPath should be expanded to '%s', got '%s'", expectedMisePath, miseTool.ConfigPath)
288+
}
289+
}

0 commit comments

Comments
 (0)