Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 2 additions & 0 deletions cli/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ func runConfigShow(cmd *cobra.Command, _ []string) error {
if state.Sandbox && state.DockerSock != "" {
out.KeyValue("Docker socket", state.DockerSock)
}
out.KeyValue("Persistence backend", state.PersistenceBackend)
out.KeyValue("Memory backend", state.MemoryBackend)
out.KeyValue("JWT secret", maskSecret(state.JWTSecret))

return nil
Expand Down
20 changes: 12 additions & 8 deletions cli/cmd/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,16 @@ func TestConfigShowNotInitialized(t *testing.T) {
func TestConfigShowDisplaysFields(t *testing.T) {
dir := t.TempDir()
state := config.State{
DataDir: dir,
ImageTag: "v1.2.3",
BackendPort: 9000,
WebPort: 4000,
Sandbox: true,
DockerSock: "/var/run/docker.sock",
LogLevel: "debug",
JWTSecret: "super-secret",
DataDir: dir,
ImageTag: "v1.2.3",
BackendPort: 9000,
WebPort: 4000,
Sandbox: true,
DockerSock: "/var/run/docker.sock",
LogLevel: "debug",
JWTSecret: "super-secret",
PersistenceBackend: "sqlite",
MemoryBackend: "mem0",
}

data, err := json.MarshalIndent(state, "", " ")
Expand Down Expand Up @@ -81,6 +83,8 @@ func TestConfigShowDisplaysFields(t *testing.T) {
"debug",
"/var/run/docker.sock",
"****",
"sqlite",
"mem0",
} {
if !bytes.Contains([]byte(out), []byte(want)) {
t.Errorf("expected %q in output, got: %s", want, out)
Expand Down
57 changes: 35 additions & 22 deletions cli/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,25 +81,29 @@ func runInit(cmd *cobra.Command, _ []string) error {

// setupAnswers holds raw form input before validation.
type setupAnswers struct {
dir string
backendPortStr string
webPortStr string
sandbox bool
dockerSock string
logLevel string
genJWT bool
dir string
backendPortStr string
webPortStr string
sandbox bool
dockerSock string
logLevel string
genJWT bool
persistenceBackend string
memoryBackend string
}

func runSetupForm() (setupAnswers, error) {
defaults := config.DefaultState()
a := setupAnswers{
dir: defaults.DataDir,
backendPortStr: fmt.Sprintf("%d", defaults.BackendPort),
webPortStr: fmt.Sprintf("%d", defaults.WebPort),
sandbox: defaults.Sandbox,
dockerSock: defaultDockerSock(),
logLevel: defaults.LogLevel,
genJWT: true,
dir: defaults.DataDir,
backendPortStr: fmt.Sprintf("%d", defaults.BackendPort),
webPortStr: fmt.Sprintf("%d", defaults.WebPort),
sandbox: defaults.Sandbox,
dockerSock: defaultDockerSock(),
logLevel: defaults.LogLevel,
genJWT: true,
persistenceBackend: defaults.PersistenceBackend,
memoryBackend: defaults.MemoryBackend,
}

form := huh.NewForm(
Expand All @@ -126,6 +130,13 @@ func runSetupForm() (setupAnswers, error) {
huh.NewConfirm().Title("Generate JWT secret?").
Description("Recommended for API authentication").Value(&a.genJWT),
),
huh.NewGroup(
huh.NewNote().Title("Backends").
Description(fmt.Sprintf(
"Persistence: %s · Memory: %s\n(More options coming soon)",
a.persistenceBackend, a.memoryBackend,
)),
),
Comment on lines +133 to +139

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Avoid hardcoded backend labels in the Note description.

This text can drift from actual defaults. Build the description from defaults.PersistenceBackend and defaults.MemoryBackend so UI always reflects config defaults.

♻️ Suggested refactor
-		huh.NewGroup(
-			huh.NewNote().Title("Backends").
-				Description("Persistence: SQLite · Memory: Mem0\n(More options coming soon)"),
-		),
+		huh.NewGroup(
+			huh.NewNote().Title("Backends").
+				Description(fmt.Sprintf(
+					"Persistence: %s · Memory: %s\n(More options coming soon)",
+					strings.Title(defaults.PersistenceBackend),
+					strings.Title(defaults.MemoryBackend),
+				)),
+		),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/cmd/init.go` around lines 133 - 136, Replace the hardcoded backend labels
inside the huh.NewNote().Description call with values drawn from
defaults.PersistenceBackend and defaults.MemoryBackend so the UI always reflects
config defaults; update the huh.NewNote().Title("Backends").Description(...)
invocation to build the description string (e.g. using fmt.Sprintf("Persistence:
%s · Memory: %s", defaults.PersistenceBackend, defaults.MemoryBackend)) and add
the defaults import if missing, keeping the same Title and grouping with
huh.NewGroup.

)

if err := form.Run(); err != nil {
Expand Down Expand Up @@ -173,14 +184,16 @@ func buildState(a setupAnswers) (config.State, error) {
}

return config.State{
DataDir: dir,
ImageTag: imageTag,
BackendPort: backendPort,
WebPort: webPort,
Sandbox: a.sandbox,
DockerSock: dockerSock,
LogLevel: a.logLevel,
JWTSecret: jwtSecret,
DataDir: dir,
ImageTag: imageTag,
BackendPort: backendPort,
WebPort: webPort,
Sandbox: a.sandbox,
DockerSock: dockerSock,
LogLevel: a.logLevel,
JWTSecret: jwtSecret,
PersistenceBackend: a.persistenceBackend,
MemoryBackend: a.memoryBackend,
}, nil
}

Expand Down
111 changes: 66 additions & 45 deletions cli/cmd/uninstall.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"runtime"
"strings"

"github.com/Aureliolo/synthorg/cli/internal/completion"
"github.com/Aureliolo/synthorg/cli/internal/config"
"github.com/Aureliolo/synthorg/cli/internal/docker"
"github.com/charmbracelet/huh"
Expand Down Expand Up @@ -59,6 +60,17 @@ func runUninstall(cmd *cobra.Command, _ []string) error {
return err
}

// Remove shell completion snippets for all supported shells
// (user may have installed completions for multiple shells).
_, _ = fmt.Fprintln(out, "Removing shell completions...")
for _, shell := range []completion.ShellType{
completion.Bash, completion.Zsh, completion.Fish, completion.PowerShell,
} {
if err := completion.Uninstall(ctx, shell); err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Warning: could not remove %s completions: %v\n", shell, err)
}
}

// Optionally remove CLI binary.
if err := confirmAndRemoveBinary(cmd); err != nil {
return err
Expand Down Expand Up @@ -113,57 +125,66 @@ func confirmAndRemoveData(cmd *cobra.Command, dataDir string) error {
if err := form.Run(); err != nil {
return err
}
if !removeData {
return nil
}
dir := filepath.Clean(dataDir)
if err := rejectUnsafeDir(dir); err != nil {
return err
}
return removeDataDir(cmd, dir)
}

if removeData {
dir := filepath.Clean(dataDir)
// Safety: refuse to remove root, home, UNC share roots, or drive roots.
home, homeErr := os.UserHomeDir()
if homeErr != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Warning: cannot determine home directory: %v\n", homeErr)
}
isHomeDir := false
if homeErr == nil {
home = filepath.Clean(home)
if runtime.GOOS == "windows" {
isHomeDir = strings.EqualFold(dir, home)
} else {
isHomeDir = dir == home
}
}
vol := filepath.VolumeName(dir)
// Only reject UNC share roots (e.g. \\server\share), not arbitrary
// paths under a UNC share (e.g. \\server\share\synthorg\data).
isUNCRoot := vol != "" &&
(strings.HasPrefix(vol, `\\`) || strings.HasPrefix(vol, "//")) &&
(dir == vol || dir == vol+`\` || dir == vol+"/")
isDriveRoot := len(dir) == 3 && dir[1] == ':' && (dir[2] == '\\' || dir[2] == '/')
if dir == "/" || isHomeDir || isDriveRoot || isUNCRoot {
return fmt.Errorf("refusing to remove %q — does not look like an app data directory", dir)
// rejectUnsafeDir refuses to remove root, home, relative, UNC share roots, or drive roots.
func rejectUnsafeDir(dir string) error {
if dir == "" || dir == "." || !filepath.IsAbs(dir) {
return fmt.Errorf("refusing to remove %q — must be an absolute path", dir)
}
home, homeErr := os.UserHomeDir()
isHomeDir := false
if homeErr == nil {
home = filepath.Clean(home)
if runtime.GOOS == "windows" {
isHomeDir = strings.EqualFold(dir, home)
} else {
isHomeDir = dir == home
}
}
vol := filepath.VolumeName(dir)
// Only reject UNC share roots (e.g. \\server\share), not arbitrary
// paths under a UNC share (e.g. \\server\share\synthorg\data).
isUNCRoot := vol != "" &&
(strings.HasPrefix(vol, `\\`) || strings.HasPrefix(vol, "//")) &&
(dir == vol || dir == vol+`\` || dir == vol+"/")
isDriveRoot := len(dir) == 3 && dir[1] == ':' && (dir[2] == '\\' || dir[2] == '/')
if dir == "/" || isHomeDir || isDriveRoot || isUNCRoot {
return fmt.Errorf("refusing to remove %q — does not look like an app data directory", dir)
}
return nil
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// On Windows the running binary cannot be deleted. If it lives
// inside the config directory, remove everything else and leave
// the binary for deferred cleanup in confirmAndRemoveBinary.
execPath, execErr := os.Executable()
if execErr != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Warning: cannot resolve executable path: %v\n", execErr)
// removeDataDir removes the data directory. On Windows, if the running
// binary lives inside the directory, it removes everything except the binary.
func removeDataDir(cmd *cobra.Command, dir string) error {
execPath, execErr := os.Executable()
if execErr != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Warning: cannot resolve executable path: %v\n", execErr)
}
if execErr == nil {
if resolved, err := filepath.EvalSymlinks(execPath); err == nil {
execPath = resolved
}
if execErr == nil {
if resolved, err := filepath.EvalSymlinks(execPath); err == nil {
execPath = resolved
}
}
if execErr == nil && runtime.GOOS == "windows" && isInsideDir(execPath, dir) {
if err := removeAllExcept(dir, execPath); err != nil {
return fmt.Errorf("removing config directory: %w", err)
}
if execErr == nil && runtime.GOOS == "windows" && isInsideDir(execPath, dir) {
if err := removeAllExcept(dir, execPath); err != nil {
return fmt.Errorf("removing config directory: %w", err)
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Removed contents of %s (binary skipped — still running)\n", dir)
} else {
if err := os.RemoveAll(dir); err != nil {
return fmt.Errorf("removing config directory: %w", err)
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Removed %s\n", dir)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Removed contents of %s (binary skipped — still running)\n", dir)
} else {
if err := os.RemoveAll(dir); err != nil {
Comment thread Dismissed
return fmt.Errorf("removing config directory: %w", err)
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Removed %s\n", dir)
}
return nil
}
Expand Down
Loading
Loading