package main import ( "bufio" "encoding/json" "fmt" "io" "net/http" "os" "strings" ) type PackageVersion struct { ID int `json:"id"` Name string `json:"name"` } type GitHubClient struct { token string owner string client *http.Client baseURL string } func NewGitHubClient(token, owner string) *GitHubClient { return &GitHubClient{ token: token, owner: owner, client: &http.Client{}, baseURL: "https://api.github.com", } } func (g *GitHubClient) makeRequest(method, url string) (*http.Response, error) { req, err := http.NewRequest(method, url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+g.token) req.Header.Set("Accept", "application/vnd.github+json") req.Header.Set("X-GitHub-Api-Version", "2022-11-28") return g.client.Do(req) } func (g *GitHubClient) getPackageVersions(packageName, packageType string) ([]PackageVersion, error) { url := fmt.Sprintf("%s/users/%s/packages/%s/%s/versions", g.baseURL, g.owner, packageType, packageName) var allVersions []PackageVersion page := 1 for { pageURL := fmt.Sprintf("%s?page=%d&per_page=100", url, page) resp, err := g.makeRequest("GET", pageURL) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(body)) } var versions []PackageVersion if err := json.NewDecoder(resp.Body).Decode(&versions); err != nil { return nil, fmt.Errorf("decode failed: %w", err) } if len(versions) == 0 { break } allVersions = append(allVersions, versions...) page++ } return allVersions, nil } func (g *GitHubClient) deleteVersion(packageName, packageType string, versionID int) error { url := fmt.Sprintf("%s/users/%s/packages/%s/%s/versions/%d", g.baseURL, g.owner, packageType, packageName, versionID) resp, err := g.makeRequest("DELETE", url) if err != nil { return fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("delete failed %d: %s", resp.StatusCode, string(body)) } return nil } func findVersionsToDelete(versions []PackageVersion, keepVersion string) []PackageVersion { var toDelete []PackageVersion found := false // Find the version to keep and mark everything after it for deletion for i := len(versions) - 1; i >= 0; i-- { if versions[i].Name == keepVersion { found = true break } toDelete = append([]PackageVersion{versions[i]}, toDelete...) } if !found { fmt.Printf("Warning: Version '%s' not found in package versions\n", keepVersion) return nil } return toDelete } func getUserInput(prompt string) string { fmt.Print(prompt) scanner := bufio.NewScanner(os.Stdin) scanner.Scan() return strings.TrimSpace(scanner.Text()) } func confirmDeletion(count int) bool { response := getUserInput(fmt.Sprintf("Delete %d versions? (y/N): ", count)) return strings.ToLower(response) == "y" } func main() { token := os.Getenv("GITHUB_TOKEN") if token == "" { fmt.Println("Error: GITHUB_TOKEN environment variable is required") fmt.Println("Create a token with 'packages:read' and 'packages:delete' scopes") os.Exit(1) } owner := getUserInput("Owner (username/org): ") if owner == "" { fmt.Println("Error: Owner is required") os.Exit(1) } packageName := getUserInput("Package name: ") if packageName == "" { fmt.Println("Error: Package name is required") os.Exit(1) } packageType := getUserInput("Package type [npm]: ") if packageType == "" { packageType = "npm" } client := NewGitHubClient(token, owner) fmt.Printf("Fetching versions for %s/%s...\n", owner, packageName) versions, err := client.getPackageVersions(packageName, packageType) if err != nil { fmt.Printf("Error fetching versions: %v\n", err) os.Exit(1) } if len(versions) == 0 { fmt.Println("No versions found") return } fmt.Printf("\nFound %d versions:\n", len(versions)) for i, v := range versions { fmt.Printf("%d. %s (ID: %d)\n", i+1, v.Name, v.ID) } keepVersion := getUserInput("\nVersion to keep (and newer): ") if keepVersion == "" { fmt.Println("Error: Version is required") os.Exit(1) } toDelete := findVersionsToDelete(versions, keepVersion) if len(toDelete) == 0 { fmt.Println("No versions to delete") return } fmt.Printf("\nVersions to delete:\n") for _, v := range toDelete { fmt.Printf("- %s (ID: %d)\n", v.Name, v.ID) } if !confirmDeletion(len(toDelete)) { fmt.Println("Cancelled") return } fmt.Println("\nDeleting versions...") deleted := 0 for _, v := range toDelete { if err := client.deleteVersion(packageName, packageType, v.ID); err != nil { fmt.Printf("Failed to delete %s: %v\n", v.Name, err) } else { fmt.Printf("Deleted %s\n", v.Name) deleted++ } } fmt.Printf("\nDeleted %d/%d versions\n", deleted, len(toDelete)) }