package cmd import ( "bufio" "fmt" "os" "strconv" "strings" "github.com/kb-search/kb/internal/api" "github.com/kb-search/kb/internal/output" "github.com/spf13/cobra" ) var bulkRemoveCmd = &cobra.Command{ Use: "bulk-remove", Short: "Delete multiple documents matching a filter", RunE: runBulkRemove, } func init() { addBulkFilterFlags(bulkRemoveCmd) rootCmd.AddCommand(bulkRemoveCmd) } func runBulkRemove(cmd *cobra.Command, args []string) error { body, err := buildBulkBody(cmd) if err != nil { return err } yes, _ := cmd.Flags().GetBool("yes") if !yes { desc := describeBulkFilter(cmd) fmt.Printf("This will delete documents matching: %s\nProceed? [y/N] ", desc) reader := bufio.NewReader(os.Stdin) answer, _ := reader.ReadString('\n') answer = strings.TrimSpace(strings.ToLower(answer)) if answer != "y" && answer != "yes" { fmt.Println("Cancelled.") return nil } } client := api.NewClient() resp, err := client.Post("/api/v1/bulk/delete", body) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if err := api.CheckError(resp); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } var result map[string]interface{} if err := api.DecodeJSON(resp, &result); err != nil { return fmt.Errorf("failed to decode response: %w", err) } if output.IsJSON() { output.PrintJSON(result) } else { printBulkResult("Deleted", result) } return nil } // --------------------------------------------------------------------------- // Shared helpers for all bulk commands // --------------------------------------------------------------------------- func addBulkFilterFlags(cmd *cobra.Command) { cmd.Flags().String("tags", "", "filter by tags (comma-separated)") cmd.Flags().String("type", "", "filter by document type") cmd.Flags().String("ids", "", "filter by document IDs (comma-separated)") cmd.Flags().Int("from-id", 0, "filter by id >= value") cmd.Flags().Int("to-id", 0, "filter by id <= value") cmd.Flags().BoolP("force", "f", false, "override safety threshold") cmd.Flags().BoolP("yes", "y", false, "skip confirmation prompt") } func buildBulkBody(cmd *cobra.Command) (map[string]interface{}, error) { body := map[string]interface{}{} tagsStr, _ := cmd.Flags().GetString("tags") if tagsStr != "" { body["tags"] = splitTags(tagsStr) } docType, _ := cmd.Flags().GetString("type") if docType != "" { body["doc_type"] = docType } idsStr, _ := cmd.Flags().GetString("ids") if idsStr != "" { ids, err := parseIntList(idsStr) if err != nil { return nil, fmt.Errorf("invalid --ids: %w", err) } body["document_ids"] = ids } fromID, _ := cmd.Flags().GetInt("from-id") if fromID > 0 { body["from_id"] = fromID } toID, _ := cmd.Flags().GetInt("to-id") if toID > 0 { body["to_id"] = toID } force, _ := cmd.Flags().GetBool("force") if force { body["force"] = true } // Ensure at least one filter hasFilter := tagsStr != "" || docType != "" || idsStr != "" || fromID > 0 || toID > 0 if !hasFilter { return nil, fmt.Errorf("at least one filter is required (--tags, --type, --ids, --from-id, --to-id)") } return body, nil } func describeBulkFilter(cmd *cobra.Command) string { var parts []string tagsStr, _ := cmd.Flags().GetString("tags") if tagsStr != "" { parts = append(parts, fmt.Sprintf("tags=[%s]", tagsStr)) } docType, _ := cmd.Flags().GetString("type") if docType != "" { parts = append(parts, fmt.Sprintf("type=%s", docType)) } idsStr, _ := cmd.Flags().GetString("ids") if idsStr != "" { parts = append(parts, fmt.Sprintf("ids=[%s]", idsStr)) } fromID, _ := cmd.Flags().GetInt("from-id") if fromID > 0 { parts = append(parts, fmt.Sprintf("from_id=%d", fromID)) } toID, _ := cmd.Flags().GetInt("to-id") if toID > 0 { parts = append(parts, fmt.Sprintf("to_id=%d", toID)) } return strings.Join(parts, " ") } func printBulkResult(action string, result map[string]interface{}) { matched := int(result["matched"].(float64)) succeeded := int(result["succeeded"].(float64)) failed := int(result["failed"].(float64)) fmt.Printf("%s %d of %d documents", action, succeeded, matched) if failed > 0 { fmt.Printf(" (%d failed)", failed) } fmt.Println() } func parseIntList(s string) ([]int, error) { var ids []int for _, part := range strings.Split(s, ",") { part = strings.TrimSpace(part) if part == "" { continue } id, err := strconv.Atoi(part) if err != nil { return nil, fmt.Errorf("invalid ID %q: %w", part, err) } ids = append(ids, id) } return ids, nil }