diff --git a/.surface b/.surface index ae0acb63..0939a8ca 100644 --- a/.surface +++ b/.surface @@ -146,6 +146,7 @@ ARG basecamp docs upload create 00 ARG basecamp docs uploads create 00 ARG basecamp docs vault create 00 ARG basecamp docs vaults create 00 +ARG basecamp docs versions 00 ARG basecamp documents archive 00 ARG basecamp documents doc create 00 ARG basecamp documents doc create 01 [content] @@ -164,6 +165,7 @@ ARG basecamp documents upload create 00 <file> ARG basecamp documents uploads create 00 <file> ARG basecamp documents vault create 00 <name> ARG basecamp documents vaults create 00 <name> +ARG basecamp documents versions 00 <upload_id|url> ARG basecamp events 00 <id|url> ARG basecamp file archive 00 <id|url> ARG basecamp file doc create 00 <title> @@ -183,6 +185,7 @@ ARG basecamp file upload create 00 <file> ARG basecamp file uploads create 00 <file> ARG basecamp file vault create 00 <name> ARG basecamp file vaults create 00 <name> +ARG basecamp file versions 00 <upload_id|url> ARG basecamp files archive 00 <id|url> ARG basecamp files doc create 00 <title> ARG basecamp files doc create 01 [content] @@ -201,6 +204,7 @@ ARG basecamp files upload create 00 <file> ARG basecamp files uploads create 00 <file> ARG basecamp files vault create 00 <name> ARG basecamp files vaults create 00 <name> +ARG basecamp files versions 00 <upload_id|url> ARG basecamp folders archive 00 <id|url> ARG basecamp folders doc create 00 <title> ARG basecamp folders doc create 01 [content] @@ -219,6 +223,7 @@ ARG basecamp folders upload create 00 <file> ARG basecamp folders uploads create 00 <file> ARG basecamp folders vault create 00 <name> ARG basecamp folders vaults create 00 <name> +ARG basecamp folders versions 00 <upload_id|url> ARG basecamp forwards replies 00 <forward_id|url> ARG basecamp forwards reply 00 <forward_id|url> ARG basecamp forwards reply 01 <reply_id|url> @@ -414,6 +419,7 @@ ARG basecamp vault upload create 00 <file> ARG basecamp vault uploads create 00 <file> ARG basecamp vault vault create 00 <name> ARG basecamp vault vaults create 00 <name> +ARG basecamp vault versions 00 <upload_id|url> ARG basecamp vaults archive 00 <id|url> ARG basecamp vaults doc create 00 <title> ARG basecamp vaults doc create 01 [content] @@ -432,6 +438,7 @@ ARG basecamp vaults upload create 00 <file> ARG basecamp vaults uploads create 00 <file> ARG basecamp vaults vault create 00 <name> ARG basecamp vaults vaults create 00 <name> +ARG basecamp vaults versions 00 <upload_id|url> ARG basecamp webhook create 00 <url> ARG basecamp webhook delete 00 <id> ARG basecamp webhook show 00 <id> @@ -652,6 +659,7 @@ CMD basecamp docs vault list CMD basecamp docs vaults CMD basecamp docs vaults create CMD basecamp docs vaults list +CMD basecamp docs versions CMD basecamp doctor CMD basecamp documents CMD basecamp documents archive @@ -688,6 +696,7 @@ CMD basecamp documents vault list CMD basecamp documents vaults CMD basecamp documents vaults create CMD basecamp documents vaults list +CMD basecamp documents versions CMD basecamp drafts CMD basecamp drafts list CMD basecamp events @@ -726,6 +735,7 @@ CMD basecamp file vault list CMD basecamp file vaults CMD basecamp file vaults create CMD basecamp file vaults list +CMD basecamp file versions CMD basecamp files CMD basecamp files archive CMD basecamp files doc @@ -761,6 +771,7 @@ CMD basecamp files vault list CMD basecamp files vaults CMD basecamp files vaults create CMD basecamp files vaults list +CMD basecamp files versions CMD basecamp folders CMD basecamp folders archive CMD basecamp folders doc @@ -796,6 +807,7 @@ CMD basecamp folders vault list CMD basecamp folders vaults CMD basecamp folders vaults create CMD basecamp folders vaults list +CMD basecamp folders versions CMD basecamp forwards CMD basecamp forwards inbox CMD basecamp forwards list @@ -1068,6 +1080,7 @@ CMD basecamp vault vault list CMD basecamp vault vaults CMD basecamp vault vaults create CMD basecamp vault vaults list +CMD basecamp vault versions CMD basecamp vaults CMD basecamp vaults archive CMD basecamp vaults doc @@ -1103,6 +1116,7 @@ CMD basecamp vaults vault list CMD basecamp vaults vaults CMD basecamp vaults vaults create CMD basecamp vaults vaults list +CMD basecamp vaults versions CMD basecamp version CMD basecamp webhook CMD basecamp webhook create @@ -6006,6 +6020,32 @@ FLAG basecamp docs vaults list --styled type=bool FLAG basecamp docs vaults list --todolist type=string FLAG basecamp docs vaults list --vault type=string FLAG basecamp docs vaults list --verbose type=count +FLAG basecamp docs versions --account type=string +FLAG basecamp docs versions --agent type=bool +FLAG basecamp docs versions --all type=bool +FLAG basecamp docs versions --cache-dir type=string +FLAG basecamp docs versions --count type=bool +FLAG basecamp docs versions --folder type=string +FLAG basecamp docs versions --help type=bool +FLAG basecamp docs versions --hints type=bool +FLAG basecamp docs versions --ids-only type=bool +FLAG basecamp docs versions --in type=string +FLAG basecamp docs versions --jq type=string +FLAG basecamp docs versions --json type=bool +FLAG basecamp docs versions --limit type=int +FLAG basecamp docs versions --markdown type=bool +FLAG basecamp docs versions --md type=bool +FLAG basecamp docs versions --no-hints type=bool +FLAG basecamp docs versions --no-stats type=bool +FLAG basecamp docs versions --page type=int +FLAG basecamp docs versions --profile type=string +FLAG basecamp docs versions --project type=string +FLAG basecamp docs versions --quiet type=bool +FLAG basecamp docs versions --stats type=bool +FLAG basecamp docs versions --styled type=bool +FLAG basecamp docs versions --todolist type=string +FLAG basecamp docs versions --vault type=string +FLAG basecamp docs versions --verbose type=count FLAG basecamp doctor --account type=string FLAG basecamp doctor --agent type=bool FLAG basecamp doctor --cache-dir type=string @@ -6920,6 +6960,32 @@ FLAG basecamp documents vaults list --styled type=bool FLAG basecamp documents vaults list --todolist type=string FLAG basecamp documents vaults list --vault type=string FLAG basecamp documents vaults list --verbose type=count +FLAG basecamp documents versions --account type=string +FLAG basecamp documents versions --agent type=bool +FLAG basecamp documents versions --all type=bool +FLAG basecamp documents versions --cache-dir type=string +FLAG basecamp documents versions --count type=bool +FLAG basecamp documents versions --folder type=string +FLAG basecamp documents versions --help type=bool +FLAG basecamp documents versions --hints type=bool +FLAG basecamp documents versions --ids-only type=bool +FLAG basecamp documents versions --in type=string +FLAG basecamp documents versions --jq type=string +FLAG basecamp documents versions --json type=bool +FLAG basecamp documents versions --limit type=int +FLAG basecamp documents versions --markdown type=bool +FLAG basecamp documents versions --md type=bool +FLAG basecamp documents versions --no-hints type=bool +FLAG basecamp documents versions --no-stats type=bool +FLAG basecamp documents versions --page type=int +FLAG basecamp documents versions --profile type=string +FLAG basecamp documents versions --project type=string +FLAG basecamp documents versions --quiet type=bool +FLAG basecamp documents versions --stats type=bool +FLAG basecamp documents versions --styled type=bool +FLAG basecamp documents versions --todolist type=string +FLAG basecamp documents versions --vault type=string +FLAG basecamp documents versions --verbose type=count FLAG basecamp drafts --account type=string FLAG basecamp drafts --agent type=bool FLAG basecamp drafts --cache-dir type=string @@ -7882,6 +7948,32 @@ FLAG basecamp file vaults list --styled type=bool FLAG basecamp file vaults list --todolist type=string FLAG basecamp file vaults list --vault type=string FLAG basecamp file vaults list --verbose type=count +FLAG basecamp file versions --account type=string +FLAG basecamp file versions --agent type=bool +FLAG basecamp file versions --all type=bool +FLAG basecamp file versions --cache-dir type=string +FLAG basecamp file versions --count type=bool +FLAG basecamp file versions --folder type=string +FLAG basecamp file versions --help type=bool +FLAG basecamp file versions --hints type=bool +FLAG basecamp file versions --ids-only type=bool +FLAG basecamp file versions --in type=string +FLAG basecamp file versions --jq type=string +FLAG basecamp file versions --json type=bool +FLAG basecamp file versions --limit type=int +FLAG basecamp file versions --markdown type=bool +FLAG basecamp file versions --md type=bool +FLAG basecamp file versions --no-hints type=bool +FLAG basecamp file versions --no-stats type=bool +FLAG basecamp file versions --page type=int +FLAG basecamp file versions --profile type=string +FLAG basecamp file versions --project type=string +FLAG basecamp file versions --quiet type=bool +FLAG basecamp file versions --stats type=bool +FLAG basecamp file versions --styled type=bool +FLAG basecamp file versions --todolist type=string +FLAG basecamp file versions --vault type=string +FLAG basecamp file versions --verbose type=count FLAG basecamp files --account type=string FLAG basecamp files --agent type=bool FLAG basecamp files --cache-dir type=string @@ -8775,6 +8867,32 @@ FLAG basecamp files vaults list --styled type=bool FLAG basecamp files vaults list --todolist type=string FLAG basecamp files vaults list --vault type=string FLAG basecamp files vaults list --verbose type=count +FLAG basecamp files versions --account type=string +FLAG basecamp files versions --agent type=bool +FLAG basecamp files versions --all type=bool +FLAG basecamp files versions --cache-dir type=string +FLAG basecamp files versions --count type=bool +FLAG basecamp files versions --folder type=string +FLAG basecamp files versions --help type=bool +FLAG basecamp files versions --hints type=bool +FLAG basecamp files versions --ids-only type=bool +FLAG basecamp files versions --in type=string +FLAG basecamp files versions --jq type=string +FLAG basecamp files versions --json type=bool +FLAG basecamp files versions --limit type=int +FLAG basecamp files versions --markdown type=bool +FLAG basecamp files versions --md type=bool +FLAG basecamp files versions --no-hints type=bool +FLAG basecamp files versions --no-stats type=bool +FLAG basecamp files versions --page type=int +FLAG basecamp files versions --profile type=string +FLAG basecamp files versions --project type=string +FLAG basecamp files versions --quiet type=bool +FLAG basecamp files versions --stats type=bool +FLAG basecamp files versions --styled type=bool +FLAG basecamp files versions --todolist type=string +FLAG basecamp files versions --vault type=string +FLAG basecamp files versions --verbose type=count FLAG basecamp folders --account type=string FLAG basecamp folders --agent type=bool FLAG basecamp folders --cache-dir type=string @@ -9668,6 +9786,32 @@ FLAG basecamp folders vaults list --styled type=bool FLAG basecamp folders vaults list --todolist type=string FLAG basecamp folders vaults list --vault type=string FLAG basecamp folders vaults list --verbose type=count +FLAG basecamp folders versions --account type=string +FLAG basecamp folders versions --agent type=bool +FLAG basecamp folders versions --all type=bool +FLAG basecamp folders versions --cache-dir type=string +FLAG basecamp folders versions --count type=bool +FLAG basecamp folders versions --folder type=string +FLAG basecamp folders versions --help type=bool +FLAG basecamp folders versions --hints type=bool +FLAG basecamp folders versions --ids-only type=bool +FLAG basecamp folders versions --in type=string +FLAG basecamp folders versions --jq type=string +FLAG basecamp folders versions --json type=bool +FLAG basecamp folders versions --limit type=int +FLAG basecamp folders versions --markdown type=bool +FLAG basecamp folders versions --md type=bool +FLAG basecamp folders versions --no-hints type=bool +FLAG basecamp folders versions --no-stats type=bool +FLAG basecamp folders versions --page type=int +FLAG basecamp folders versions --profile type=string +FLAG basecamp folders versions --project type=string +FLAG basecamp folders versions --quiet type=bool +FLAG basecamp folders versions --stats type=bool +FLAG basecamp folders versions --styled type=bool +FLAG basecamp folders versions --todolist type=string +FLAG basecamp folders versions --vault type=string +FLAG basecamp folders versions --verbose type=count FLAG basecamp forwards --account type=string FLAG basecamp forwards --agent type=bool FLAG basecamp forwards --cache-dir type=string @@ -15978,6 +16122,32 @@ FLAG basecamp vault vaults list --styled type=bool FLAG basecamp vault vaults list --todolist type=string FLAG basecamp vault vaults list --vault type=string FLAG basecamp vault vaults list --verbose type=count +FLAG basecamp vault versions --account type=string +FLAG basecamp vault versions --agent type=bool +FLAG basecamp vault versions --all type=bool +FLAG basecamp vault versions --cache-dir type=string +FLAG basecamp vault versions --count type=bool +FLAG basecamp vault versions --folder type=string +FLAG basecamp vault versions --help type=bool +FLAG basecamp vault versions --hints type=bool +FLAG basecamp vault versions --ids-only type=bool +FLAG basecamp vault versions --in type=string +FLAG basecamp vault versions --jq type=string +FLAG basecamp vault versions --json type=bool +FLAG basecamp vault versions --limit type=int +FLAG basecamp vault versions --markdown type=bool +FLAG basecamp vault versions --md type=bool +FLAG basecamp vault versions --no-hints type=bool +FLAG basecamp vault versions --no-stats type=bool +FLAG basecamp vault versions --page type=int +FLAG basecamp vault versions --profile type=string +FLAG basecamp vault versions --project type=string +FLAG basecamp vault versions --quiet type=bool +FLAG basecamp vault versions --stats type=bool +FLAG basecamp vault versions --styled type=bool +FLAG basecamp vault versions --todolist type=string +FLAG basecamp vault versions --vault type=string +FLAG basecamp vault versions --verbose type=count FLAG basecamp vaults --account type=string FLAG basecamp vaults --agent type=bool FLAG basecamp vaults --cache-dir type=string @@ -16871,6 +17041,32 @@ FLAG basecamp vaults vaults list --styled type=bool FLAG basecamp vaults vaults list --todolist type=string FLAG basecamp vaults vaults list --vault type=string FLAG basecamp vaults vaults list --verbose type=count +FLAG basecamp vaults versions --account type=string +FLAG basecamp vaults versions --agent type=bool +FLAG basecamp vaults versions --all type=bool +FLAG basecamp vaults versions --cache-dir type=string +FLAG basecamp vaults versions --count type=bool +FLAG basecamp vaults versions --folder type=string +FLAG basecamp vaults versions --help type=bool +FLAG basecamp vaults versions --hints type=bool +FLAG basecamp vaults versions --ids-only type=bool +FLAG basecamp vaults versions --in type=string +FLAG basecamp vaults versions --jq type=string +FLAG basecamp vaults versions --json type=bool +FLAG basecamp vaults versions --limit type=int +FLAG basecamp vaults versions --markdown type=bool +FLAG basecamp vaults versions --md type=bool +FLAG basecamp vaults versions --no-hints type=bool +FLAG basecamp vaults versions --no-stats type=bool +FLAG basecamp vaults versions --page type=int +FLAG basecamp vaults versions --profile type=string +FLAG basecamp vaults versions --project type=string +FLAG basecamp vaults versions --quiet type=bool +FLAG basecamp vaults versions --stats type=bool +FLAG basecamp vaults versions --styled type=bool +FLAG basecamp vaults versions --todolist type=string +FLAG basecamp vaults versions --vault type=string +FLAG basecamp vaults versions --verbose type=count FLAG basecamp version --account type=string FLAG basecamp version --agent type=bool FLAG basecamp version --cache-dir type=string @@ -17365,6 +17561,7 @@ SUB basecamp docs vault list SUB basecamp docs vaults SUB basecamp docs vaults create SUB basecamp docs vaults list +SUB basecamp docs versions SUB basecamp doctor SUB basecamp documents SUB basecamp documents archive @@ -17401,6 +17598,7 @@ SUB basecamp documents vault list SUB basecamp documents vaults SUB basecamp documents vaults create SUB basecamp documents vaults list +SUB basecamp documents versions SUB basecamp drafts SUB basecamp drafts list SUB basecamp events @@ -17439,6 +17637,7 @@ SUB basecamp file vault list SUB basecamp file vaults SUB basecamp file vaults create SUB basecamp file vaults list +SUB basecamp file versions SUB basecamp files SUB basecamp files archive SUB basecamp files doc @@ -17474,6 +17673,7 @@ SUB basecamp files vault list SUB basecamp files vaults SUB basecamp files vaults create SUB basecamp files vaults list +SUB basecamp files versions SUB basecamp folders SUB basecamp folders archive SUB basecamp folders doc @@ -17509,6 +17709,7 @@ SUB basecamp folders vault list SUB basecamp folders vaults SUB basecamp folders vaults create SUB basecamp folders vaults list +SUB basecamp folders versions SUB basecamp forwards SUB basecamp forwards inbox SUB basecamp forwards list @@ -17781,6 +17982,7 @@ SUB basecamp vault vault list SUB basecamp vault vaults SUB basecamp vault vaults create SUB basecamp vault vaults list +SUB basecamp vault versions SUB basecamp vaults SUB basecamp vaults archive SUB basecamp vaults doc @@ -17816,6 +18018,7 @@ SUB basecamp vaults vault list SUB basecamp vaults vaults SUB basecamp vaults vaults create SUB basecamp vaults vaults list +SUB basecamp vaults versions SUB basecamp version SUB basecamp webhook SUB basecamp webhook create diff --git a/API-COVERAGE.md b/API-COVERAGE.md index 20307708..472289cb 100644 --- a/API-COVERAGE.md +++ b/API-COVERAGE.md @@ -190,7 +190,7 @@ The **Since** column tags each row with the Basecamp version that introduced its | search | 2 | `search` | ✅ | BC4 | - | Full-text search + metadata. Filters: `--project`/`--in`, `--type`, `--creator`, `--since` (BC5-only), `--file-type`, `--exclude-chat`. Metadata lists recording/file search types | | recordings | 4 | `recordings` | ✅ | BC4 | - | Browse by type/status, trash/archive/restore | | **Files & Documents** | -| uploads | 8 | `files`, `uploads` | ✅ | BC4 | - | list, show, create. Create supports `--visible-to-clients` (root vault only) | +| uploads | 8 | `files`, `uploads` | ✅ | BC4 | - | list, show, create, update, download, versions (`files versions <id>`). Create supports `--visible-to-clients` (root vault only); trash/archive/restore go through `recordings` | | vaults | 8 | `files`, `vaults` | ✅ | BC4 | - | list, show, create | | documents | 8 | `files`, `docs` | ✅ | BC4 | - | list, show, create, update. Create supports `--subscribe`/`--no-subscribe`, `--visible-to-clients` (root vault only) | | attachments | 1 | `uploads`, `attachments` | ✅ | BC4 | - | Upload via `attach`; list embedded attachments via `attachments list` (parses `<bc-attachment>` from content) | diff --git a/e2e/smoke/smoke_files_read.bats b/e2e/smoke/smoke_files_read.bats index 0b037e8d..7f462955 100644 --- a/e2e/smoke/smoke_files_read.bats +++ b/e2e/smoke/smoke_files_read.bats @@ -71,6 +71,14 @@ setup_file() { assert_json_value '.ok' 'true' } +@test "files versions lists an upload's versions" { + ensure_upload || mark_unverifiable "No upload in project" + + run_smoke basecamp files versions "$QA_UPLOAD" -p "$QA_PROJECT" --json + assert_success + assert_json_value '.ok' 'true' +} + @test "files download downloads a file" { ensure_upload || return 0 diff --git a/e2e/smoke/smoke_lifecycle.bats b/e2e/smoke/smoke_lifecycle.bats index e0d4ff7f..f32d7bc6 100644 --- a/e2e/smoke/smoke_lifecycle.bats +++ b/e2e/smoke/smoke_lifecycle.bats @@ -159,6 +159,10 @@ load smoke_helper mark_out_of_scope "Shares implementation with files group (tested)" } +@test "docs versions is out of scope" { + mark_out_of_scope "Shares implementation with files group (tested)" +} + @test "docs uploads create is out of scope" { mark_out_of_scope "Shares implementation with files group (tested)" } @@ -201,6 +205,10 @@ load smoke_helper mark_out_of_scope "Shares implementation with files group (tested)" } +@test "vaults versions is out of scope" { + mark_out_of_scope "Shares implementation with files group (tested)" +} + @test "vaults uploads create is out of scope" { mark_out_of_scope "Shares implementation with files group (tested)" } diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 0d5c4e4a..be9b435a 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -44,7 +44,7 @@ func CommandCategories() []CommandCategory { {Name: "messages", Category: "core", Description: "Manage messages", Actions: []string{"list", "show", "create", "update", "publish", "pin", "unpin", "trash", "archive", "restore"}}, {Name: "chat", Category: "core", Description: "Chat in real-time", Actions: []string{"list", "messages", "post", "upload", "line", "update", "delete"}}, {Name: "cards", Category: "core", Description: "Manage Kanban cards", Actions: []string{"list", "show", "create", "update", "move", "done", "columns", "wormholes", "steps", "trash", "archive", "restore"}}, - {Name: "files", Category: "core", Description: "Manage files, documents, and folders", Actions: []string{"list", "show", "download", "update", "trash", "archive", "restore"}}, + {Name: "files", Category: "core", Description: "Manage files, documents, and folders", Actions: []string{"list", "show", "versions", "download", "update", "trash", "archive", "restore"}}, {Name: "checkins", Category: "core", Description: "View automatic check-ins", Actions: []string{"questions", "question", "answers", "answer", "reminders"}}, {Name: "schedule", Category: "core", Description: "Manage schedule entries", Actions: []string{"show", "entries", "create", "update"}}, }, diff --git a/internal/commands/files.go b/internal/commands/files.go index 1f42f742..314c496a 100644 --- a/internal/commands/files.go +++ b/internal/commands/files.go @@ -45,6 +45,7 @@ Each project has a root folder containing documents, uploads, and subfolders.`, newUploadsCmd(&project, &vaultID), newDocsCmd(&project, &vaultID), newFilesShowCmd(&project), + newFilesVersionsCmd(), newFilesUpdateCmd(&project), newFilesDownloadCmd(&project), newRecordableTrashCmd("file"), @@ -1540,6 +1541,94 @@ You can pass either an item ID or a Basecamp URL: return cmd } +func newFilesVersionsCmd() *cobra.Command { + var limit int + var page int + var all bool + + cmd := &cobra.Command{ + Use: "versions <upload_id|url>", + Short: "List an upload's versions", + Long: `List every version of an uploaded file. + +Replacing a file in Basecamp keeps the earlier copies as versions of the same +upload, so the upload ID stays stable while its contents change. + +You can pass either an upload ID or a Basecamp URL: + basecamp files versions 789 --in my-project + basecamp files versions https://3.basecamp.com/123/buckets/456/uploads/789`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + app := appctx.FromContext(cmd.Context()) + + // Validate flag combinations + if all && limit > 0 { + return output.ErrUsage("--all and --limit are mutually exclusive") + } + if page > 0 && (all || limit > 0) { + return output.ErrUsage("--page cannot be combined with --all or --limit") + } + if page > 1 { + return output.ErrUsage("only --page 1 is supported; use --all to fetch everything") + } + + if err := ensureAccount(cmd, app); err != nil { + return err + } + + uploadIDStr := extractID(args[0]) + uploadID, err := strconv.ParseInt(uploadIDStr, 10, 64) + if err != nil { + return output.ErrUsage("Invalid upload ID") + } + + // Build pagination options. The SDK treats Limit 0 as "every + // version", which is also this command's default. + opts := &basecamp.UploadVersionListOptions{} + if limit > 0 { + opts.Limit = limit + } + if page > 0 { + opts.Page = page + } + + versionsResult, err := app.Account().Uploads().ListVersions(cmd.Context(), uploadID, opts) + if err != nil { + return convertSDKError(err) + } + versions := versionsResult.Versions + + respOpts := []output.ResponseOption{ + output.WithSummary(fmt.Sprintf("%d versions of upload #%s", len(versions), uploadIDStr)), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "show", + Cmd: fmt.Sprintf("basecamp files show %s", uploadIDStr), + Description: "Show file details", + }, + output.Breadcrumb{ + Action: "download", + Cmd: fmt.Sprintf("basecamp files download %s", uploadIDStr), + Description: "Download the current version", + }, + ), + } + + if notice := output.TruncationNoticeWithTotal(len(versions), versionsResult.Meta.TotalCount); notice != "" { + respOpts = append(respOpts, output.WithNotice(notice)) + } + + return app.OK(versions, respOpts...) + }, + } + + cmd.Flags().IntVarP(&limit, "limit", "n", 0, "Maximum number of versions to fetch (0 = all)") + cmd.Flags().BoolVar(&all, "all", false, "Fetch all versions (no limit)") + cmd.Flags().IntVar(&page, "page", 0, "Fetch a single page (use --all for everything)") + + return cmd +} + func newFilesUpdateCmd(project *string) *cobra.Command { var title string var content string diff --git a/internal/commands/files_test.go b/internal/commands/files_test.go index 78f881bb..70e67cf8 100644 --- a/internal/commands/files_test.go +++ b/internal/commands/files_test.go @@ -1236,3 +1236,75 @@ func TestFilesGroupSpellingsGetHonestAccountWideSemantics(t *testing.T) { assert.NotContains(t, transport.last(t).Query, "kind=") }) } + +// mockUploadVersionsTransport serves the versions listing for upload 789 and +// records every path it is asked for, so a stray call to the wrong endpoint +// fails the test instead of passing on a lucky response. +type mockUploadVersionsTransport struct { + requests []string +} + +func (t *mockUploadVersionsTransport) RoundTrip(req *http.Request) (*http.Response, error) { + t.requests = append(t.requests, req.Method+" "+req.URL.Path) + + header := make(http.Header) + header.Set("Content-Type", "application/json") + + if req.Method != http.MethodGet || !strings.HasSuffix(req.URL.Path, "/uploads/789/versions.json") { + return nil, fmt.Errorf("unexpected request: %s %s", req.Method, req.URL.Path) + } + + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader( + `[{"id":790,"title":"report.pdf","filename":"report.pdf","status":"active"}]`, + )), + Header: header, + }, nil +} + +// TestFilesVersionsListsUploadVersions verifies the command reaches the +// account-level versions endpoint — no project scope, no extra lookups. +func TestFilesVersionsListsUploadVersions(t *testing.T) { + transport := &mockUploadVersionsTransport{} + app := showTestApp(t, transport) + + cmd := NewFilesCmd() + err := executeMessagesCommand(cmd, app, "versions", "789") + require.NoError(t, err) + + assert.Equal(t, []string{"GET /99999/uploads/789/versions.json"}, transport.requests) +} + +// TestFilesVersionsAcceptsURL verifies a pasted upload URL resolves to the +// same request as the bare ID. +func TestFilesVersionsAcceptsURL(t *testing.T) { + transport := &mockUploadVersionsTransport{} + app := showTestApp(t, transport) + + cmd := NewFilesCmd() + err := executeMessagesCommand(cmd, app, "versions", "https://3.basecamp.com/99999/buckets/456/uploads/789") + require.NoError(t, err) + + assert.Equal(t, []string{"GET /99999/uploads/789/versions.json"}, transport.requests) +} + +// TestFilesVersionsRejectsConflictingPagination pins the same pagination +// contract the other bounded listings use: --page disables the walk, so it +// cannot be combined with --all or --limit, and only page 1 is reachable. +func TestFilesVersionsRejectsConflictingPagination(t *testing.T) { + for name, args := range map[string][]string{ + "--all with --limit": {"versions", "789", "--all", "--limit", "5"}, + "--page with --all": {"versions", "789", "--page", "1", "--all"}, + "--page beyond 1": {"versions", "789", "--page", "2"}, + } { + t.Run(name, func(t *testing.T) { + transport := &mockUploadVersionsTransport{} + app := showTestApp(t, transport) + + err := executeMessagesCommand(NewFilesCmd(), app, args...) + require.Error(t, err) + assert.Empty(t, transport.requests, "must refuse before any request") + }) + } +} diff --git a/skills/basecamp/SKILL.md b/skills/basecamp/SKILL.md index 44f79ac2..72a45465 100644 --- a/skills/basecamp/SKILL.md +++ b/skills/basecamp/SKILL.md @@ -747,6 +747,8 @@ basecamp files list --all-projects --limit 500 # Walk pages until 500 c basecamp files list --all-projects --page 2 # Exactly page 2 basecamp files list --all-projects --all # Every page (slow on big accounts) basecamp files show <id> --in <project> # Show item (auto-detects type) +basecamp files versions <upload_id> --json # Every version of an uploaded file +basecamp files versions <upload_id> --limit 5 --json # Cap results (default: all) basecamp files download <id> --in <project> # Download file basecamp files download <id> --out ./dir # Download to specific dir basecamp files download "https://storage.../download/f" # Download from storage URL @@ -777,6 +779,11 @@ server default; as with Messages, a **client-authenticated caller always creates client-visible records** regardless. `recordings visibility` is **not** a remediation for nested docs/uploads. +**Upload versions:** replacing a file keeps the earlier copies under the same +upload ID, so `basecamp files versions <upload_id>` is how you see the history of +one file. A file that was never replaced returns its single current version, not +an error. Only `--page 1` is accepted; use `--all` to walk every page. + **Subcommands:** `folders`, `uploads`, `documents` (each with pagination flags) ### Schedule