feat: use XDG base directories in fluid-cli - #67
Conversation
Replace hardcoded ~/.fluid/ with XDG-compliant paths. New resolution order: $XDG_CONFIG_HOME/fluid, %AppData%/fluid (Windows), ~/.config/fluid. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code ReviewGood goal — XDG compliance is the right direction for a CLI tool. The implementation is clean, but there are several issues worth addressing before merging. Breaking change: no migration from
|
| Issue | Severity |
|---|---|
No migration from ~/.fluid/ |
High |
Missing tests for paths package |
High (per CLAUDE.md) |
state.db in wrong XDG directory |
Medium |
Error handling regression in paths.go |
Medium |
| Windows fallback to Unix path | Low |
Unrelated API @BasePath change bundled in |
Low |
The core approach is sound — a centralized paths package is clearly better than scattered filepath.Join(home, ".fluid", ...) calls. Address the migration story and add tests and this will be in good shape.
There was a problem hiding this comment.
Pull request overview
This pull request implements XDG Base Directory Specification support for the fluid-cli configuration system, replacing hardcoded ~/.fluid/ paths with platform-appropriate configuration directories. The PR also updates API documentation to consistently include the /v1 prefix in route paths rather than relying on a BasePath annotation.
Changes:
- Introduced
fluid-cli/internal/pathspackage for centralized, XDG-compliant path resolution across Unix, macOS, and Windows - Refactored config, store, updater, and CLI entry points to use the new paths package
- Updated OpenAPI specification and Swagger annotations to include
/v1in route paths with BasePath changed from/v1to/
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| fluid-cli/internal/paths/paths.go | New package providing XDG-compliant path resolution with platform-specific defaults |
| fluid-cli/internal/config/config.go | Updated to use paths.ConfigDir() instead of hardcoded home directory path |
| fluid-cli/internal/store/sqlite/sqlite.go | Updated to use paths.StateDB() for database path resolution |
| fluid-cli/internal/updater/updater.go | Updated to use paths.ConfigDir() for cache directory |
| fluid-cli/cmd/fluid-cli/main.go | Updated to use paths.ConfigFile() and revised help text to mention XDG |
| fluid-cli/.gitignore | Removed fluid-cli entry (binary is actually bin/fluid, already ignored) |
| web/src/components/docs/api-endpoint-card.tsx | Removed /v1 prefix prepending since paths now include it |
| web/src/content/blog/how-does-tunneling-work.mdx | Minor content flow improvement |
| api/internal/rest/*_handlers.go | Updated Swagger @Router annotations to include /v1 prefix |
| api/docs/openapi.yaml | Updated server URL and all route paths to include /v1 prefix |
| api/cmd/server/main.go | Updated @BasePath annotation from /v1 to / |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| package paths | ||
|
|
||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
| "runtime" | ||
| ) | ||
|
|
||
| // ConfigDir returns the fluid configuration directory. | ||
| // | ||
| // Resolution order: | ||
| // 1. $XDG_CONFIG_HOME/fluid (if set) | ||
| // 2. %AppData%/fluid (Windows) | ||
| // 3. ~/.config/fluid (macOS, Linux) | ||
| func ConfigDir() string { | ||
| if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" { | ||
| return filepath.Join(xdg, "fluid") | ||
| } | ||
| if runtime.GOOS == "windows" { | ||
| if dir, err := os.UserConfigDir(); err == nil { | ||
| return filepath.Join(dir, "fluid") | ||
| } | ||
| } | ||
| home, _ := os.UserHomeDir() | ||
| return filepath.Join(home, ".config", "fluid") | ||
| } | ||
|
|
||
| // ConfigFile returns the path to the config.yaml file. | ||
| func ConfigFile() string { | ||
| return filepath.Join(ConfigDir(), "config.yaml") | ||
| } | ||
|
|
||
| // StateDB returns the path to the SQLite state database. | ||
| func StateDB() string { | ||
| return filepath.Join(ConfigDir(), "state.db") | ||
| } |
There was a problem hiding this comment.
The new paths package lacks test coverage. Given that path resolution is critical for the application's ability to find its configuration and state, tests should be added to verify:
- XDG_CONFIG_HOME environment variable handling
- Platform-specific defaults (Windows vs Unix)
- Behavior when os.UserHomeDir() fails
- That ConfigFile() and StateDB() return correct paths relative to ConfigDir()
This is especially important since other parts of the codebase (like updater.go) check for empty string returns from ConfigDir(), suggesting there may be edge cases to handle.
| home, _ := os.UserHomeDir() | ||
| return filepath.Join(home, ".config", "fluid") |
There was a problem hiding this comment.
The error from os.UserHomeDir() is silently ignored, which means that if it fails, the function will return .config/fluid (relative path) instead of an absolute path. This could lead to unexpected behavior where the config directory is created relative to the current working directory rather than a proper fallback location.
Consider handling this error explicitly, either by checking if home is empty and providing a better fallback, or by returning an error from ConfigDir() that callers can handle appropriately.
| home, _ := os.UserHomeDir() | |
| return filepath.Join(home, ".config", "fluid") | |
| home, err := os.UserHomeDir() | |
| if err == nil && home != "" { | |
| return filepath.Join(home, ".config", "fluid") | |
| } | |
| // Fallback: use the system temporary directory to ensure an absolute path. | |
| return filepath.Join(os.TempDir(), "fluid") |
|
|
||
| func init() { | ||
| rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default ~/.fluid/config.yaml)") | ||
| rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default $XDG_CONFIG_HOME/fluid/config.yaml)") |
There was a problem hiding this comment.
The help text says "default $XDG_CONFIG_HOME/fluid/config.yaml", but this is not entirely accurate. The actual default depends on the platform and environment:
- If XDG_CONFIG_HOME is set:
$XDG_CONFIG_HOME/fluid/config.yaml - On Windows:
%AppData%/fluid/config.yaml - On Unix (when XDG_CONFIG_HOME is not set):
~/.config/fluid/config.yaml
Consider updating the help text to be more accurate, such as: "config file (default: XDG config dir or ~/.config/fluid/config.yaml)" or similar wording that acknowledges the platform-specific behavior without being overly verbose.
| rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default $XDG_CONFIG_HOME/fluid/config.yaml)") | |
| rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default: XDG config dir or ~/.config/fluid/config.yaml)") |
- Return (string, error) from all paths functions to surface failures - Add DataDir() using $XDG_DATA_HOME for state.db, history, cache - Add MaybeMigrate() to copy files from legacy ~/.fluid/ on first run - Fix Windows fallback (return error instead of falling through to Unix) - Fix stale ~/.fluid references in onboarding and comments - Add comprehensive tests for paths and migration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Review: feat: use XDG base directories in fluid-cliOverall this is a solid improvement — better cross-platform defaults, proper error handling where Bug: History file migration destination doesn't match where the app reads it
func HistoryPath(configPath string) string {
return filepath.Join(filepath.Dir(configPath), "history")
}After migration, Either:
Partial migration is silent and unrecoverableErrors from for _, name := range configFiles {
_ = copyFile(filepath.Join(oldDir, name), filepath.Join(configDir, name))
}If the copy fails mid-migration (disk full, permissions, etc.), the new config dir will exist (migration created it) but files won't all be there. On the next startup, Suggested fix: collect errors, report them to stderr, and consider not creating the config dir until all copies succeed (or use a sentinel file like Migration not triggered in
|
- HistoryPath() now uses paths.DataDir() directly instead of deriving from configPath, fixing history silently lost after migration - Use sentinel file (.migrated-from-dot-fluid) instead of config dir existence check, making partial migration failures retriable - Collect and report copy errors to stderr instead of silently discarding - Add MaybeMigrate() call to doctorCmd so first-run migration works - Replace log.Printf with fmt.Fprintf in mustConfigDir() to avoid TUI corruption from timestamp-prefixed log output Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code ReviewThe XDG migration is well thought-out — non-destructive copy, sentinel file to avoid re-running, good test coverage of edge cases. A few issues worth addressing: Bug: SQLite WAL files not included in migration
dataFiles := []string{"state.db", "history"}If the previous session was killed mid-transaction while SQLite's WAL mode was active, the WAL file holds uncommitted data that makes the main database file incomplete. Copying dataFiles := []string{"state.db", "state.db-wal", "state.db-shm", "history"}Missing files are already silently skipped ( Code duplication:
|
- Include SQLite WAL/SHM sidecar files in migration to prevent DB corruption - Deduplicate MaybeMigrate() calls via PersistentPreRunE on rootCmd - Fix Windows DataDir to use %LOCALAPPDATA% instead of %APPDATA% - Skip symlinks in copyDir to avoid io.Copy failures on Linux Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code ReviewThe previous review feedback has been addressed well: WAL files are now included in the migration, Stale comment in
|
Ensures kernel buffers are flushed to disk after io.Copy, preventing partially-written state.db if a crash occurs before the sentinel is written. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code ReviewTwo of the three issues from the previous review are still unresolved, and one was fixed. Here's the status: Fixed:
|
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code ReviewThis is a well-structured change — XDG compliance is the right direction and the implementation is largely solid. The migration logic and test coverage are good. Here are the issues I found, roughly in order of severity. Bug: Copying SQLite WAL files is unsafeIn dataFiles := []string{"state.db", "state.db-wal", "state.db-shm", "history"}WAL and SHM files are only meaningful alongside their originating database connection. Copying all three with a file-level
|
Code ReviewThe XDG migration approach is sound — migration sentinel, non-destructive copy, good test coverage for the main scenarios. The previous review captured the key issues well; here are a few additional points not yet mentioned. Bug:
|
Summary
fluid-cli/internal/pathspackage for centralized XDG-compliant path resolution~/.fluid/with$XDG_CONFIG_HOME/fluid(or~/.config/fluidon Unix,%AppData%/fluidon Windows)Test plan
cd fluid-cli && go build ./...compiles cleancd fluid-cli && make testall tests passXDG_CONFIG_HOME=/tmp/test-fluid fluid --helpshows correct default path🤖 Generated with Claude Code