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
18 changes: 11 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/registry/registries/descriptions`), package search primary path (`/packages/info`), token management (`/app/token/activelist`) and user management (`/app/config/features/manage`)
- **GraphQL API**: Used for projects, user info, and package search fallback (`/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 primary path (`/packages/info`), token management (`/app/token/activelist`) and user management (`/app/config/features/manage`)
- **GraphQL API**: Used for projects, user info, and package search/info fallback (`/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 (REST primary via `/packages/info`, GraphQL fallback; supports filtering by registry)
- `jh project`: Project management (list with GraphQL, supports user filtering)
- `jh package`: Package search (REST primary via `/packages/info`, GraphQL fallback)
- `jh user`: User information (info with GraphQL)
Expand Down Expand Up @@ -91,12 +92,14 @@ go run . dataset download <dataset-name>
go run . dataset upload --new ./file.tar.gz
```

### Test package search operations
### 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
Expand Down Expand Up @@ -318,13 +321,14 @@ 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`) tries REST API (`/packages/info`) first, then falls back to GraphQL (`FilteredPackagesWithCount` via `/v1/graphql`) on failure; a warning is printed to stderr when the fallback is used
- Package search GraphQL fallback passes `--registries` as registry IDs to the `registries` variable
- `fetchRegistries` in `registries.go` is used by both `listRegistries` (for display) and `packageSearchCmd` (to resolve registry names to IDs for GraphQL and names for REST fallback)
- Both REST and GraphQL package search paths produce identical output columns (Registry and Owner); GraphQL resolves registry names from the `registryIDs`/`registryNames` already in `PackageSearchParams` — no extra API call needed
- Package search (`jh package search`) and info (`jh package info`) both try REST API (`/packages/info`) first, then fall back to GraphQL (`FilteredPackagesWithCount` via `/v1/graphql`) on failure; a warning is printed to stderr when the fallback is used
- REST API passes `--registries` as comma-separated registry names to the `registries` query param; GraphQL fallback passes registry IDs to the `registries` variable
- `fetchRegistries` in `registries.go` is used by `listRegistries`, `packageSearchCmd`, and `packageInfoCmd` to resolve registry names to IDs (for GraphQL) and names (for REST)
- Both REST and GraphQL package search/info paths produce identical output columns (Registry and Owner); GraphQL resolves registry names from the `registryIDs`/`registryNames` already in `PackageSearchParams` — no extra API call needed
- A package in multiple registries appears as multiple rows (one per registry) in both REST and GraphQL paths, since the GraphQL view (`package_rank_vw`) is already flattened per package-registry combination
- GraphQL fallback uses `package_search_with_count.gql` which fetches both the package list and aggregate count in a single request (`package_search` + `package_search_aggregate` root fields)
- `executeGraphQL(server, token, req)` in `packages.go` is a shared helper for GraphQL POST requests (sets Authorization, Content-Type, Accept, X-Hasura-Role headers)
- `getPackageInfo` in `packages.go` implements exact name-match lookup using REST-first (`getPackageInfoREST`), GraphQL fallback (`getPackageInfoGraphQL`); `packageInfoCmd` in `main.go` resolves registries via `fetchRegistries`

## Implementation Details

Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +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 Search**: Search Julia packages across registries via REST API (GraphQL fallback)
- **Package Management**: Search and explore Julia packages across registries via REST API (GraphQL 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 @@ -151,14 +151,16 @@ go build -o jh .
- `jh dataset upload [dataset-id] <file-path>` - Upload a dataset
- `jh dataset status <dataset-id> [version]` - Show dataset status

### Package Search (`jh package`)
### Package Management (`jh package`)

- `jh package search [search-term]` - Search for Julia packages
- Default: Shows Name, Registry, Owner, Version, and Description
- `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`)

Expand Down Expand Up @@ -244,7 +246,7 @@ jh dataset upload --new ./my-data.tar.gz
jh dataset upload my-dataset ./updated-data.tar.gz
```

### Package Search
### Package Operations

```bash
# Search for packages by name
Expand All @@ -258,6 +260,10 @@ 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
Expand Down Expand Up @@ -359,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 and package search (primary); GraphQL API for projects, user info, and package search fallback (single request returns results + total count)
- **APIs**: REST API for datasets and package search/info (primary); GraphQL API for projects, user info, and package search/info fallback (single request returns results + total count)
- **Git Integration**: Seamless authentication via HTTP headers or credential helper
- **Cross-platform**: Supports Windows, macOS, and Linux

Expand Down
76 changes: 75 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,78 @@ Use --verbose flag for comprehensive output, or get a concise summary by default
},
}

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 @@ -1230,6 +1302,8 @@ func init() {
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 @@ -1248,7 +1322,7 @@ func init() {
authCmd.AddCommand(authLoginCmd, authRefreshCmd, authStatusCmd, authEnvCmd)
jobCmd.AddCommand(jobListCmd, jobStartCmd)
datasetCmd.AddCommand(datasetListCmd, datasetDownloadCmd, datasetUploadCmd, datasetStatusCmd)
packageCmd.AddCommand(packageSearchCmd)
packageCmd.AddCommand(packageSearchCmd, packageInfoCmd)
registryCmd.AddCommand(registryListCmd)
projectCmd.AddCommand(projectListCmd)
userCmd.AddCommand(userInfoCmd)
Expand Down
62 changes: 62 additions & 0 deletions package_search.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
query FilteredPackages(
$search: String
$limit: Int
$offset: Int
$matchtags: _text
$registries: _int8
$hasfailures: Boolean
$installed: Boolean
$notinstalled: Boolean
$licenses: _text
$order: [package_rank_vw_order_by!]
$filter: package_rank_vw_bool_exp = {}
) {
package_search(
args: {
search: $search
matchtags: $matchtags
licenses: $licenses
isinstalled: $installed
notinstalled: $notinstalled
hasfailures: $hasfailures
registrylist: $registries
}
order_by: $order
limit: $limit
offset: $offset
where: { _and: [{ fit: { _gte: 1 } }, $filter] }
) {
name
owner
slug
license
isapp
score
registrymap {
version
registryid
status
isapp
isjsml
__typename
}
metadata {
docshosteduri
versions
description
docslink
repo
owner
tags
starcount
__typename
}
uuid
installed
failures {
package_version
__typename
}
__typename
}
}
Loading
Loading