Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions cmd/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ func TestDocsCommands(t *testing.T) {
{"set-paragraph-style"},
{"add-list"},
{"remove-list"},
{"trash"},
}

for _, tt := range tests {
Expand Down
70 changes: 70 additions & 0 deletions cmd/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -129,6 +130,22 @@ Positions are 1-based indices. Use 'gws docs read <id> --include-formatting' to
RunE: runDocsRemoveList,
}

var docsTrashCmd = &cobra.Command{
Use: "trash <document-id>",
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)
Expand All @@ -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)")
Expand Down Expand Up @@ -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,
})
}
129 changes: 129 additions & 0 deletions cmd/docs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"

"google.golang.org/api/docs/v1"
"google.golang.org/api/drive/v3"
"google.golang.org/api/option"
)

Expand Down Expand Up @@ -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")
}
}
11 changes: 11 additions & 0 deletions skills/docs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ For initial setup, see the `gws-auth` skill.
| Set paragraph style | `gws docs set-paragraph-style <doc-id> --from 1 --to 100 --alignment CENTER` |
| Add a list | `gws docs add-list <doc-id> --at 1 --type bullet --items "A;B;C"` |
| Remove list | `gws docs remove-list <doc-id> --from 1 --to 50` |
| Trash document | `gws docs trash <doc-id>` |
| Permanently delete | `gws docs trash <doc-id> --permanent` |

## Detailed Usage

Expand Down Expand Up @@ -192,6 +194,15 @@ gws docs remove-list <document-id> [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 <document-id> [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`.
Expand Down
32 changes: 32 additions & 0 deletions skills/docs/references/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <document-id> [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.
Expand Down
Loading