diff --git a/TODOS.md b/TODOS.md index 19d705b..2dfc257 100644 --- a/TODOS.md +++ b/TODOS.md @@ -12,9 +12,7 @@ 6. Add tests for untested commands. -7. Add uninstall command. - -8. Add purge command? +7. Add purge command? ## Double-check AI output diff --git a/cli.odin b/cli.odin index 83256d2..86458e4 100644 --- a/cli.odin +++ b/cli.odin @@ -143,6 +143,13 @@ key somewhere, otherwise your data could be lost forever.`, flags = {.Help}, args = {{name = "shell", ntype = "string", completion = "shells"}}, }, + { + name = "uninstall", + usage = "envr uninstall", + short = "Remove envr configuration and database", + long = "This will remove all files generated by envr from your system.", + flags = GLOBAL_FLAGS + {.Force}, + }, } // Caller is responsible for calling delete_command(cmd). diff --git a/cmd_uninstall.odin b/cmd_uninstall.odin new file mode 100644 index 0000000..a801e63 --- /dev/null +++ b/cmd_uninstall.odin @@ -0,0 +1,64 @@ +package main + +import "core:bufio" +import "core:fmt" +import "core:os" +import "core:path/filepath" +import "core:strings" +import "core:terminal" + +cmd_uninstall :: proc(cmd: ^Command) { + envr_d := envr_dir(cmd.flags.config_file) + + if !os.exists(envr_d) { + fmt.wprintln(cmd.out, "Nothing to remove.", flush = false) + return + } + + entries, err := os.read_all_directory_by_path(envr_d, context.temp_allocator) + if err != nil { + fmt.wprintf(cmd.err, "Error reading %s: %v\n", envr_d, err, flush = false) + return + } + + paths := make([]string, len(entries), context.temp_allocator) + for entry, i in entries { + paths[i], _ = filepath.join([]string{envr_d, entry.name}, context.temp_allocator) + } + + fmt.wprintln(cmd.out, "This will remove:", flush = false) + for path in paths { + fmt.wprintf(cmd.out, " %s\n", path, flush = false) + } + fmt.wprintf(cmd.out, " %s\n", envr_d, flush = false) + + if !cmd.flags.force { + if !terminal.is_terminal(os.stdin) { + fmt.wprintln(cmd.err, "Run with --force to skip confirmation.", flush = false) + return + } + + fmt.wprintf(cmd.out, "\nAre you sure? [y/N] ", flush = false) + bufio.writer_flush(cmd.out_buf) + + buf: [128]u8 + n, _ := os.read(os.stdin, buf[:]) + response := strings.trim_space(string(buf[:n])) + + if response != "y" && response != "Y" { + fmt.wprintln(cmd.out, "Aborted.", flush = false) + return + } + } + + remove_err := os.remove_all(envr_d) + if remove_err != nil { + fmt.wprintf(cmd.err, "Error removing %s: %v\n", envr_d, remove_err, flush = false) + return + } + + for path in paths { + fmt.wprintf(cmd.out, "Removed %s\n", path, flush = false) + } + fmt.wprintf(cmd.out, "Removed %s\n", envr_d, flush = false) +} diff --git a/cmd_uninstall_test.odin b/cmd_uninstall_test.odin new file mode 100644 index 0000000..7e0b5b8 --- /dev/null +++ b/cmd_uninstall_test.odin @@ -0,0 +1,97 @@ +#+test +package main + +import "core:bufio" +import "core:os" +import "core:path/filepath" +import "core:strings" +import "core:testing" + +@(test) +test_cmd_uninstall_force :: proc(t: ^testing.T) { + base := test_temp_dir(t, "envr-test-uninstall-*") + + cfg_path, _ := filepath.join([]string{base, ".envr", "config.json"}, context.temp_allocator) + cfg := new_config([]string{"fixtures/keys/insecure-test-key"}, cfg_path) + testing.expect(t, save_config(cfg, force = true), "save should succeed") + delete_config(&cfg) + + db, db_ok := db_open(cfg_path) + testing.expect(t, db_ok, "db should open") + if !db_ok do return + f := make_test_env_file("/project/.env", "abc123", "SECRET=value") + defer delete(f.remotes) + db_insert(&db, f) + db_close(&db) + + envr_d := filepath.dir(cfg_path) + testing.expect(t, os.exists(envr_d), ".envr directory should exist before uninstall") + + out_b: strings.Builder + strings.builder_init(&out_b) + defer strings.builder_destroy(&out_b) + err_b: strings.Builder + strings.builder_init(&err_b) + defer strings.builder_destroy(&err_b) + + cmd, ok := parse_args( + []string{"envr", "uninstall", "--force", "--config-file", cfg_path}, + strings.to_stream(&out_b), + strings.to_stream(&err_b), + ) + testing.expect(t, ok, "parse_args should succeed") + if !ok do return + defer delete_command(&cmd) + + cmd_uninstall(&cmd) + bufio.writer_flush(cmd.out_buf) + + testing.expect(t, !os.exists(envr_d), ".envr directory should be removed") + + output := strings.to_string(out_b) + testing.expect(t, strings.contains(output, "Removed"), "output should list removed files") + testing.expect( + t, + strings.contains(output, "config.json"), + "output should mention config.json", + ) + testing.expect( + t, + strings.contains(output, "data.envr"), + "output should mention data.envr", + ) +} + +@(test) +test_cmd_uninstall_nothing_to_remove :: proc(t: ^testing.T) { + base := test_temp_dir(t, "envr-test-uninstall-empty-*") + defer os.remove_all(base) + + cfg_path, _ := filepath.join([]string{base, ".envr", "config.json"}, context.temp_allocator) + + out_b: strings.Builder + strings.builder_init(&out_b) + defer strings.builder_destroy(&out_b) + err_b: strings.Builder + strings.builder_init(&err_b) + defer strings.builder_destroy(&err_b) + + cmd, ok := parse_args( + []string{"envr", "uninstall", "--force", "--config-file", cfg_path}, + strings.to_stream(&out_b), + strings.to_stream(&err_b), + ) + testing.expect(t, ok, "parse_args should succeed") + if !ok do return + defer delete_command(&cmd) + + cmd_uninstall(&cmd) + bufio.writer_flush(cmd.out_buf) + + output := strings.to_string(out_b) + testing.expect( + t, + strings.contains(output, "Nothing to remove"), + "output should say nothing to remove", + ) +} diff --git a/main.odin b/main.odin index f2fdd2e..3c61915 100644 --- a/main.odin +++ b/main.odin @@ -79,6 +79,8 @@ main :: proc() { cmd_sync(&cmd) case "completion": cmd_completion(&cmd) + case "uninstall": + cmd_uninstall(&cmd) case: fmt.wprintf(cmd.err, "Unknown command: %s\n", cmd.name) write_usage(cmd.out)