diff --git a/cli/cmd/config.go b/cli/cmd/config.go index ac694d536d..6f33faaa33 100644 --- a/cli/cmd/config.go +++ b/cli/cmd/config.go @@ -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 diff --git a/cli/cmd/config_test.go b/cli/cmd/config_test.go index 0fe80bddfc..16e75f9740 100644 --- a/cli/cmd/config_test.go +++ b/cli/cmd/config_test.go @@ -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, "", " ") @@ -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) diff --git a/cli/cmd/init.go b/cli/cmd/init.go index 0738f4a706..95550e4fee 100644 --- a/cli/cmd/init.go +++ b/cli/cmd/init.go @@ -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( @@ -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, + )), + ), ) if err := form.Run(); err != nil { @@ -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 } diff --git a/cli/cmd/uninstall.go b/cli/cmd/uninstall.go index 73ba146848..1486dd3ce1 100644 --- a/cli/cmd/uninstall.go +++ b/cli/cmd/uninstall.go @@ -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" @@ -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 @@ -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 +} - // 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 { + return fmt.Errorf("removing config directory: %w", err) } + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Removed %s\n", dir) } return nil } diff --git a/cli/coverage.out b/cli/coverage.out new file mode 100644 index 0000000000..41a1fefb48 --- /dev/null +++ b/cli/coverage.out @@ -0,0 +1,135 @@ +mode: set +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:31.36,32.11 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:33.12,34.16 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:35.11,36.15 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:37.12,38.16 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:39.18,40.22 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:41.10,42.19 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:50.30,52.17 2 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:52.17,54.10 2 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:55.39,56.15 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:57.38,58.14 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:59.39,60.15 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:61.79,62.21 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:65.2,65.31 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:65.31,67.3 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:68.2,68.16 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:79.92,82.55 2 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:82.55,84.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:86.2,86.15 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:87.12,88.26 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:89.11,90.34 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:91.12,92.35 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:93.18,94.37 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:95.10,96.57 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:100.46,102.16 2 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:102.16,104.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:105.2,109.16 4 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:109.16,111.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:112.2,112.15 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:112.15,115.3 2 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:117.2,118.44 2 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:121.69,123.16 2 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:123.16,125.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:128.2,132.45 4 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:132.45,134.3 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:137.2,137.52 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:137.52,139.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:140.2,141.55 2 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:141.55,143.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:144.2,144.67 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:144.67,146.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:149.2,152.51 4 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:152.51,154.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:155.2,155.16 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:155.16,157.54 2 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:157.54,159.4 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:162.2,162.17 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:165.70,167.16 2 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:167.16,169.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:171.2,175.45 4 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:175.45,177.3 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:180.2,180.52 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:180.52,182.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:183.2,184.62 2 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:184.62,186.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:187.2,187.67 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:187.67,189.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:190.2,190.17 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:193.73,195.16 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:195.16,197.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:198.2,201.51 3 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:201.51,203.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:204.2,204.15 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:204.15,207.3 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:209.2,210.44 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:214.65,216.16 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:216.16,218.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:221.2,222.16 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:222.16,224.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:227.2,227.55 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:227.55,231.17 4 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:231.17,232.12 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:234.3,235.31 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:235.31,236.12 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:238.3,239.25 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:239.25,240.12 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:243.3,244.17 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:244.17,247.4 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:248.3,250.52 3 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:250.52,251.12 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:253.3,253.24 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:257.2,257.31 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:257.31,259.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:260.2,260.94 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:265.51,267.16 2 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:267.16,268.37 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:268.37,270.4 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:271.3,271.56 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:273.2,273.49 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:277.60,278.15 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:279.12,280.25 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:281.11,282.24 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:283.12,284.25 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:285.18,286.34 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:287.10,288.52 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:292.28,294.16 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:294.16,296.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:297.2,297.58 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:300.27,302.16 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:302.16,304.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:306.2,307.79 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:307.79,309.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:311.2,311.57 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:314.28,316.16 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:316.16,318.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:319.2,320.79 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:320.79,322.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:323.2,323.12 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:326.53,328.16 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:328.16,330.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:331.2,331.35 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:337.43,339.16 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:339.16,340.37 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:340.37,342.4 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:343.3,343.49 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:345.2,346.40 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:346.40,348.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:350.2,353.29 4 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:353.29,354.40 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:354.40,356.12 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:358.3,358.14 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:358.14,360.37 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:360.37,361.13 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:364.4,365.12 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:367.3,367.32 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:370.2,371.51 2 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:375.47,377.48 2 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:377.48,379.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:380.2,381.16 2 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:381.16,383.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:384.2,386.21 3 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:386.21,388.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:389.2,389.21 1 1 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:389.21,391.3 1 0 +github.com/Aureliolo/synthorg/cli/internal/completion/install.go:392.2,392.12 1 1 diff --git a/cli/internal/completion/install.go b/cli/internal/completion/install.go index 876d293d05..88e2bd26fb 100644 --- a/cli/internal/completion/install.go +++ b/cli/internal/completion/install.go @@ -273,6 +273,124 @@ func fileContains(path, sub string) (bool, error) { return strings.Contains(string(data), sub), nil } +// Uninstall removes shell completion snippets and generated files. +func Uninstall(ctx context.Context, shell ShellType) error { + switch shell { + case Bash: + return uninstallBash() + case Zsh: + return uninstallZsh() + case Fish: + return uninstallFish() + case PowerShell: + return uninstallPowerShell(ctx) + default: + return fmt.Errorf("unsupported shell: %s", shell) + } +} + +func uninstallBash() error { + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("cannot determine home directory: %w", err) + } + return removeMarkerBlock(filepath.Join(home, ".bashrc")) +} + +func uninstallZsh() error { + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("cannot determine home directory: %w", err) + } + // Remove completion function file. + compFile := filepath.Join(home, ".zsh", "completion", "_synthorg") + if err := os.Remove(compFile); err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("removing completion file: %w", err) + } + // Remove fpath snippet from .zshrc. + return removeMarkerBlock(filepath.Join(home, ".zshrc")) +} + +func uninstallFish() error { + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("cannot determine home directory: %w", err) + } + compFile := filepath.Join(home, ".config", "fish", "completions", "synthorg.fish") + if err := os.Remove(compFile); err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("removing completion file: %w", err) + } + return nil +} + +func uninstallPowerShell(ctx context.Context) error { + profile, err := powershellProfilePath(ctx) + if err != nil { + return err + } + return removeMarkerBlock(profile) +} + +// maxSnippetLines caps how many non-empty lines after the marker are +// treated as part of the snippet, preventing unbounded deletion if a +// user's content follows without a blank-line separator. +const maxSnippetLines = 5 + +// removeMarkerBlock removes the first marker block from a shell profile. +// A block starts at the marker line and includes up to maxSnippetLines +// contiguous non-empty lines after it, plus the terminating empty line. +// Only the first occurrence is removed to avoid greedy deletion. +// The original file permissions are preserved. +// If the file does not exist or has no marker, this is a no-op. +func removeMarkerBlock(path string) error { + info, err := os.Stat(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil + } + return fmt.Errorf("stat %s: %w", path, err) + } + data, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("reading %s: %w", path, err) + } + content := string(data) + if !strings.Contains(content, marker) { + return nil + } + + var result []string + lines := strings.Split(content, "\n") + inBlock := false + found := false + blockLines := 0 + for _, line := range lines { + if !found && strings.TrimSpace(line) == marker { + inBlock = true + found = true + blockLines = 0 + continue + } + if inBlock { + if strings.TrimSpace(line) != "" && blockLines < maxSnippetLines { + blockLines++ + continue + } + // Empty line or cap reached — end the block. + inBlock = false + if strings.TrimSpace(line) == "" { + // Consume the terminating empty line. + continue + } + // Cap reached on a non-empty line — keep it. + } + result = append(result, line) + } + + cleaned := strings.Join(result, "\n") + return os.WriteFile(path, []byte(cleaned), info.Mode()) +} + // appendToFile appends content to a file, creating it if needed. func appendToFile(path, content string) error { dir := filepath.Dir(path) diff --git a/cli/internal/completion/install_test.go b/cli/internal/completion/install_test.go index 99e2376662..bc24935b31 100644 --- a/cli/internal/completion/install_test.go +++ b/cli/internal/completion/install_test.go @@ -201,3 +201,207 @@ func TestInstallUnknownShell(t *testing.T) { t.Error("expected error for unknown shell") } } + +func TestRemoveMarkerBlock(t *testing.T) { + tests := []struct { + name string + input string + want string + missing bool // file does not exist + }{ + { + name: "removes bash snippet", + input: "export PATH=/usr/bin\n\n# synthorg shell completion\neval \"$(synthorg completion bash)\"\n\nexport FOO=bar\n", + want: "export PATH=/usr/bin\n\nexport FOO=bar\n", + }, + { + name: "removes zsh multi-line snippet", + input: "# zsh config\n\n# synthorg shell completion\nfpath=(~/.zsh/completion $fpath)\nautoload -Uz compinit && compinit\n\n# end\n", + want: "# zsh config\n\n# end\n", + }, + { + name: "marker at end of file with content", + input: "line1\n# synthorg shell completion\nsome content\n", + want: "line1", + }, + { + name: "no marker — unchanged", + input: "export PATH=/usr/bin\n", + want: "export PATH=/usr/bin\n", + }, + { + name: "file does not exist — no-op", + missing: true, + }, + { + name: "only removes first marker", + input: "# synthorg shell completion\nfirst\n\n# synthorg shell completion\nsecond\n\nend\n", + want: "# synthorg shell completion\nsecond\n\nend\n", + }, + { + name: "preserves file permissions", + input: "# synthorg shell completion\nsnippet\n\n", + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "profile") + + if tt.missing { + err := removeMarkerBlock(path) + if err != nil { + t.Fatalf("removeMarkerBlock on missing file: %v", err) + } + return + } + + if err := os.WriteFile(path, []byte(tt.input), 0o644); err != nil { + t.Fatal(err) + } + + if err := removeMarkerBlock(path); err != nil { + t.Fatalf("removeMarkerBlock: %v", err) + } + + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if got := string(data); got != tt.want { + t.Errorf("content mismatch\ngot: %q\nwant: %q", got, tt.want) + } + + }) + } + + // Unix-only: verify permissions are preserved. + if runtime.GOOS != "windows" { + t.Run("preserves_permissions_unix", func(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "profile") + content := "before\n# synthorg shell completion\nsnippet\n\nafter\n" + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + t.Fatal(err) + } + if err := removeMarkerBlock(path); err != nil { + t.Fatalf("removeMarkerBlock: %v", err) + } + info, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if got := info.Mode().Perm(); got != 0o600 { + t.Errorf("permissions = %o, want 600", got) + } + }) + } +} + +func TestUninstallBashRoundTrip(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + + root := testRootCmd() + + // Install. + if _, err := Install(context.Background(), root, Bash); err != nil { + t.Fatalf("install: %v", err) + } + + // Verify marker is present. + bashrc := filepath.Join(home, ".bashrc") + data, err := os.ReadFile(bashrc) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(data), marker) { + t.Fatal("marker not found after install") + } + + // Uninstall. + if err := Uninstall(context.Background(), Bash); err != nil { + t.Fatalf("uninstall: %v", err) + } + + // Verify marker is gone. + data, err = os.ReadFile(bashrc) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(data), marker) { + t.Error("marker still present after uninstall") + } +} + +func TestUninstallZshRoundTrip(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + + root := testRootCmd() + + // Install. + if _, err := Install(context.Background(), root, Zsh); err != nil { + t.Fatalf("install: %v", err) + } + + compFile := filepath.Join(home, ".zsh", "completion", "_synthorg") + if _, err := os.Stat(compFile); err != nil { + t.Fatalf("completion file missing after install: %v", err) + } + + // Uninstall. + if err := Uninstall(context.Background(), Zsh); err != nil { + t.Fatalf("uninstall: %v", err) + } + + // Completion file should be gone. + if _, err := os.Stat(compFile); !os.IsNotExist(err) { + t.Error("completion file still exists after uninstall") + } + + // Marker should be gone from .zshrc. + data, err := os.ReadFile(filepath.Join(home, ".zshrc")) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(data), marker) { + t.Error("marker still present in .zshrc after uninstall") + } +} + +func TestUninstallFishRoundTrip(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + + root := testRootCmd() + + if _, err := Install(context.Background(), root, Fish); err != nil { + t.Fatalf("install: %v", err) + } + + compFile := filepath.Join(home, ".config", "fish", "completions", "synthorg.fish") + if _, err := os.Stat(compFile); err != nil { + t.Fatalf("completion file missing after install: %v", err) + } + + if err := Uninstall(context.Background(), Fish); err != nil { + t.Fatalf("uninstall: %v", err) + } + + if _, err := os.Stat(compFile); !os.IsNotExist(err) { + t.Error("completion file still exists after uninstall") + } +} + +func TestUninstallUnknownShell(t *testing.T) { + err := Uninstall(context.Background(), Unknown) + if err == nil { + t.Error("expected error for unknown shell") + } +} diff --git a/cli/internal/compose/compose.yml.tmpl b/cli/internal/compose/compose.yml.tmpl index b2a2b9bb26..297e8722b6 100644 --- a/cli/internal/compose/compose.yml.tmpl +++ b/cli/internal/compose/compose.yml.tmpl @@ -13,6 +13,8 @@ services: SYNTHORG_PORT: "8000" SYNTHORG_DB_PATH: "/data/synthorg.db" SYNTHORG_MEMORY_DIR: "/data/memory" + SYNTHORG_PERSISTENCE_BACKEND: {{yamlStr .PersistenceBackend}} + SYNTHORG_MEMORY_BACKEND: {{yamlStr .MemoryBackend}} SYNTHORG_LOG_LEVEL: {{yamlStr .LogLevel}} {{- if .JWTSecret}} SYNTHORG_JWT_SECRET: {{yamlStr .JWTSecret}} diff --git a/cli/internal/compose/generate.go b/cli/internal/compose/generate.go index 914587fef2..3694bcab31 100644 --- a/cli/internal/compose/generate.go +++ b/cli/internal/compose/generate.go @@ -29,27 +29,31 @@ var allowedLogLevels = map[string]bool{ // Params are the template parameters for compose generation. type Params struct { - CLIVersion string - ImageTag string - BackendPort int - WebPort int - LogLevel string - JWTSecret string - Sandbox bool - DockerSock string + CLIVersion string + ImageTag string + BackendPort int + WebPort int + LogLevel string + JWTSecret string + Sandbox bool + DockerSock string + PersistenceBackend string + MemoryBackend string } // ParamsFromState creates Params from a persisted State. func ParamsFromState(s config.State) Params { return Params{ - CLIVersion: version.Version, - ImageTag: s.ImageTag, - BackendPort: s.BackendPort, - WebPort: s.WebPort, - LogLevel: s.LogLevel, - JWTSecret: s.JWTSecret, - Sandbox: s.Sandbox, - DockerSock: s.DockerSock, + CLIVersion: version.Version, + ImageTag: s.ImageTag, + BackendPort: s.BackendPort, + WebPort: s.WebPort, + LogLevel: s.LogLevel, + JWTSecret: s.JWTSecret, + Sandbox: s.Sandbox, + DockerSock: s.DockerSock, + PersistenceBackend: s.PersistenceBackend, + MemoryBackend: s.MemoryBackend, } } @@ -100,6 +104,12 @@ func validateParams(p Params) error { return fmt.Errorf("docker socket path %q contains unsafe characters", p.DockerSock) } } + if !config.IsValidPersistenceBackend(p.PersistenceBackend) { + return fmt.Errorf("invalid persistence backend %q: must be one of %s", p.PersistenceBackend, config.PersistenceBackendNames()) + } + if !config.IsValidMemoryBackend(p.MemoryBackend) { + return fmt.Errorf("invalid memory backend %q: must be one of %s", p.MemoryBackend, config.MemoryBackendNames()) + } return nil } diff --git a/cli/internal/compose/generate_test.go b/cli/internal/compose/generate_test.go index acea3a7298..b17039f42f 100644 --- a/cli/internal/compose/generate_test.go +++ b/cli/internal/compose/generate_test.go @@ -11,11 +11,13 @@ import ( func TestGenerateDefault(t *testing.T) { p := Params{ - CLIVersion: "dev", - ImageTag: "latest", - BackendPort: 8000, - WebPort: 3000, - LogLevel: "info", + CLIVersion: "dev", + ImageTag: "latest", + BackendPort: 8000, + WebPort: 3000, + LogLevel: "info", + PersistenceBackend: "sqlite", + MemoryBackend: "mem0", } out, err := Generate(p) if err != nil { @@ -50,12 +52,14 @@ func TestGenerateDefault(t *testing.T) { func TestGenerateCustomPorts(t *testing.T) { p := Params{ - CLIVersion: "dev", - ImageTag: "v0.2.0", - BackendPort: 9000, - WebPort: 4000, - LogLevel: "debug", - JWTSecret: "test-secret-value", + CLIVersion: "dev", + ImageTag: "v0.2.0", + BackendPort: 9000, + WebPort: 4000, + LogLevel: "debug", + JWTSecret: "test-secret-value", + PersistenceBackend: "sqlite", + MemoryBackend: "mem0", } out, err := Generate(p) if err != nil { @@ -74,13 +78,15 @@ func TestGenerateCustomPorts(t *testing.T) { func TestGenerateWithSandbox(t *testing.T) { p := Params{ - CLIVersion: "dev", - ImageTag: "latest", - BackendPort: 8000, - WebPort: 3000, - LogLevel: "info", - Sandbox: true, - DockerSock: "/var/run/docker.sock", + CLIVersion: "dev", + ImageTag: "latest", + BackendPort: 8000, + WebPort: 3000, + LogLevel: "info", + Sandbox: true, + DockerSock: "/var/run/docker.sock", + PersistenceBackend: "sqlite", + MemoryBackend: "mem0", } out, err := Generate(p) if err != nil { @@ -95,11 +101,13 @@ func TestGenerateWithSandbox(t *testing.T) { func TestGenerateHardeningPresent(t *testing.T) { p := Params{ - CLIVersion: "dev", - ImageTag: "latest", - BackendPort: 8000, - WebPort: 3000, - LogLevel: "info", + CLIVersion: "dev", + ImageTag: "latest", + BackendPort: 8000, + WebPort: 3000, + LogLevel: "info", + PersistenceBackend: "sqlite", + MemoryBackend: "mem0", } out, err := Generate(p) if err != nil { @@ -123,14 +131,16 @@ func TestGenerateHardeningPresent(t *testing.T) { func TestParamsFromState(t *testing.T) { s := config.State{ - DataDir: "/tmp/test", - ImageTag: "v1.0.0", - BackendPort: 9000, - WebPort: 4000, - LogLevel: "debug", - JWTSecret: "secret", - Sandbox: true, - DockerSock: "/var/run/docker.sock", + DataDir: "/tmp/test", + ImageTag: "v1.0.0", + BackendPort: 9000, + WebPort: 4000, + LogLevel: "debug", + JWTSecret: "secret", + Sandbox: true, + DockerSock: "/var/run/docker.sock", + PersistenceBackend: "sqlite", + MemoryBackend: "mem0", } p := ParamsFromState(s) @@ -149,6 +159,12 @@ func TestParamsFromState(t *testing.T) { if p.DockerSock != "/var/run/docker.sock" { t.Errorf("DockerSock = %q", p.DockerSock) } + if p.PersistenceBackend != "sqlite" { + t.Errorf("PersistenceBackend = %q, want sqlite", p.PersistenceBackend) + } + if p.MemoryBackend != "mem0" { + t.Errorf("MemoryBackend = %q, want mem0", p.MemoryBackend) + } } func assertContains(t *testing.T, s, substr string) { diff --git a/cli/internal/config/state.go b/cli/internal/config/state.go index 5622603b3a..f5770451dc 100644 --- a/cli/internal/config/state.go +++ b/cli/internal/config/state.go @@ -6,20 +6,24 @@ import ( "fmt" "os" "path/filepath" + "sort" + "strings" ) const stateFileName = "config.json" // State is the persisted CLI configuration written by `synthorg init`. type State struct { - DataDir string `json:"data_dir"` - ImageTag string `json:"image_tag"` - BackendPort int `json:"backend_port"` - WebPort int `json:"web_port"` - Sandbox bool `json:"sandbox"` - DockerSock string `json:"docker_sock,omitempty"` - LogLevel string `json:"log_level"` - JWTSecret string `json:"jwt_secret,omitempty"` + DataDir string `json:"data_dir"` + ImageTag string `json:"image_tag"` + BackendPort int `json:"backend_port"` + WebPort int `json:"web_port"` + Sandbox bool `json:"sandbox"` + DockerSock string `json:"docker_sock,omitempty"` + LogLevel string `json:"log_level"` + JWTSecret string `json:"jwt_secret,omitempty"` + PersistenceBackend string `json:"persistence_backend"` + MemoryBackend string `json:"memory_backend"` } // DefaultState returns a State with sensible defaults for the interactive init @@ -27,12 +31,14 @@ type State struct { // when no config file exists. func DefaultState() State { return State{ - DataDir: DataDir(), - ImageTag: "latest", - BackendPort: 8000, - WebPort: 3000, - Sandbox: true, - LogLevel: "info", + DataDir: DataDir(), + ImageTag: "latest", + BackendPort: 8000, + WebPort: 3000, + Sandbox: true, + LogLevel: "info", + PersistenceBackend: "sqlite", + MemoryBackend: "mem0", } } @@ -83,6 +89,35 @@ func Load(dataDir string) (State, error) { return s, nil } +var validPersistenceBackends = map[string]bool{"sqlite": true} +var validMemoryBackends = map[string]bool{"mem0": true} + +// sortedKeys returns a comma-separated sorted list of map keys. +func sortedKeys(m map[string]bool) string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return strings.Join(keys, ", ") +} + +// IsValidPersistenceBackend reports whether name is a known persistence backend. +func IsValidPersistenceBackend(name string) bool { + return validPersistenceBackends[name] +} + +// IsValidMemoryBackend reports whether name is a known memory backend. +func IsValidMemoryBackend(name string) bool { + return validMemoryBackends[name] +} + +// PersistenceBackendNames returns the allowed persistence backend names. +func PersistenceBackendNames() string { return sortedKeys(validPersistenceBackends) } + +// MemoryBackendNames returns the allowed memory backend names. +func MemoryBackendNames() string { return sortedKeys(validMemoryBackends) } + // validate checks that loaded config values are within safe ranges. func (s State) validate() error { if s.BackendPort < 1 || s.BackendPort > 65535 { @@ -91,6 +126,12 @@ func (s State) validate() error { if s.WebPort < 1 || s.WebPort > 65535 { return fmt.Errorf("invalid web_port %d: must be 1-65535", s.WebPort) } + if !IsValidPersistenceBackend(s.PersistenceBackend) { + return fmt.Errorf("invalid persistence_backend %q: must be one of %s", s.PersistenceBackend, sortedKeys(validPersistenceBackends)) + } + if !IsValidMemoryBackend(s.MemoryBackend) { + return fmt.Errorf("invalid memory_backend %q: must be one of %s", s.MemoryBackend, sortedKeys(validMemoryBackends)) + } return nil } diff --git a/cli/internal/config/state_test.go b/cli/internal/config/state_test.go index 7091aea96c..ccdff6e181 100644 --- a/cli/internal/config/state_test.go +++ b/cli/internal/config/state_test.go @@ -28,17 +28,25 @@ func TestDefaultState(t *testing.T) { if s.DataDir == "" { t.Error("DataDir should not be empty") } + if s.PersistenceBackend != "sqlite" { + t.Errorf("PersistenceBackend = %q, want sqlite", s.PersistenceBackend) + } + if s.MemoryBackend != "mem0" { + t.Errorf("MemoryBackend = %q, want mem0", s.MemoryBackend) + } } func TestSaveAndLoad(t *testing.T) { tmp := t.TempDir() s := State{ - DataDir: tmp, - ImageTag: "v0.1.5", - BackendPort: 9000, - WebPort: 3001, - LogLevel: "debug", - JWTSecret: "test-secret", + DataDir: tmp, + ImageTag: "v0.1.5", + BackendPort: 9000, + WebPort: 3001, + LogLevel: "debug", + JWTSecret: "test-secret", + PersistenceBackend: "sqlite", + MemoryBackend: "mem0", } if err := Save(s); err != nil { @@ -65,6 +73,12 @@ func TestSaveAndLoad(t *testing.T) { if loaded.LogLevel != s.LogLevel { t.Errorf("LogLevel = %q, want %q", loaded.LogLevel, s.LogLevel) } + if loaded.PersistenceBackend != s.PersistenceBackend { + t.Errorf("PersistenceBackend = %q, want %q", loaded.PersistenceBackend, s.PersistenceBackend) + } + if loaded.MemoryBackend != s.MemoryBackend { + t.Errorf("MemoryBackend = %q, want %q", loaded.MemoryBackend, s.MemoryBackend) + } } func TestSaveCreatesDirectory(t *testing.T) { @@ -148,6 +162,41 @@ func TestLoadInvalid(t *testing.T) { } } +func TestLoadRejectsInvalidBackends(t *testing.T) { + tests := []struct { + name string + persist string + memory string + }{ + {"empty persistence", "", "mem0"}, + {"empty memory", "sqlite", ""}, + {"unknown persistence", "postgres", "mem0"}, + {"unknown memory", "sqlite", "redis"}, + {"both empty", "", ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmp := t.TempDir() + raw, _ := json.Marshal(map[string]any{ + "data_dir": tmp, + "image_tag": "latest", + "backend_port": 8000, + "web_port": 3000, + "log_level": "info", + "persistence_backend": tt.persist, + "memory_backend": tt.memory, + }) + if err := os.WriteFile(filepath.Join(tmp, stateFileName), raw, 0o600); err != nil { + t.Fatal(err) + } + _, err := Load(tmp) + if err == nil { + t.Errorf("expected validation error for persist=%q memory=%q", tt.persist, tt.memory) + } + }) + } +} + func TestStatePath(t *testing.T) { path := StatePath("/some/dir") if filepath.Base(path) != stateFileName { @@ -158,14 +207,16 @@ func TestStatePath(t *testing.T) { func TestSaveLoadRoundTrip(t *testing.T) { tmp := t.TempDir() original := State{ - DataDir: tmp, - ImageTag: "v2.0.0", - BackendPort: 8080, - WebPort: 3030, - Sandbox: true, - DockerSock: "/custom/docker.sock", - LogLevel: "warn", - JWTSecret: "super-secret-key", + DataDir: tmp, + ImageTag: "v2.0.0", + BackendPort: 8080, + WebPort: 3030, + Sandbox: true, + DockerSock: "/custom/docker.sock", + LogLevel: "warn", + JWTSecret: "super-secret-key", + PersistenceBackend: "sqlite", + MemoryBackend: "mem0", } if err := Save(original); err != nil { diff --git a/cli/testdata/compose_custom_ports.yml b/cli/testdata/compose_custom_ports.yml index 7be1dc49cb..c35a04eb4a 100644 --- a/cli/testdata/compose_custom_ports.yml +++ b/cli/testdata/compose_custom_ports.yml @@ -13,6 +13,8 @@ services: SYNTHORG_PORT: "8000" SYNTHORG_DB_PATH: "/data/synthorg.db" SYNTHORG_MEMORY_DIR: "/data/memory" + SYNTHORG_PERSISTENCE_BACKEND: "sqlite" + SYNTHORG_MEMORY_BACKEND: "mem0" SYNTHORG_LOG_LEVEL: "debug" SYNTHORG_JWT_SECRET: "test-secret-value" user: "65532:65532" diff --git a/cli/testdata/compose_default.yml b/cli/testdata/compose_default.yml index 76a5b01660..2cd2844951 100644 --- a/cli/testdata/compose_default.yml +++ b/cli/testdata/compose_default.yml @@ -13,6 +13,8 @@ services: SYNTHORG_PORT: "8000" SYNTHORG_DB_PATH: "/data/synthorg.db" SYNTHORG_MEMORY_DIR: "/data/memory" + SYNTHORG_PERSISTENCE_BACKEND: "sqlite" + SYNTHORG_MEMORY_BACKEND: "mem0" SYNTHORG_LOG_LEVEL: "info" user: "65532:65532" # CIS Docker Benchmark v1.6.0 hardening (5.3, 5.12, 5.25, 5.28) diff --git a/docker/.env.example b/docker/.env.example index 7ce8d5fc88..e5fa6a1db6 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -20,6 +20,12 @@ SYNTHORG_DB_PATH=/data/synthorg.db # Agent memory storage directory (inside container: /data/memory) SYNTHORG_MEMORY_DIR=/data/memory +# Persistence backend (currently: sqlite) +SYNTHORG_PERSISTENCE_BACKEND=sqlite + +# Memory backend (currently: mem0) +SYNTHORG_MEMORY_BACKEND=mem0 + # --- Container Networking ---------------------------------------------------- # Host port for the backend API BACKEND_PORT=8000 diff --git a/docs/user_guide.md b/docs/user_guide.md index 04a3857b2b..142c8a9e17 100644 --- a/docs/user_guide.md +++ b/docs/user_guide.md @@ -51,6 +51,8 @@ Configuration is in `docker/.env` (copy from `docker/.env.example`): | `SYNTHORG_JWT_SECRET` | *(auto-generated)* | JWT signing secret. Auto-generated and persisted on first run. Set explicitly only for multi-instance deployments. Must be >= 32 characters if set. | | `SYNTHORG_DB_PATH` | `/data/synthorg.db` | SQLite database path (inside container). | | `SYNTHORG_MEMORY_DIR` | `/data/memory` | Agent memory storage directory (inside container). | +| `SYNTHORG_PERSISTENCE_BACKEND` | `sqlite` | Persistence backend for operational data. | +| `SYNTHORG_MEMORY_BACKEND` | `mem0` | Memory backend for agent memory. | | `BACKEND_PORT` | `8000` | Host port for the backend API. | | `WEB_PORT` | `3000` | Host port for the web dashboard. | | `DOCKER_HOST` | *(unset)* | Docker socket for agent code execution sandbox (optional). |