From 848095908d4b306867b1a454753700526db72026 Mon Sep 17 00:00:00 2001 From: Neeladri Das Date: Thu, 29 Jan 2026 11:15:28 +0000 Subject: [PATCH 1/3] registry list cmd --- CLAUDE.md | 11 ++++-- README.md | 15 +++++++++ main.go | 45 +++++++++++++++++++++++-- registries.go | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 registries.go diff --git a/CLAUDE.md b/CLAUDE.md index bd32ad4..0442344 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -This is a Go-based CLI tool for interacting with JuliaHub, a platform for Julia computing. The CLI provides commands for authentication, dataset management, project management, user information, Git integration, and Julia integration. +This is a Go-based CLI tool for interacting with JuliaHub, a platform for Julia computing. The CLI provides commands for authentication, dataset management, registry management, project management, user information, Git integration, and Julia integration. ## Architecture @@ -13,6 +13,7 @@ The application follows a command-line interface pattern using the Cobra library - **main.go**: Core CLI structure with command definitions and configuration management - **auth.go**: OAuth2 device flow authentication with JWT token handling - **datasets.go**: Dataset operations (list, download, upload, status) with REST API integration +- **registries.go**: Registry operations (list) with REST API integration - **projects.go**: Project management using GraphQL API with user filtering - **user.go**: User information retrieval using GraphQL API - **git.go**: Git integration (clone, push, fetch, pull) with JuliaHub authentication @@ -29,7 +30,7 @@ The application follows a command-line interface pattern using the Cobra library - Stores tokens securely in `~/.juliahub` with 0600 permissions 2. **API Integration**: - - **REST API**: Used for dataset operations (`/api/v1/datasets`, `/datasets/{uuid}/url/{version}`) + - **REST API**: Used for dataset operations (`/api/v1/datasets`, `/datasets/{uuid}/url/{version}`) and registry operations (`/api/v1/ui/registries/descriptions`) - **GraphQL API**: Used for projects and user info (`/v1/graphql`) - **Headers**: All GraphQL requests require `X-Hasura-Role: jhuser` header - **Authentication**: Uses ID tokens (`token.IDToken`) for API calls @@ -37,6 +38,7 @@ The application follows a command-line interface pattern using the Cobra library 3. **Command Structure**: - `jh auth`: Authentication commands (login, refresh, status, env) - `jh dataset`: Dataset operations (list, download, upload, status) + - `jh registry`: Registry operations (list) - `jh project`: Project management (list with GraphQL, supports user filtering) - `jh user`: User information (info with GraphQL) - `jh clone`: Git clone with JuliaHub authentication and project name resolution @@ -84,6 +86,11 @@ go run . dataset download go run . dataset upload --new ./file.tar.gz ``` +### Test registry operations +```bash +go run . registry list +``` + ### Test project and user operations ```bash go run . project list diff --git a/README.md b/README.md index e896a05..6766022 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ A command-line interface for interacting with JuliaHub, a platform for Julia com - **Authentication**: OAuth2 device flow authentication with JWT token handling - **Dataset Management**: List, download, upload, and check status of datasets +- **Registry Management**: List and manage Julia package registries - **Project Management**: List and filter projects using GraphQL API - **Git Integration**: Clone, push, fetch, and pull with automatic JuliaHub authentication - **Julia Integration**: Install Julia and run with JuliaHub package server configuration @@ -148,6 +149,10 @@ go build -o jh . - `jh dataset upload [dataset-id] ` - Upload a dataset - `jh dataset status [version]` - Show dataset status +### Registry Management (`jh registry`) + +- `jh registry list` - List all package registries on JuliaHub + ### Project Management (`jh project`) - `jh project list` - List all accessible projects @@ -214,6 +219,16 @@ jh dataset upload --new ./my-data.tar.gz jh dataset upload my-dataset ./updated-data.tar.gz ``` +### Registry Operations + +```bash +# List all registries +jh registry list + +# List registries on custom server +jh registry list -s yourinstall +``` + ### Project Operations ```bash diff --git a/main.go b/main.go index 71b31c2..e8d46ff 100644 --- a/main.go +++ b/main.go @@ -161,11 +161,12 @@ job execution, project management, Git integration, and package hosting capabili Available command categories: auth - Authentication and token management dataset - Dataset operations (list, download, upload, status) + registry - Registry management (list registries) project - Project management (list, filter by user) user - User information and profile clone - Clone projects with automatic authentication push - Push changes with authentication - fetch - Fetch updates with authentication + fetch - Fetch updates with authentication pull - Pull changes with authentication julia - Julia installation and management run - Run Julia with JuliaHub configuration @@ -550,6 +551,44 @@ Displays: }, } +var registryCmd = &cobra.Command{ + Use: "registry", + Short: "Registry management commands", + Long: `Manage Julia package registries on JuliaHub. + +Registries are collections of Julia packages that can be registered and +installed. JuliaHub supports multiple registries including the General +registry, custom organizational registries, and test registries.`, +} + +var registryListCmd = &cobra.Command{ + Use: "list", + Short: "List registries", + Long: `List all package registries on JuliaHub. + +Displays information including: +- Registry UUID +- Registry name and ID +- Owner information +- Creation date +- Package count +- Description +- Registration status`, + Example: " jh registry list\n jh registry list -s custom-server.com", + Run: func(cmd *cobra.Command, args []string) { + server, err := getServerFromFlagOrConfig(cmd) + if err != nil { + fmt.Printf("Failed to get server config: %v\n", err) + os.Exit(1) + } + + if err := listRegistries(server); err != nil { + fmt.Printf("Failed to list registries: %v\n", err) + os.Exit(1) + } + }, +} + var projectCmd = &cobra.Command{ Use: "project", Short: "Project management commands", @@ -982,6 +1021,7 @@ func init() { datasetUploadCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") datasetUploadCmd.Flags().Bool("new", false, "Create a new dataset") datasetStatusCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") + registryListCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") projectListCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") projectListCmd.Flags().String("user", "", "Filter projects by user (leave empty to show only your own projects)") userInfoCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") @@ -994,13 +1034,14 @@ func init() { authCmd.AddCommand(authLoginCmd, authRefreshCmd, authStatusCmd, authEnvCmd) jobCmd.AddCommand(jobListCmd, jobStartCmd) datasetCmd.AddCommand(datasetListCmd, datasetDownloadCmd, datasetUploadCmd, datasetStatusCmd) + registryCmd.AddCommand(registryListCmd) projectCmd.AddCommand(projectListCmd) userCmd.AddCommand(userInfoCmd) juliaCmd.AddCommand(juliaInstallCmd) runCmd.AddCommand(runSetupCmd) gitCredentialCmd.AddCommand(gitCredentialHelperCmd, gitCredentialGetCmd, gitCredentialStoreCmd, gitCredentialEraseCmd, gitCredentialSetupCmd) - rootCmd.AddCommand(authCmd, jobCmd, datasetCmd, projectCmd, userCmd, juliaCmd, cloneCmd, pushCmd, fetchCmd, pullCmd, runCmd, gitCredentialCmd, updateCmd) + rootCmd.AddCommand(authCmd, jobCmd, datasetCmd, registryCmd, projectCmd, userCmd, juliaCmd, cloneCmd, pushCmd, fetchCmd, pullCmd, runCmd, gitCredentialCmd, updateCmd) } func main() { diff --git a/registries.go b/registries.go new file mode 100644 index 0000000..6d46180 --- /dev/null +++ b/registries.go @@ -0,0 +1,92 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +type Registry struct { + UUID string `json:"uuid"` + Name string `json:"name"` + RegistryID int `json:"registry_id"` + Owner *string `json:"owner"` + Register bool `json:"register"` + CreationDate CustomTime `json:"creation_date"` + PackageCount int `json:"package_count"` + Description string `json:"description"` +} + +func listRegistries(server string) error { + token, err := ensureValidToken() + if err != nil { + return fmt.Errorf("authentication required: %w", err) + } + + url := fmt.Sprintf("https://%s/api/v1/ui/registries/descriptions", server) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.IDToken)) + req.Header.Set("Accept", "application/json") + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to make request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("API request failed (status %d): %s", resp.StatusCode, string(body)) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response: %w", err) + } + + var registries []Registry + if err := json.Unmarshal(body, ®istries); err != nil { + return fmt.Errorf("failed to parse response: %w", err) + } + + if len(registries) == 0 { + fmt.Println("No registries found") + return nil + } + + fmt.Printf("Found %d registr%s:\n\n", len(registries), pluralize(len(registries), "y", "ies")) + for _, registry := range registries { + fmt.Printf("UUID: %s\n", registry.UUID) + fmt.Printf("Name: %s\n", registry.Name) + fmt.Printf("Registry ID: %d\n", registry.RegistryID) + if registry.Owner != nil { + fmt.Printf("Owner: %s\n", *registry.Owner) + } else { + fmt.Printf("Owner: (none)\n") + } + fmt.Printf("Register: %t\n", registry.Register) + fmt.Printf("Creation Date: %s\n", registry.CreationDate.Time.Format(time.RFC3339)) + fmt.Printf("Package Count: %d\n", registry.PackageCount) + if registry.Description != "" { + fmt.Printf("Description: %s\n", registry.Description) + } + fmt.Println() + } + + return nil +} + +func pluralize(count int, singular, plural string) string { + if count == 1 { + return singular + } + return plural +} From dd52750c171255014804e64cc91740dcb8c57f9c Mon Sep 17 00:00:00 2001 From: Neeladri Das Date: Mon, 2 Feb 2026 07:58:33 +0000 Subject: [PATCH 2/3] added verbose for detailed view --- CLAUDE.md | 4 +++- README.md | 7 ++++++- main.go | 10 +++++++--- registries.go | 40 +++++++++++++++++++++++++--------------- 4 files changed, 41 insertions(+), 20 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0442344..b3819e0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,7 +38,7 @@ The application follows a command-line interface pattern using the Cobra library 3. **Command Structure**: - `jh auth`: Authentication commands (login, refresh, status, env) - `jh dataset`: Dataset operations (list, download, upload, status) - - `jh registry`: Registry operations (list) + - `jh registry`: Registry operations (list with REST API, supports verbose mode) - `jh project`: Project management (list with GraphQL, supports user filtering) - `jh user`: User information (info with GraphQL) - `jh clone`: Git clone with JuliaHub authentication and project name resolution @@ -89,6 +89,7 @@ go run . dataset upload --new ./file.tar.gz ### Test registry operations ```bash go run . registry list +go run . registry list --verbose ``` ### Test project and user operations @@ -280,6 +281,7 @@ jh run setup - Clone command automatically resolves `username/project` format to project UUIDs - Folder naming conflicts are resolved with automatic numbering (project-1, project-2, etc.) - Credential helper follows Git protocol: responds only to JuliaHub URLs, ignores others +- Registry list output is concise by default (UUID and Name only); use `--verbose` flag for detailed information (owner, creation date, package count, description) ## Implementation Details diff --git a/README.md b/README.md index 6766022..2de4c8d 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,8 @@ go build -o jh . ### Registry Management (`jh registry`) - `jh registry list` - List all package registries on JuliaHub + - Default: Shows only UUID and Name + - `jh registry list --verbose` - Show detailed registry information including owner, creation date, package count, and description ### Project Management (`jh project`) @@ -222,9 +224,12 @@ jh dataset upload my-dataset ./updated-data.tar.gz ### Registry Operations ```bash -# List all registries +# List all registries (UUID and Name only) jh registry list +# List registries with detailed information +jh registry list --verbose + # List registries on custom server jh registry list -s yourinstall ``` diff --git a/main.go b/main.go index e8d46ff..77af961 100644 --- a/main.go +++ b/main.go @@ -566,7 +566,8 @@ var registryListCmd = &cobra.Command{ Short: "List registries", Long: `List all package registries on JuliaHub. -Displays information including: +By default, displays only UUID and Name for each registry. +Use --verbose flag to display comprehensive information including: - Registry UUID - Registry name and ID - Owner information @@ -574,7 +575,7 @@ Displays information including: - Package count - Description - Registration status`, - Example: " jh registry list\n jh registry list -s custom-server.com", + Example: " jh registry list\n jh registry list --verbose\n jh registry list -s custom-server.com", Run: func(cmd *cobra.Command, args []string) { server, err := getServerFromFlagOrConfig(cmd) if err != nil { @@ -582,7 +583,9 @@ Displays information including: os.Exit(1) } - if err := listRegistries(server); err != nil { + verbose, _ := cmd.Flags().GetBool("verbose") + + if err := listRegistries(server, verbose); err != nil { fmt.Printf("Failed to list registries: %v\n", err) os.Exit(1) } @@ -1022,6 +1025,7 @@ func init() { datasetUploadCmd.Flags().Bool("new", false, "Create a new dataset") datasetStatusCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") registryListCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") + registryListCmd.Flags().Bool("verbose", false, "Show detailed registry information") projectListCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") projectListCmd.Flags().String("user", "", "Filter projects by user (leave empty to show only your own projects)") userInfoCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") diff --git a/registries.go b/registries.go index 6d46180..6bc8e9e 100644 --- a/registries.go +++ b/registries.go @@ -19,7 +19,7 @@ type Registry struct { Description string `json:"description"` } -func listRegistries(server string) error { +func listRegistries(server string, verbose bool) error { token, err := ensureValidToken() if err != nil { return fmt.Errorf("authentication required: %w", err) @@ -63,22 +63,32 @@ func listRegistries(server string) error { } fmt.Printf("Found %d registr%s:\n\n", len(registries), pluralize(len(registries), "y", "ies")) - for _, registry := range registries { - fmt.Printf("UUID: %s\n", registry.UUID) - fmt.Printf("Name: %s\n", registry.Name) - fmt.Printf("Registry ID: %d\n", registry.RegistryID) - if registry.Owner != nil { - fmt.Printf("Owner: %s\n", *registry.Owner) - } else { - fmt.Printf("Owner: (none)\n") + + if verbose { + // Verbose mode: show all details + for _, registry := range registries { + fmt.Printf("UUID: %s\n", registry.UUID) + fmt.Printf("Name: %s\n", registry.Name) + if registry.Owner != nil { + fmt.Printf("Owner: %s\n", *registry.Owner) + } else { + fmt.Printf("Owner: (none)\n") + } + fmt.Printf("Register: %t\n", registry.Register) + fmt.Printf("Creation Date: %s\n", registry.CreationDate.Time.Format(time.RFC3339)) + fmt.Printf("Package Count: %d\n", registry.PackageCount) + if registry.Description != "" { + fmt.Printf("Description: %s\n", registry.Description) + } + fmt.Println() } - fmt.Printf("Register: %t\n", registry.Register) - fmt.Printf("Creation Date: %s\n", registry.CreationDate.Time.Format(time.RFC3339)) - fmt.Printf("Package Count: %d\n", registry.PackageCount) - if registry.Description != "" { - fmt.Printf("Description: %s\n", registry.Description) + } else { + // Default mode: show only UUID and Name + for _, registry := range registries { + fmt.Printf("UUID: %s\n", registry.UUID) + fmt.Printf("Name: %s\n", registry.Name) + fmt.Println() } - fmt.Println() } return nil From 395bd28758e75b04a99a32ac3e0bd90a5b506581 Mon Sep 17 00:00:00 2001 From: Neeladri Das Date: Tue, 3 Feb 2026 10:16:10 +0000 Subject: [PATCH 3/3] updated the registry api --- registries.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registries.go b/registries.go index 6bc8e9e..7012504 100644 --- a/registries.go +++ b/registries.go @@ -25,7 +25,7 @@ func listRegistries(server string, verbose bool) error { return fmt.Errorf("authentication required: %w", err) } - url := fmt.Sprintf("https://%s/api/v1/ui/registries/descriptions", server) + url := fmt.Sprintf("https://%s/api/v1/registry/registries/descriptions", server) req, err := http.NewRequest("GET", url, nil) if err != nil {