diff --git a/cmd/commands_test.go b/cmd/commands_test.go index 33af825..90a4cc5 100644 --- a/cmd/commands_test.go +++ b/cmd/commands_test.go @@ -367,6 +367,7 @@ func TestDocsCommands(t *testing.T) { {"set-paragraph-style"}, {"add-list"}, {"remove-list"}, + {"trash"}, } for _, tt := range tests { diff --git a/cmd/docs.go b/cmd/docs.go index e82bf77..d7c0bd6 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -11,6 +11,7 @@ import ( "github.com/omriariav/workspace-cli/internal/printer" "github.com/spf13/cobra" "google.golang.org/api/docs/v1" + "google.golang.org/api/drive/v3" ) var docsCmd = &cobra.Command{ @@ -129,6 +130,22 @@ Positions are 1-based indices. Use 'gws docs read --include-formatting' to RunE: runDocsRemoveList, } +var docsTrashCmd = &cobra.Command{ + Use: "trash ", + Short: "Trash or permanently delete a document", + Long: `Moves a Google Doc to the trash via the Drive API. + +By default, moves the document to trash. Use --permanent to permanently delete. + +Warning: --permanent bypasses trash and cannot be undone. + +Examples: + gws docs trash 1abc123xyz + gws docs trash 1abc123xyz --permanent`, + Args: cobra.ExactArgs(1), + RunE: runDocsTrash, +} + func init() { rootCmd.AddCommand(docsCmd) docsCmd.AddCommand(docsReadCmd) @@ -143,6 +160,10 @@ func init() { docsCmd.AddCommand(docsSetParagraphStyleCmd) docsCmd.AddCommand(docsAddListCmd) docsCmd.AddCommand(docsRemoveListCmd) + docsCmd.AddCommand(docsTrashCmd) + + // Trash flags + docsTrashCmd.Flags().Bool("permanent", false, "Permanently delete (skip trash)") // Format flags docsFormatCmd.Flags().Int64("from", 0, "Start position (1-based index, required)") @@ -1039,3 +1060,52 @@ func runDocsRemoveList(cmd *cobra.Command, args []string) error { "to": to, }) } + +func runDocsTrash(cmd *cobra.Command, args []string) error { + p := printer.New(os.Stdout, GetFormat()) + ctx := context.Background() + + factory, err := client.NewFactory(ctx) + if err != nil { + return p.PrintError(err) + } + + svc, err := factory.Drive() + if err != nil { + return p.PrintError(err) + } + + docID := args[0] + permanent, _ := cmd.Flags().GetBool("permanent") + + // Get file info first for the response + file, err := svc.Files.Get(docID).SupportsAllDrives(true).Fields("name").Do() + if err != nil { + return p.PrintError(fmt.Errorf("failed to get document info: %w", err)) + } + + if permanent { + err = svc.Files.Delete(docID).SupportsAllDrives(true).Do() + if err != nil { + return p.PrintError(fmt.Errorf("failed to delete document: %w", err)) + } + + return p.Print(map[string]interface{}{ + "status": "deleted", + "document_id": docID, + "name": file.Name, + }) + } + + // Move to trash + _, err = svc.Files.Update(docID, &drive.File{Trashed: true}).SupportsAllDrives(true).Do() + if err != nil { + return p.PrintError(fmt.Errorf("failed to trash document: %w", err)) + } + + return p.Print(map[string]interface{}{ + "status": "trashed", + "document_id": docID, + "name": file.Name, + }) +} diff --git a/cmd/docs_test.go b/cmd/docs_test.go index 7ce4a3c..c6807bf 100644 --- a/cmd/docs_test.go +++ b/cmd/docs_test.go @@ -9,6 +9,7 @@ import ( "testing" "google.golang.org/api/docs/v1" + "google.golang.org/api/drive/v3" "google.golang.org/api/option" ) @@ -1371,3 +1372,131 @@ func TestParseDocsHexColor(t *testing.T) { }) } } + +// TestDocsTrashCommand_Flags tests trash command flags +func TestDocsTrashCommand_Flags(t *testing.T) { + cmd := findSubcommand(docsCmd, "trash") + if cmd == nil { + t.Fatal("docs trash command not found") + } + + if cmd.Flags().Lookup("permanent") == nil { + t.Error("expected --permanent flag") + } +} + +// TestDocsTrash_MoveToTrash tests trashing a document (default behavior) +func TestDocsTrash_MoveToTrash(t *testing.T) { + getCalled := false + updateCalled := false + + handlers := map[string]func(w http.ResponseWriter, r *http.Request){ + "/files/doc-trash-1": func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + getCalled = true + json.NewEncoder(w).Encode(&drive.File{ + Id: "doc-trash-1", + Name: "My Document", + }) + } else if r.Method == "PATCH" { + updateCalled = true + + var req drive.File + json.NewDecoder(r.Body).Decode(&req) + + if !req.Trashed { + t.Error("expected Trashed to be true") + } + + json.NewEncoder(w).Encode(&drive.File{ + Id: "doc-trash-1", + Name: "My Document", + Trashed: true, + }) + } + }, + } + + server := mockDocsServer(t, handlers) + defer server.Close() + + svc, err := drive.NewService(context.Background(), option.WithoutAuthentication(), option.WithEndpoint(server.URL)) + if err != nil { + t.Fatalf("failed to create drive service: %v", err) + } + + // Get file info + file, err := svc.Files.Get("doc-trash-1").SupportsAllDrives(true).Fields("name").Do() + if err != nil { + t.Fatalf("failed to get file: %v", err) + } + + if file.Name != "My Document" { + t.Errorf("expected name 'My Document', got '%s'", file.Name) + } + + // Move to trash + _, err = svc.Files.Update("doc-trash-1", &drive.File{Trashed: true}).SupportsAllDrives(true).Do() + if err != nil { + t.Fatalf("failed to trash document: %v", err) + } + + if !getCalled { + t.Error("get endpoint was not called") + } + if !updateCalled { + t.Error("update endpoint was not called") + } +} + +// TestDocsTrash_PermanentDelete tests permanently deleting a document +func TestDocsTrash_PermanentDelete(t *testing.T) { + getCalled := false + deleteCalled := false + + handlers := map[string]func(w http.ResponseWriter, r *http.Request){ + "/files/doc-perm-del": func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + getCalled = true + json.NewEncoder(w).Encode(&drive.File{ + Id: "doc-perm-del", + Name: "Delete Me", + }) + } else if r.Method == "DELETE" { + deleteCalled = true + w.WriteHeader(http.StatusNoContent) + } + }, + } + + server := mockDocsServer(t, handlers) + defer server.Close() + + svc, err := drive.NewService(context.Background(), option.WithoutAuthentication(), option.WithEndpoint(server.URL)) + if err != nil { + t.Fatalf("failed to create drive service: %v", err) + } + + // Get file info + file, err := svc.Files.Get("doc-perm-del").SupportsAllDrives(true).Fields("name").Do() + if err != nil { + t.Fatalf("failed to get file: %v", err) + } + + if file.Name != "Delete Me" { + t.Errorf("expected name 'Delete Me', got '%s'", file.Name) + } + + // Permanently delete + err = svc.Files.Delete("doc-perm-del").SupportsAllDrives(true).Do() + if err != nil { + t.Fatalf("failed to delete document: %v", err) + } + + if !getCalled { + t.Error("get endpoint was not called") + } + if !deleteCalled { + t.Error("delete endpoint was not called") + } +} diff --git a/skills/docs/SKILL.md b/skills/docs/SKILL.md index 20a550d..c898677 100644 --- a/skills/docs/SKILL.md +++ b/skills/docs/SKILL.md @@ -47,6 +47,8 @@ For initial setup, see the `gws-auth` skill. | Set paragraph style | `gws docs set-paragraph-style --from 1 --to 100 --alignment CENTER` | | Add a list | `gws docs add-list --at 1 --type bullet --items "A;B;C"` | | Remove list | `gws docs remove-list --from 1 --to 50` | +| Trash document | `gws docs trash ` | +| Permanently delete | `gws docs trash --permanent` | ## Detailed Usage @@ -192,6 +194,15 @@ gws docs remove-list [flags] - `--from int` — Start position (1-based index, required) - `--to int` — End position (1-based index, required) +### trash — Trash or permanently delete a document + +```bash +gws docs trash [flags] +``` + +**Flags:** +- `--permanent` — Permanently delete instead of trashing (cannot be undone) + ## Content Formats The `--content-format` flag controls how `--text` input is handled for `create`, `append`, and `insert`. diff --git a/skills/docs/references/commands.md b/skills/docs/references/commands.md index 08e4029..88d71fc 100644 --- a/skills/docs/references/commands.md +++ b/skills/docs/references/commands.md @@ -315,6 +315,38 @@ gws docs remove-list 1abc123xyz --from 1 --to 999999 --- +## gws docs trash + +Moves a Google Doc to the trash via the Drive API. + +``` +Usage: gws docs trash [flags] +``` + +### Flags + +| Flag | Type | Default | Required | Description | +|------|------|---------|----------|-------------| +| `--permanent` | bool | false | No | Permanently delete (skip trash) | + +### Examples + +```bash +# Move a document to trash +gws docs trash 1abc123xyz + +# Permanently delete a document (cannot be undone) +gws docs trash 1abc123xyz --permanent +``` + +### Notes + +- Default behavior moves the document to Drive trash (recoverable) +- `--permanent` bypasses trash and permanently deletes the document +- Uses the Drive API since the Docs API does not have a native delete endpoint + +--- + ## Content Formats The `--content-format` flag is available on `create`, `append`, and `insert` commands.