Skip to content

Commit a27aab7

Browse files
fix(cli): expand leading tilde in DuckDB mirror paths (#1274)
DuckDB config resolution currently leaves a leading `~` literal in `[duckdb].path`, so daemon-backed pushes can canonicalize the server mirror path against the daemon working directory instead of the user's home directory. This expands leading-home shorthand during DuckDB config resolution, alongside the existing defaulting and env expansion, so the daemon and in-process callers resolve the same mirror path from the same config value. `${HOME}` expansion, default-path fallback, and non-leading tildes keep their current behavior. The change stays in `internal/config` with focused resolution regression coverage. The config snippet and daemon-vs-in-process repro came from @halms's issue report. Closes #1264 --------- Co-authored-by: Marius van Niekerk <marius.v.niekerk@gmail.com>
1 parent 6c3317a commit a27aab7

15 files changed

Lines changed: 396 additions & 5 deletions

cmd/agentsview/duckdb.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/spf13/cobra"
1919
"go.kenn.io/agentsview/internal/config"
2020
duckdbsync "go.kenn.io/agentsview/internal/duckdb"
21+
"go.kenn.io/agentsview/internal/pathutil"
2122
"go.kenn.io/agentsview/internal/server"
2223
)
2324

@@ -515,7 +516,10 @@ func runDuckDBQuackServe(cfg DuckDBQuackServeConfig) {
515516
fatal("duckdb quack serve: %v", err)
516517
}
517518
if cfg.Path != "" {
518-
duckCfg.Path = cfg.Path
519+
duckCfg.Path, err = pathutil.ExpandHome(cfg.Path)
520+
if err != nil {
521+
fatal("duckdb quack serve: expanding --path: %v", err)
522+
}
519523
}
520524
if cfg.AllowInsecure {
521525
duckCfg.AllowInsecure = true

cmd/agentsview/import.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"go.kenn.io/agentsview/internal/config"
1212
"go.kenn.io/agentsview/internal/db"
1313
"go.kenn.io/agentsview/internal/importer"
14+
"go.kenn.io/agentsview/internal/pathutil"
1415
)
1516

1617
type ImportConfig struct {
@@ -19,6 +20,12 @@ type ImportConfig struct {
1920
}
2021

2122
func runImport(cfg ImportConfig) {
23+
expandedPath, err := pathutil.ExpandHome(cfg.Path)
24+
if err != nil {
25+
log.Fatalf("expanding import path: %v", err)
26+
}
27+
cfg.Path = expandedPath
28+
2229
appCfg, err := config.LoadMinimal()
2330
if err != nil {
2431
log.Fatalf("loading config: %v", err)

cmd/agentsview/recall.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"go.kenn.io/agentsview/internal/config"
1717
"go.kenn.io/agentsview/internal/db"
18+
"go.kenn.io/agentsview/internal/pathutil"
1819
corerecall "go.kenn.io/agentsview/internal/recall"
1920
"go.kenn.io/agentsview/internal/service"
2021
)
@@ -912,7 +913,11 @@ func newRecallImportCommand() *cobra.Command {
912913
return err
913914
}
914915
}
915-
f, err := os.Open(args[0])
916+
path, err := pathutil.ExpandHome(args[0])
917+
if err != nil {
918+
return fmt.Errorf("expanding recall import path: %w", err)
919+
}
920+
f, err := os.Open(path)
916921
if err != nil {
917922
return fmt.Errorf("opening recall import file: %w", err)
918923
}
@@ -1050,7 +1055,7 @@ func addRecallFilterFlags(cmd *cobra.Command, f *service.RecallFilter) {
10501055
flags := cmd.Flags()
10511056
flags.StringVar(&f.Query, "query", "", "Filter by query text")
10521057
flags.StringVar(&f.Project, "project", "", "Filter by project")
1053-
flags.StringVar(&f.CWD, "cwd", "", "Filter by cwd")
1058+
flags.Var(&homePathValue{value: &f.CWD}, "cwd", "Filter by cwd")
10541059
flags.StringVar(&f.GitBranch, "git-branch", "", "Filter by git branch")
10551060
flags.StringVar(&f.Agent, "agent", "", "Filter by agent")
10561061
flags.StringVar(&f.Type, "type", "", "Filter by recall type")
@@ -1101,6 +1106,28 @@ func addRecallFilterFlags(cmd *cobra.Command, f *service.RecallFilter) {
11011106
flags.IntVar(&f.Limit, "limit", 0, "Maximum entries to return")
11021107
}
11031108

1109+
type homePathValue struct {
1110+
value *string
1111+
}
1112+
1113+
func (v *homePathValue) Set(path string) error {
1114+
expanded, err := pathutil.ExpandHome(path)
1115+
if err != nil {
1116+
return err
1117+
}
1118+
*v.value = expanded
1119+
return nil
1120+
}
1121+
1122+
func (v *homePathValue) String() string {
1123+
if v == nil || v.value == nil {
1124+
return ""
1125+
}
1126+
return *v.value
1127+
}
1128+
1129+
func (*homePathValue) Type() string { return "path" }
1130+
11041131
func addRecallEntryCurrentCWDFlag(cmd *cobra.Command, currentCWD *bool) {
11051132
cmd.Flags().BoolVar(
11061133
currentCWD,
@@ -1238,7 +1265,7 @@ func addRecallQueryFlags(cmd *cobra.Command, req *service.RecallQuery) {
12381265
"Retrieval mode: lexical, vector, or hybrid",
12391266
)
12401267
flags.StringVar(&req.Project, "project", "", "Filter by project")
1241-
flags.StringVar(&req.CWD, "cwd", "", "Filter by cwd")
1268+
flags.Var(&homePathValue{value: &req.CWD}, "cwd", "Filter by cwd")
12421269
flags.StringVar(&req.GitBranch, "git-branch", "", "Filter by git branch")
12431270
flags.StringVar(&req.Agent, "agent", "", "Filter by agent")
12441271
flags.StringVar(&req.Type, "type", "", "Filter by recall type")

cmd/agentsview/recall_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"strings"
1515
"testing"
1616

17+
"github.com/spf13/cobra"
1718
"github.com/stretchr/testify/assert"
1819
"github.com/stretchr/testify/require"
1920

@@ -22,6 +23,18 @@ import (
2223
"go.kenn.io/agentsview/internal/service"
2324
)
2425

26+
func TestRecallCWDFlagExpandsHome(t *testing.T) {
27+
home := t.TempDir()
28+
t.Setenv("HOME", home)
29+
t.Setenv("USERPROFILE", home)
30+
var filter service.RecallFilter
31+
cmd := &cobra.Command{Use: "test"}
32+
addRecallFilterFlags(cmd, &filter)
33+
34+
require.NoError(t, cmd.Flags().Parse([]string{"--cwd", "~/work"}))
35+
assert.Equal(t, filepath.Join(home, "work"), filter.CWD)
36+
}
37+
2538
func TestPrintRecallEntryReviewLineDefaultsReviewStateToUnreviewedAuto(
2639
t *testing.T,
2740
) {

cmd/agentsview/session.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/spf13/cobra"
1313
"github.com/spf13/pflag"
1414
"go.kenn.io/agentsview/internal/config"
15+
"go.kenn.io/agentsview/internal/pathutil"
1516
"go.kenn.io/agentsview/internal/service"
1617
"go.kenn.io/agentsview/internal/timeutil"
1718
)
@@ -219,6 +220,10 @@ func explicitServerToken(cmd *cobra.Command) (string, error) {
219220
}
220221
path, err := cmd.Flags().GetString("server-token-file")
221222
if err == nil && strings.TrimSpace(path) != "" {
223+
path, err = pathutil.ExpandHome(path)
224+
if err != nil {
225+
return "", fmt.Errorf("expanding --server-token-file: %w", err)
226+
}
222227
b, err := os.ReadFile(path)
223228
if err != nil {
224229
return "", fmt.Errorf("reading --server-token-file: %w", err)

cmd/agentsview/session_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1518,6 +1518,31 @@ func TestSessionUsage_ServerTokenSendsBearer(t *testing.T) {
15181518
require.NotNil(t, out)
15191519
}
15201520

1521+
func TestSessionUsage_ServerTokenFileExpandsHome(t *testing.T) {
1522+
home := t.TempDir()
1523+
t.Setenv("HOME", home)
1524+
t.Setenv("USERPROFILE", home)
1525+
tokenFile := filepath.Join(home, "remote-token")
1526+
require.NoError(t, os.WriteFile(
1527+
tokenFile, []byte("remote-secret\n"), 0o600,
1528+
))
1529+
1530+
ts, _ := newRemoteUsageServer(t, remoteUsageSpec{
1531+
canonicalID: "remote-session",
1532+
bearer: "remote-secret",
1533+
serverRunning: true,
1534+
})
1535+
1536+
cmd := sessionUsageCommand(t,
1537+
"session", "usage", "remote-session",
1538+
"--server", ts.URL,
1539+
"--server-token-file", "~/remote-token")
1540+
1541+
out, _, err := sessionUsageDataForCommand(cmd, "remote-session")
1542+
require.NoError(t, err)
1543+
require.NotNil(t, out)
1544+
}
1545+
15211546
func TestSessionUsage_ServerHTTPClientHasTimeout(t *testing.T) {
15221547
oldClient := sessionUsageHTTPClient
15231548
sessionUsageHTTPClient = &http.Client{Timeout: 20 * time.Millisecond}

cmd/agentsview/sync_profile.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"runtime/pprof"
1010
"runtime/trace"
1111
"slices"
12+
13+
"go.kenn.io/agentsview/internal/pathutil"
1214
)
1315

1416
// startSyncProfile starts whichever of the hidden --cpuprofile,
@@ -19,6 +21,9 @@ import (
1921
// profiling typo never aborts a real sync.
2022
func startSyncProfile(cfg SyncConfig) func() {
2123
var stoppers []func()
24+
cfg.CPUProfile = expandSyncProfilePath("cpuprofile", cfg.CPUProfile)
25+
cfg.MemProfile = expandSyncProfilePath("memprofile", cfg.MemProfile)
26+
cfg.Trace = expandSyncProfilePath("trace", cfg.Trace)
2227

2328
if cfg.CPUProfile != "" {
2429
f, err := os.Create(cfg.CPUProfile)
@@ -81,3 +86,12 @@ func startSyncProfile(cfg SyncConfig) func() {
8186
}
8287
}
8388
}
89+
90+
func expandSyncProfilePath(name, path string) string {
91+
expanded, err := pathutil.ExpandHome(path)
92+
if err != nil {
93+
log.Printf("%s: expand path: %v", name, err)
94+
return ""
95+
}
96+
return expanded
97+
}

internal/config/config.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/gofrs/flock"
2727
"github.com/spf13/pflag"
2828
"go.kenn.io/agentsview/internal/parser"
29+
"go.kenn.io/agentsview/internal/pathutil"
2930
)
3031

3132
// TerminalConfig holds terminal launch preferences.
@@ -873,6 +874,9 @@ func loadPGServeBase() (Config, error) {
873874
return cfg, err
874875
}
875876
cfg.loadEnv()
877+
if err := expandDataDir(&cfg); err != nil {
878+
return cfg, err
879+
}
876880
if err := cfg.loadFile(); err != nil {
877881
return cfg, fmt.Errorf("loading config file: %w", err)
878882
}
@@ -905,6 +909,9 @@ func LoadMinimal() (Config, error) {
905909
return cfg, err
906910
}
907911
cfg.loadEnv()
912+
if err := expandDataDir(&cfg); err != nil {
913+
return cfg, err
914+
}
908915

909916
if err := cfg.loadFile(); err != nil {
910917
return cfg, fmt.Errorf("loading config file: %w", err)
@@ -928,6 +935,9 @@ func LoadReadOnly() (Config, error) {
928935
return cfg, err
929936
}
930937
cfg.loadEnv()
938+
if err := expandDataDir(&cfg); err != nil {
939+
return cfg, err
940+
}
931941

932942
if err := cfg.loadFileReadOnly(); err != nil {
933943
return cfg, fmt.Errorf("loading config file: %w", err)
@@ -1755,6 +1765,9 @@ func splitFlagList(value string) []string {
17551765

17561766
func finalize(cfg *Config) error {
17571767
var err error
1768+
if err := expandLocalPaths(cfg); err != nil {
1769+
return err
1770+
}
17581771
if strings.TrimSpace(cfg.LocalMachineName) == "" {
17591772
return fmt.Errorf("identify local sync machine: hostname is empty")
17601773
}
@@ -2137,6 +2150,9 @@ func ResolveDataDir() (string, error) {
21372150
if v := dataDirFromEnv(); v != "" {
21382151
cfg.DataDir = v
21392152
}
2153+
if err := expandDataDir(&cfg); err != nil {
2154+
return "", err
2155+
}
21402156
return cfg.DataDir, nil
21412157
}
21422158

@@ -2402,6 +2418,10 @@ func (c *Config) ResolveDuckDB() (DuckDBConfig, error) {
24022418
if err != nil {
24032419
return duck, fmt.Errorf("expanding path: %w", err)
24042420
}
2421+
expanded, err = pathutil.ExpandHome(expanded)
2422+
if err != nil {
2423+
return duck, fmt.Errorf("expanding path: %w", err)
2424+
}
24052425
duck.Path = expanded
24062426
}
24072427
if duck.URL != "" {
@@ -2504,6 +2524,13 @@ func expandBracedEnv(s string) (string, error) {
25042524

25052525
// SaveTerminalConfig persists terminal settings to the config file.
25062526
func (c *Config) SaveTerminalConfig(tc TerminalConfig) error {
2527+
live := tc
2528+
expanded, err := pathutil.ExpandHome(live.CustomBin)
2529+
if err != nil {
2530+
return fmt.Errorf("expanding terminal custom binary: %w", err)
2531+
}
2532+
live.CustomBin = expanded
2533+
25072534
return c.withConfigLock(func() error {
25082535
existing, err := c.readConfigMap()
25092536
if err != nil {
@@ -2514,7 +2541,7 @@ func (c *Config) SaveTerminalConfig(tc TerminalConfig) error {
25142541
if err := c.writeConfigMap(existing); err != nil {
25152542
return err
25162543
}
2517-
c.Terminal = tc
2544+
c.Terminal = live
25182545
return nil
25192546
})
25202547
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package config
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestResolveDuckDB_ExpandsLeadingTildePath(t *testing.T) {
13+
home, err := os.UserHomeDir()
14+
require.NoError(t, err)
15+
16+
cfg := Config{
17+
DuckDB: DuckDBConfig{
18+
Path: "~/mirror.duckdb",
19+
},
20+
}
21+
22+
resolved, err := cfg.ResolveDuckDB()
23+
require.NoError(t, err)
24+
assert.Equal(t, filepath.Join(home, "mirror.duckdb"), resolved.Path)
25+
}
26+
27+
func TestResolveDuckDB_DoesNotExpandMidStringTilde(t *testing.T) {
28+
cfg := Config{
29+
DuckDB: DuckDBConfig{
30+
Path: "path/with~marker.duckdb",
31+
},
32+
}
33+
34+
resolved, err := cfg.ResolveDuckDB()
35+
require.NoError(t, err)
36+
assert.Equal(t, "path/with~marker.duckdb", resolved.Path)
37+
}

0 commit comments

Comments
 (0)