Skip to content

Implement records CLI commands (list, search, get, versions)#36

Merged
ran-codes merged 1 commit intomainfrom
issue-11/records-cli
Feb 17, 2026
Merged

Implement records CLI commands (list, search, get, versions)#36
ran-codes merged 1 commit intomainfrom
issue-11/records-cli

Conversation

@ran-codes
Copy link
Copy Markdown
Owner

ELI5

This is where you can actually use the CLI to look at Zenodo records. You can list your own records, search all of Zenodo, get details on a specific record (including in BibTeX or DataCite format), and see all versions of a record. Add `--all` to get every result automatically paginated, or use `--fields id,title,doi` to pick which columns you care about. The output respects your `--output` choice (table/json/csv).

Summary

  • `zenodo records list` with `--status`, `--community`, `--all` flags
  • `zenodo records search ` with `--community`, `--all`
  • `zenodo records get ` with `--format bibtex|datacite` for non-JSON output
  • `zenodo records versions `
  • All wired through `appCtx` for token/baseURL/output/fields

Code changes

File What
`internal/cli/records.go` 4 cobra subcommands under `records`, pagination, format dispatch

Test plan

  • `go build ./...` compiles
  • `go test ./...` — all 54 tests pass
  • `zenodo records --help` shows all subcommands with descriptions

Closes #11

🤖 Generated with Claude Code

Add user-facing CLI commands for all records operations.

- records list: list user's records with --status, --community, --all
- records search <query>: Elasticsearch query with --community, --all
- records get <id>: full record details, supports --format bibtex|datacite
- records versions <id>: list all versions of a record
- Auto-pagination via --all flag with 10k ceiling warning
- All commands output via Format() respecting --output and --fields

Closes #11

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 17, 2026 19:53
@ran-codes ran-codes merged commit ddb8298 into main Feb 17, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements the core records CLI commands for the Zenodo CLI tool, enabling users to list their records, search all Zenodo records, retrieve specific records in various formats, and view all versions of a record. The implementation integrates with the existing API client layer (from #9) and output formatters (from #8), providing a consistent command-line interface with pagination support, flexible output formats, and proper separation of data output from status messages.

Changes:

  • Added four subcommands under records: list, search, get, and versions
  • Implemented auto-pagination with --all flag that respects rate limits and warns at the 10k ceiling
  • Added --format flag for records get to support BibTeX and DataCite XML exports via Accept headers
  • Integrated with the global appCtx for configuration (token, baseURL, output format, field selection)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +121 to +136
switch format {
case "bibtex":
data, err := client.GetRaw(fmt.Sprintf("/records/%d", id), "application/x-bibtex")
if err != nil {
return err
}
fmt.Println(string(data))
return nil
case "datacite":
data, err := client.GetRaw(fmt.Sprintf("/records/%d", id), "application/vnd.datacite.datacite+xml")
if err != nil {
return err
}
fmt.Println(string(data))
return nil
}
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format flag handling has an inconsistency. When format is "json", it falls through to the default case and uses the output formatter with appCtx.Output, instead of using the Accept header approach like bibtex and datacite. This means --format json and --output json would behave differently under the hood. Consider adding a case for "json" that uses GetRaw with "application/json" Accept header for consistency, or explicitly document that --format json is equivalent to --output json.

Copilot uses AI. Check for mistakes.
recordsSearchCmd.Flags().Bool("all", false, "Fetch all pages (up to 10k results)")

// records get flags
recordsGetCmd.Flags().String("format", "", "Response format: json, bibtex, datacite (default: uses --output)")
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format flag help text lists "json, bibtex, datacite" but doesn't mention "table" which is specified in requirement FR-2.3. Additionally, the help text should clarify the behavior: when format is empty, it uses the --output flag setting; when format is "bibtex" or "datacite", it requests that specific format from the API via Accept headers. The current text "(default: uses --output)" is somewhat ambiguous.

Suggested change
recordsGetCmd.Flags().String("format", "", "Response format: json, bibtex, datacite (default: uses --output)")
recordsGetCmd.Flags().String("format", "", "Response format: table, json, bibtex, datacite. When empty, uses --output; bibtex/datacite are requested via Accept headers.")

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +187
package cli

import (
"fmt"
"os"
"strconv"

"github.com/ran-codes/zenodo-cli/internal/api"
"github.com/ran-codes/zenodo-cli/internal/model"
"github.com/ran-codes/zenodo-cli/internal/output"
"github.com/spf13/cobra"
)

var recordsCmd = &cobra.Command{
Use: "records",
Short: "List, search, and view records",
}

var recordsListCmd = &cobra.Command{
Use: "list",
Short: "List your records and drafts",
Long: `List the authenticated user's records and drafts.

Examples:
zenodo records list
zenodo records list --status draft
zenodo records list --community my-org --all`,
RunE: func(cmd *cobra.Command, args []string) error {
client := api.NewClient(appCtx.BaseURL, appCtx.Token)
status, _ := cmd.Flags().GetString("status")
community, _ := cmd.Flags().GetString("community")
all, _ := cmd.Flags().GetBool("all")

params := api.RecordListParams{
Status: status,
Community: community,
}

if all {
records, total, err := api.PaginateAll(func(page int) (*model.RecordSearchResult, error) {
params.Page = page
return client.ListUserRecords(params)
})
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: %v\n", err)
}
fmt.Fprintf(os.Stderr, "Total: %d records\n", total)
return output.Format(os.Stdout, records, appCtx.Output, appCtx.Fields)
}

result, err := client.ListUserRecords(params)
if err != nil {
return err
}
fmt.Fprintf(os.Stderr, "Showing %d of %d records\n", len(result.Hits.Hits), result.Hits.Total)
return output.Format(os.Stdout, result.Hits.Hits, appCtx.Output, appCtx.Fields)
},
}

var recordsSearchCmd = &cobra.Command{
Use: "search <query>",
Short: "Search published records",
Long: `Search all published records using Elasticsearch query syntax.

Examples:
zenodo records search "climate change"
zenodo records search --community my-org "dataset"
zenodo records search "publication_date:[2024-01-01 TO 2024-12-31]" --all`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client := api.NewClient(appCtx.BaseURL, appCtx.Token)
query := args[0]
community, _ := cmd.Flags().GetString("community")
all, _ := cmd.Flags().GetBool("all")

params := api.RecordListParams{
Community: community,
}

if all {
records, total, err := api.PaginateAll(func(page int) (*model.RecordSearchResult, error) {
params.Page = page
return client.SearchRecords(query, params)
})
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: %v\n", err)
}
fmt.Fprintf(os.Stderr, "Total: %d records\n", total)
return output.Format(os.Stdout, records, appCtx.Output, appCtx.Fields)
}

result, err := client.SearchRecords(query, params)
if err != nil {
return err
}
fmt.Fprintf(os.Stderr, "Showing %d of %d records\n", len(result.Hits.Hits), result.Hits.Total)
return output.Format(os.Stdout, result.Hits.Hits, appCtx.Output, appCtx.Fields)
},
}

var recordsGetCmd = &cobra.Command{
Use: "get <id>",
Short: "Get a record by ID",
Long: `Retrieve full details of a published record.

Examples:
zenodo records get 12345
zenodo records get 12345 --output json
zenodo records get 12345 --format bibtex`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid record ID: %s", args[0])
}

client := api.NewClient(appCtx.BaseURL, appCtx.Token)
format, _ := cmd.Flags().GetString("format")

// Handle non-JSON formats via Accept header.
switch format {
case "bibtex":
data, err := client.GetRaw(fmt.Sprintf("/records/%d", id), "application/x-bibtex")
if err != nil {
return err
}
fmt.Println(string(data))
return nil
case "datacite":
data, err := client.GetRaw(fmt.Sprintf("/records/%d", id), "application/vnd.datacite.datacite+xml")
if err != nil {
return err
}
fmt.Println(string(data))
return nil
}

record, err := client.GetRecord(id)
if err != nil {
return err
}
return output.Format(os.Stdout, record, appCtx.Output, appCtx.Fields)
},
}

var recordsVersionsCmd = &cobra.Command{
Use: "versions <id>",
Short: "List all versions of a record",
Long: `List all versions of a record by its ID.

Examples:
zenodo records versions 12345`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid record ID: %s", args[0])
}

client := api.NewClient(appCtx.BaseURL, appCtx.Token)
result, err := client.ListVersions(id)
if err != nil {
return err
}
return output.Format(os.Stdout, result.Hits.Hits, appCtx.Output, appCtx.Fields)
},
}

func init() {
// records list flags
recordsListCmd.Flags().String("status", "", "Filter by status: draft, published")
recordsListCmd.Flags().String("community", "", "Filter by community ID")
recordsListCmd.Flags().Bool("all", false, "Fetch all pages (up to 10k results)")

// records search flags
recordsSearchCmd.Flags().String("community", "", "Filter by community ID")
recordsSearchCmd.Flags().Bool("all", false, "Fetch all pages (up to 10k results)")

// records get flags
recordsGetCmd.Flags().String("format", "", "Response format: json, bibtex, datacite (default: uses --output)")

recordsCmd.AddCommand(recordsListCmd)
recordsCmd.AddCommand(recordsSearchCmd)
recordsCmd.AddCommand(recordsGetCmd)
recordsCmd.AddCommand(recordsVersionsCmd)
rootCmd.AddCommand(recordsCmd)
}
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new records CLI commands lack test coverage. While the underlying API layer (internal/api/records.go) and output formatters (internal/output/) have comprehensive tests, the CLI commands themselves are untested. Consider adding tests that verify:

  1. Flag parsing and validation (e.g., status, community, all, format)
  2. Error handling when API calls fail
  3. Correct delegation to API client methods
  4. Output formatting via the output package
  5. stderr vs stdout output separation

This would align with the testing patterns established in other packages like internal/api, internal/config, and internal/output which all have comprehensive test files.

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +136
format, _ := cmd.Flags().GetString("format")

// Handle non-JSON formats via Accept header.
switch format {
case "bibtex":
data, err := client.GetRaw(fmt.Sprintf("/records/%d", id), "application/x-bibtex")
if err != nil {
return err
}
fmt.Println(string(data))
return nil
case "datacite":
data, err := client.GetRaw(fmt.Sprintf("/records/%d", id), "application/vnd.datacite.datacite+xml")
if err != nil {
return err
}
fmt.Println(string(data))
return nil
}
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format flag validation is missing. If a user provides an invalid format value (e.g., --format pdf), it silently falls through to the default behavior instead of returning an error. Consider adding validation to ensure format is one of the documented values ("bibtex", "datacite", or empty for default). Valid format values could be checked before the switch statement, or a default case could be added to return an error like: fmt.Errorf("unsupported format %q; supported: json, bibtex, datacite", format)

Copilot uses AI. Check for mistakes.
@ran-codes ran-codes deleted the issue-11/records-cli branch February 18, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement records CLI commands (list, search, get, versions)

2 participants