Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
26 changes: 22 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## 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, registry management, project management, user information, token management, Git integration, and Julia integration.

## Architecture
Expand All @@ -13,7 +12,8 @@ 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
- **registries.go**: Registry operations (list, fetch) with REST API integration
- **packages.go**: Package search with GraphQL primary path and REST API fallback (`/packages/info`)
- **projects.go**: Project management using GraphQL API with user filtering
- **user.go**: User information retrieval using GraphQL API and REST API for listing users
- **tokens.go**: Token management operations (list) with REST API integration
Expand All @@ -31,15 +31,16 @@ 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}`), registry operations (`/api/v1/ui/registries/descriptions`), token management (`/app/token/activelist`) and user management (`/app/config/features/manage`)
- **GraphQL API**: Used for projects and user info (`/v1/graphql`)
- **REST API**: Used for dataset operations (`/api/v1/datasets`, `/datasets/{uuid}/url/{version}`), registry operations (`/api/v1/registry/registries/descriptions`), package search/info fallback (`/packages/info`), token management (`/app/token/activelist`) and user management (`/app/config/features/manage`)
- **GraphQL API**: Used for projects, user info, package search, and package info primary path (`/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 package`: Package search and info (GraphQL primary, REST fallback via `/packages/info`; supports filtering by registry)
- `jh project`: Project management (list with GraphQL, supports user filtering)
- `jh user`: User information (info with GraphQL)
- `jh admin`: Administrative commands (user management, token management)
Expand Down Expand Up @@ -90,6 +91,16 @@ go run . dataset download <dataset-name>
go run . dataset upload --new ./file.tar.gz
```

### Test package operations
```bash
go run . package search dataframes
go run . package search --verbose plots
go run . package search --limit 20 ml
go run . package search --registries General optimization
go run . package info DataFrames
go run . package info Plots --registries General
```

### Test registry operations
```bash
go run . registry list
Expand Down Expand Up @@ -187,6 +198,7 @@ The application uses OAuth2 device flow:
- **Dataset operations**: Use presigned URLs for upload/download
- **User management**: `/app/config/features/manage` endpoint for listing all users
- **Token management**: `/app/token/activelist` endpoint for listing all API tokens
- **Package search fallback**: `/packages/info` endpoint with `name`, `registries`, `tags`, `licenses`, `limit`, `offset` query params; returns `{packages: [...], meta: {total: N}}`
- **Authentication**: Bearer token with ID token
- **Upload workflow**: 3-step process (request presigned URL, upload to URL, close upload)

Expand Down Expand Up @@ -308,6 +320,12 @@ jh run setup
- Token list output is concise by default (Subject, Created By, and Expired status only); use `--verbose` flag for detailed information (signature, creation date, expiration date with estimate indicator)
- Token dates are formatted in human-readable format and converted to local timezone (respects system timezone or TZ environment variable)
- Token expiration estimate indicator only shown when `expires_at_is_estimate` is true in API response
- Package search (`jh package search`) and info (`jh package info`) both try GraphQL (`FilteredPackages` via `/v1/graphql`) first, then fall back to REST API (`/packages/info`) on failure; a warning is printed to stderr when the fallback is used
- REST fallback passes `--registries` as comma-separated registry names to the `registries` query param
- `fetchRegistries` in `registries.go` is used by `listRegistries`, `packageSearchCmd`, and `packageInfoCmd` to resolve registry names to IDs (for GraphQL) and names (for REST fallback)
- `executePackageQuery` in `packages.go` is the shared GraphQL helper used by both `searchPackages` and `getPackageInfo`
- `fetchPackagesREST` in `packages.go` is the shared REST helper used by both `searchPackages` and `getPackageInfo` as fallback
- `displayPackageDetails` renders GraphQL `Package` results; `displayRESTPackage` renders `RESTPackage` results from the REST fallback

## Implementation Details

Expand Down
34 changes: 33 additions & 1 deletion 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
- **Package Management**: Search and explore Julia packages across registries (GraphQL primary, REST fallback)
- **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
Expand Down Expand Up @@ -150,6 +151,17 @@ go build -o jh .
- `jh dataset upload [dataset-id] <file-path>` - Upload a dataset
- `jh dataset status <dataset-id> [version]` - Show dataset status

### Package Management (`jh package`)

- `jh package search [search-term]` - Search for Julia packages
- Default: Shows concise output with NAME, OWNER, VERSION, REGISTRIES, and DESCRIPTION columns
- `jh package search --verbose` - Show detailed package information including UUID, repository, tags, stars, docs, and license
- `--registries <names>` - Filter by registry names (comma-separated, e.g. `General,MyRegistry`)
- `--limit <n>` - Maximum results to return (default: 10)
- `--offset <n>` - Number of results to skip
- `jh package info <package-name>` - Get detailed information about a specific package (exact name match, case-insensitive)
- `jh package info --registries General` - Search in specific registries only

### Registry Management (`jh registry`)

- `jh registry list` - List all package registries on JuliaHub
Expand Down Expand Up @@ -234,6 +246,26 @@ jh dataset upload --new ./my-data.tar.gz
jh dataset upload my-dataset ./updated-data.tar.gz
```

### Package Operations

```bash
# Search for packages by name
jh package search dataframes

# Search with verbose output
jh package search --verbose plots

# Filter by registry
jh package search --registries General optimization

# Limit and paginate results
jh package search --limit 20 --offset 0 ml

# Get detailed info about a specific package
jh package info DataFrames
jh package info Plots --registries General
```

### Registry Operations

```bash
Expand Down Expand Up @@ -333,7 +365,7 @@ Note: Arguments after `--` are passed directly to Julia. The `jh run` command:

- **Built with Go** using the Cobra CLI framework
- **Authentication**: OAuth2 device flow with JWT token management
- **APIs**: REST API for datasets, GraphQL API for projects and user info
- **APIs**: REST API for datasets; GraphQL API for projects, user info, and package search (with REST fallback)
- **Git Integration**: Seamless authentication via HTTP headers or credential helper
- **Cross-platform**: Supports Windows, macOS, and Linux

Expand Down
178 changes: 177 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ 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)
package - Package search and exploration
registry - Registry management (list registries)
project - Project management (list, filter by user)
user - User information and profile
Expand Down Expand Up @@ -555,6 +556,173 @@ Displays:
},
}

var packageCmd = &cobra.Command{
Use: "package",
Short: "Package search commands",
Long: `Search and explore Julia packages on JuliaHub.

Packages are Julia libraries that provide reusable functionality. JuliaHub
hosts packages from multiple registries and provides comprehensive search
capabilities including filtering by tags, registries, and more.`,
}

var packageSearchCmd = &cobra.Command{
Use: "search [search-term]",
Short: "Search for packages",
Long: `Search for Julia packages on JuliaHub.

Displays package information including:
- Package name, owner, and UUID
- Version information
- Description and repository
- Tags and star count
- License information

Filtering options:
- Filter by registry using --registries flag (searches all registries by default)

Use --verbose flag for comprehensive output, or get a concise summary by default.`,
Example: " jh package search dataframes\n jh package search --verbose plots\n jh package search --limit 20 ml\n jh package search --registries General optimization",
Args: cobra.MaximumNArgs(1),
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)
}

search := ""
if len(args) > 0 {
search = args[0]
}

limit, _ := cmd.Flags().GetInt("limit")
offset, _ := cmd.Flags().GetInt("offset")
verbose, _ := cmd.Flags().GetBool("verbose")
registryNamesStr, _ := cmd.Flags().GetString("registries")

// Fetch all registries from the API
allRegistries, err := fetchRegistries(server)
if err != nil {
fmt.Printf("Failed to fetch registries: %v\n", err)
os.Exit(1)
}

// Determine which registry IDs and names to use
var registryIDs []int
var registryNames []string
if registryNamesStr != "" {
// Use only specified registries
requestedNames := strings.Split(registryNamesStr, ",")
for _, requestedName := range requestedNames {
requestedName = strings.TrimSpace(requestedName)
if requestedName == "" {
continue
}

// Find matching registry (case-insensitive)
found := false
for _, reg := range allRegistries {
if strings.EqualFold(reg.Name, requestedName) {
registryIDs = append(registryIDs, reg.RegistryID)
registryNames = append(registryNames, reg.Name)
found = true
break
}
}

if !found {
fmt.Printf("Registry not found: '%s'\n", requestedName)
os.Exit(1)
}
}
} else {
// Use all registries
for _, reg := range allRegistries {
registryIDs = append(registryIDs, reg.RegistryID)
registryNames = append(registryNames, reg.Name)
}
}

if err := searchPackages(server, search, limit, offset, registryIDs, registryNames, verbose); err != nil {
fmt.Printf("Failed to search packages: %v\n", err)
os.Exit(1)
}
},
}

var packageInfoCmd = &cobra.Command{
Use: "info <package-name>",
Short: "Get detailed information about a package",
Long: `Display detailed information about a specific Julia package by exact name match.

Shows comprehensive package information including:
- Package name, UUID, and owner
- Version information and status
- Description and repository
- Tags and star count
- License information
- Documentation links

The package name must match exactly (case-insensitive).`,
Example: " jh package info DataFrames\n jh package info Plots\n jh package info CSV",
Args: cobra.ExactArgs(1),
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)
}

packageName := args[0]
registryNamesStr, _ := cmd.Flags().GetString("registries")

// Fetch all registries from the API
allRegistries, err := fetchRegistries(server)
if err != nil {
fmt.Printf("Failed to fetch registries: %v\n", err)
os.Exit(1)
}

var registryIDs []int
var registryNames []string
if registryNamesStr != "" {
requestedNames := strings.Split(registryNamesStr, ",")
for _, requestedName := range requestedNames {
requestedName = strings.TrimSpace(requestedName)
if requestedName == "" {
continue
}

found := false
for _, reg := range allRegistries {
if strings.EqualFold(reg.Name, requestedName) {
registryIDs = append(registryIDs, reg.RegistryID)
registryNames = append(registryNames, reg.Name)
found = true
break
}
}

if !found {
fmt.Printf("Registry not found: '%s'\n", requestedName)
os.Exit(1)
}
}
} else {
for _, reg := range allRegistries {
registryIDs = append(registryIDs, reg.RegistryID)
registryNames = append(registryNames, reg.Name)
}
}

if err := getPackageInfo(server, packageName, registryIDs, registryNames); err != nil {
fmt.Printf("Failed to get package info: %v\n", err)
os.Exit(1)
}
},
}

var registryCmd = &cobra.Command{
Use: "registry",
Short: "Registry management commands",
Expand Down Expand Up @@ -1121,6 +1289,13 @@ 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")
packageSearchCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
packageSearchCmd.Flags().Int("limit", 10, "Maximum number of results to return")
packageSearchCmd.Flags().Int("offset", 0, "Number of results to skip")
packageSearchCmd.Flags().String("registries", "", "Filter by registry names (comma-separated, e.g., 'General,CustomRegistry')")
packageSearchCmd.Flags().Bool("verbose", false, "Show detailed package information")
packageInfoCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
packageInfoCmd.Flags().String("registries", "", "Filter by registry names (comma-separated, e.g., 'General,CustomRegistry')")
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")
Expand All @@ -1139,6 +1314,7 @@ func init() {
authCmd.AddCommand(authLoginCmd, authRefreshCmd, authStatusCmd, authEnvCmd)
jobCmd.AddCommand(jobListCmd, jobStartCmd)
datasetCmd.AddCommand(datasetListCmd, datasetDownloadCmd, datasetUploadCmd, datasetStatusCmd)
packageCmd.AddCommand(packageSearchCmd, packageInfoCmd)
registryCmd.AddCommand(registryListCmd)
projectCmd.AddCommand(projectListCmd)
userCmd.AddCommand(userInfoCmd)
Expand All @@ -1149,7 +1325,7 @@ func init() {
runCmd.AddCommand(runSetupCmd)
gitCredentialCmd.AddCommand(gitCredentialHelperCmd, gitCredentialGetCmd, gitCredentialStoreCmd, gitCredentialEraseCmd, gitCredentialSetupCmd)

rootCmd.AddCommand(authCmd, jobCmd, datasetCmd, projectCmd, registryCmd, userCmd, adminCmd, juliaCmd, cloneCmd, pushCmd, fetchCmd, pullCmd, runCmd, gitCredentialCmd, updateCmd)
rootCmd.AddCommand(authCmd, jobCmd, datasetCmd, projectCmd, packageCmd, registryCmd, userCmd, adminCmd, juliaCmd, cloneCmd, pushCmd, fetchCmd, pullCmd, runCmd, gitCredentialCmd, updateCmd)
}

func main() {
Expand Down
Loading