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
13 changes: 11 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -29,14 +30,15 @@ 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

3. **Command Structure**:
- `jh auth`: Authentication commands (login, refresh, status, env)
- `jh dataset`: Dataset operations (list, download, upload, status)
- `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
Expand Down Expand Up @@ -84,6 +86,12 @@ go run . dataset download <dataset-name>
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
```bash
go run . project list
Expand Down Expand Up @@ -273,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

Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -148,6 +149,12 @@ go build -o jh .
- `jh dataset upload [dataset-id] <file-path>` - Upload a dataset
- `jh dataset status <dataset-id> [version]` - Show dataset status

### 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`)

- `jh project list` - List all accessible projects
Expand Down Expand Up @@ -214,6 +221,19 @@ jh dataset upload --new ./my-data.tar.gz
jh dataset upload my-dataset ./updated-data.tar.gz
```

### Registry Operations

```bash
# 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
```

### Project Operations

```bash
Expand Down
49 changes: 47 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -550,6 +551,47 @@ 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.

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
- Creation date
- Package count
- Description
- Registration status`,
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 {
fmt.Printf("Failed to get server config: %v\n", err)
os.Exit(1)
}

verbose, _ := cmd.Flags().GetBool("verbose")

if err := listRegistries(server, verbose); err != nil {
fmt.Printf("Failed to list registries: %v\n", err)
os.Exit(1)
}
},
}

var projectCmd = &cobra.Command{
Use: "project",
Short: "Project management commands",
Expand Down Expand Up @@ -982,6 +1024,8 @@ 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")
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")
Expand All @@ -994,13 +1038,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() {
Expand Down
102 changes: 102 additions & 0 deletions registries.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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, verbose bool) error {
token, err := ensureValidToken()
if err != nil {
return fmt.Errorf("authentication required: %w", err)
}

url := fmt.Sprintf("https://%s/api/v1/registry/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, &registries); 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"))

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()
}
} 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()
}
}

return nil
}

func pluralize(count int, singular, plural string) string {
if count == 1 {
return singular
}
return plural
}