Skip to content

Commit 24db724

Browse files
feat: package search cmd with version info (#21)
* registry list cmd * added verbose for detailed view * package search cmd with flags * fixed gql query issues * refactoring * display refactoring * added openapi endpoints for package search, with fallback logic * refactorings * refactorings * print package search with total count, further refactorings
1 parent 598769b commit 24db724

File tree

6 files changed

+658
-13
lines changed

6 files changed

+658
-13
lines changed

CLAUDE.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
44

55
## Project Overview
6-
76
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.
87

98
## Architecture
@@ -13,7 +12,8 @@ The application follows a command-line interface pattern using the Cobra library
1312
- **main.go**: Core CLI structure with command definitions and configuration management
1413
- **auth.go**: OAuth2 device flow authentication with JWT token handling
1514
- **datasets.go**: Dataset operations (list, download, upload, status) with REST API integration
16-
- **registries.go**: Registry operations (list) with REST API integration
15+
- **registries.go**: Registry operations (list, fetch) with REST API integration
16+
- **packages.go**: Package search with REST API primary path (`/packages/info`) and GraphQL fallback
1717
- **projects.go**: Project management using GraphQL API with user filtering
1818
- **user.go**: User information retrieval using GraphQL API and REST API for listing users
1919
- **tokens.go**: Token management operations (list) with REST API integration
@@ -31,8 +31,8 @@ The application follows a command-line interface pattern using the Cobra library
3131
- Stores tokens securely in `~/.juliahub` with 0600 permissions
3232

3333
2. **API Integration**:
34-
- **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`)
35-
- **GraphQL API**: Used for projects and user info (`/v1/graphql`)
34+
- **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`)
35+
- **GraphQL API**: Used for projects, user info, and package search fallback (`/v1/graphql`)
3636
- **Headers**: All GraphQL requests require `X-Hasura-Role: jhuser` header
3737
- **Authentication**: Uses ID tokens (`token.IDToken`) for API calls
3838

@@ -41,6 +41,7 @@ The application follows a command-line interface pattern using the Cobra library
4141
- `jh dataset`: Dataset operations (list, download, upload, status)
4242
- `jh registry`: Registry operations (list with REST API, supports verbose mode)
4343
- `jh project`: Project management (list with GraphQL, supports user filtering)
44+
- `jh package`: Package search (REST primary via `/packages/info`, GraphQL fallback)
4445
- `jh user`: User information (info with GraphQL)
4546
- `jh admin`: Administrative commands (user management, token management)
4647
- `jh admin user`: User management (list all users with REST API, supports verbose mode)
@@ -90,6 +91,14 @@ go run . dataset download <dataset-name>
9091
go run . dataset upload --new ./file.tar.gz
9192
```
9293

94+
### Test package search operations
95+
```bash
96+
go run . package search dataframes
97+
go run . package search --verbose plots
98+
go run . package search --limit 20 ml
99+
go run . package search --registries General optimization
100+
```
101+
93102
### Test registry operations
94103
```bash
95104
go run . registry list
@@ -187,6 +196,7 @@ The application uses OAuth2 device flow:
187196
- **Dataset operations**: Use presigned URLs for upload/download
188197
- **User management**: `/app/config/features/manage` endpoint for listing all users
189198
- **Token management**: `/app/token/activelist` endpoint for listing all API tokens
199+
- **Package search primary**: `/packages/info` endpoint with `name`, `registries`, `tags`, `licenses`, `limit`, `offset` query params; returns `{packages: [...], meta: {total: N}}`
190200
- **Authentication**: Bearer token with ID token
191201
- **Upload workflow**: 3-step process (request presigned URL, upload to URL, close upload)
192202

@@ -308,6 +318,13 @@ jh run setup
308318
- 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)
309319
- Token dates are formatted in human-readable format and converted to local timezone (respects system timezone or TZ environment variable)
310320
- Token expiration estimate indicator only shown when `expires_at_is_estimate` is true in API response
321+
- 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
322+
- Package search GraphQL fallback passes `--registries` as registry IDs to the `registries` variable
323+
- `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)
324+
- 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
325+
- 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
326+
- 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)
327+
- `executeGraphQL(server, token, req)` in `packages.go` is a shared helper for GraphQL POST requests (sets Authorization, Content-Type, Accept, X-Hasura-Role headers)
311328

312329
## Implementation Details
313330

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ A command-line interface for interacting with JuliaHub, a platform for Julia com
66

77
- **Authentication**: OAuth2 device flow authentication with JWT token handling
88
- **Dataset Management**: List, download, upload, and check status of datasets
9+
- **Package Search**: Search Julia packages across registries via REST API (GraphQL fallback)
910
- **Registry Management**: List and manage Julia package registries
1011
- **Project Management**: List and filter projects using GraphQL API
1112
- **Git Integration**: Clone, push, fetch, and pull with automatic JuliaHub authentication
@@ -150,6 +151,15 @@ go build -o jh .
150151
- `jh dataset upload [dataset-id] <file-path>` - Upload a dataset
151152
- `jh dataset status <dataset-id> [version]` - Show dataset status
152153

154+
### Package Search (`jh package`)
155+
156+
- `jh package search [search-term]` - Search for Julia packages
157+
- Default: Shows Name, Registry, Owner, Version, and Description
158+
- `jh package search --verbose` - Show detailed package information including UUID, repository, tags, stars, docs, and license
159+
- `--registries <names>` - Filter by registry names (comma-separated, e.g. `General,MyRegistry`)
160+
- `--limit <n>` - Maximum results to return (default: 10)
161+
- `--offset <n>` - Number of results to skip
162+
153163
### Registry Management (`jh registry`)
154164

155165
- `jh registry list` - List all package registries on JuliaHub
@@ -234,6 +244,22 @@ jh dataset upload --new ./my-data.tar.gz
234244
jh dataset upload my-dataset ./updated-data.tar.gz
235245
```
236246

247+
### Package Search
248+
249+
```bash
250+
# Search for packages by name
251+
jh package search dataframes
252+
253+
# Search with verbose output
254+
jh package search --verbose plots
255+
256+
# Filter by registry
257+
jh package search --registries General optimization
258+
259+
# Limit and paginate results
260+
jh package search --limit 20 --offset 0 ml
261+
```
262+
237263
### Registry Operations
238264

239265
```bash
@@ -333,7 +359,7 @@ Note: Arguments after `--` are passed directly to Julia. The `jh run` command:
333359

334360
- **Built with Go** using the Cobra CLI framework
335361
- **Authentication**: OAuth2 device flow with JWT token management
336-
- **APIs**: REST API for datasets, GraphQL API for projects and user info
362+
- **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)
337363
- **Git Integration**: Seamless authentication via HTTP headers or credential helper
338364
- **Cross-platform**: Supports Windows, macOS, and Linux
339365

main.go

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ job execution, project management, Git integration, and package hosting capabili
164164
Available command categories:
165165
auth - Authentication and token management
166166
dataset - Dataset operations (list, download, upload, status)
167+
package - Package search and exploration
167168
registry - Registry management (list registries)
168169
project - Project management (list, filter by user)
169170
user - User information and profile
@@ -555,6 +556,109 @@ Displays:
555556
},
556557
}
557558

559+
var packageCmd = &cobra.Command{
560+
Use: "package",
561+
Short: "Package search commands",
562+
Long: `Search and explore Julia packages on JuliaHub.
563+
564+
Packages are Julia libraries that provide reusable functionality. JuliaHub
565+
hosts packages from multiple registries and provides comprehensive search
566+
capabilities including filtering by tags, registries, and more.`,
567+
}
568+
569+
var packageSearchCmd = &cobra.Command{
570+
Use: "search [search-term]",
571+
Short: "Search for packages",
572+
Long: `Search for Julia packages on JuliaHub.
573+
574+
Displays package information including:
575+
- Package name, owner, and UUID
576+
- Version information
577+
- Description and repository
578+
- Tags and star count
579+
- License information
580+
581+
Filtering options:
582+
- Filter by registry using --registries flag (searches all registries by default)
583+
584+
Use --verbose flag for comprehensive output, or get a concise summary by default.`,
585+
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",
586+
Args: cobra.MaximumNArgs(1),
587+
Run: func(cmd *cobra.Command, args []string) {
588+
server, err := getServerFromFlagOrConfig(cmd)
589+
if err != nil {
590+
fmt.Printf("Failed to get server config: %v\n", err)
591+
os.Exit(1)
592+
}
593+
594+
search := ""
595+
if len(args) > 0 {
596+
search = args[0]
597+
}
598+
599+
limit, _ := cmd.Flags().GetInt("limit")
600+
offset, _ := cmd.Flags().GetInt("offset")
601+
verbose, _ := cmd.Flags().GetBool("verbose")
602+
registryNamesStr, _ := cmd.Flags().GetString("registries")
603+
604+
// Fetch all registries from the API
605+
allRegistries, err := fetchRegistries(server)
606+
if err != nil {
607+
fmt.Printf("Failed to fetch registries: %v\n", err)
608+
os.Exit(1)
609+
}
610+
611+
// Determine which registry IDs and names to use
612+
var registryIDs []int
613+
var registryNames []string
614+
if registryNamesStr != "" {
615+
// Use only specified registries
616+
requestedNames := strings.Split(registryNamesStr, ",")
617+
for _, requestedName := range requestedNames {
618+
requestedName = strings.TrimSpace(requestedName)
619+
if requestedName == "" {
620+
continue
621+
}
622+
623+
// Find matching registry (case-insensitive)
624+
found := false
625+
for _, reg := range allRegistries {
626+
if strings.EqualFold(reg.Name, requestedName) {
627+
registryIDs = append(registryIDs, reg.RegistryID)
628+
registryNames = append(registryNames, reg.Name)
629+
found = true
630+
break
631+
}
632+
}
633+
634+
if !found {
635+
fmt.Printf("Registry not found: '%s'\n", requestedName)
636+
os.Exit(1)
637+
}
638+
}
639+
} else {
640+
// Use all registries
641+
for _, reg := range allRegistries {
642+
registryIDs = append(registryIDs, reg.RegistryID)
643+
registryNames = append(registryNames, reg.Name)
644+
}
645+
}
646+
647+
if err := searchPackages(PackageSearchParams{
648+
Server: server,
649+
Search: search,
650+
Limit: limit,
651+
Offset: offset,
652+
RegistryIDs: registryIDs,
653+
RegistryNames: registryNames,
654+
Verbose: verbose,
655+
}); err != nil {
656+
fmt.Printf("Failed to search packages: %v\n", err)
657+
os.Exit(1)
658+
}
659+
},
660+
}
661+
558662
var registryCmd = &cobra.Command{
559663
Use: "registry",
560664
Short: "Registry management commands",
@@ -1121,6 +1225,11 @@ func init() {
11211225
datasetUploadCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
11221226
datasetUploadCmd.Flags().Bool("new", false, "Create a new dataset")
11231227
datasetStatusCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
1228+
packageSearchCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
1229+
packageSearchCmd.Flags().Int("limit", 10, "Maximum number of results to return")
1230+
packageSearchCmd.Flags().Int("offset", 0, "Number of results to skip")
1231+
packageSearchCmd.Flags().String("registries", "", "Filter by registry names (comma-separated, e.g., 'General,CustomRegistry')")
1232+
packageSearchCmd.Flags().Bool("verbose", false, "Show detailed package information")
11241233
registryListCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
11251234
registryListCmd.Flags().Bool("verbose", false, "Show detailed registry information")
11261235
projectListCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
@@ -1139,6 +1248,7 @@ func init() {
11391248
authCmd.AddCommand(authLoginCmd, authRefreshCmd, authStatusCmd, authEnvCmd)
11401249
jobCmd.AddCommand(jobListCmd, jobStartCmd)
11411250
datasetCmd.AddCommand(datasetListCmd, datasetDownloadCmd, datasetUploadCmd, datasetStatusCmd)
1251+
packageCmd.AddCommand(packageSearchCmd)
11421252
registryCmd.AddCommand(registryListCmd)
11431253
projectCmd.AddCommand(projectListCmd)
11441254
userCmd.AddCommand(userInfoCmd)
@@ -1149,7 +1259,7 @@ func init() {
11491259
runCmd.AddCommand(runSetupCmd)
11501260
gitCredentialCmd.AddCommand(gitCredentialHelperCmd, gitCredentialGetCmd, gitCredentialStoreCmd, gitCredentialEraseCmd, gitCredentialSetupCmd)
11511261

1152-
rootCmd.AddCommand(authCmd, jobCmd, datasetCmd, projectCmd, registryCmd, userCmd, adminCmd, juliaCmd, cloneCmd, pushCmd, fetchCmd, pullCmd, runCmd, gitCredentialCmd, updateCmd)
1262+
rootCmd.AddCommand(authCmd, jobCmd, datasetCmd, projectCmd, packageCmd, registryCmd, userCmd, adminCmd, juliaCmd, cloneCmd, pushCmd, fetchCmd, pullCmd, runCmd, gitCredentialCmd, updateCmd)
11531263
}
11541264

11551265
func main() {

package_search_with_count.gql

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
query FilteredPackagesWithCount(
2+
$search: String
3+
$limit: Int
4+
$offset: Int
5+
$matchtags: _text
6+
$registries: _int8
7+
$hasfailures: Boolean
8+
$installed: Boolean
9+
$notinstalled: Boolean
10+
$licenses: _text
11+
$order: [package_rank_vw_order_by!]
12+
$filter: package_rank_vw_bool_exp = {}
13+
) {
14+
package_search(
15+
args: {
16+
search: $search
17+
matchtags: $matchtags
18+
licenses: $licenses
19+
isinstalled: $installed
20+
notinstalled: $notinstalled
21+
hasfailures: $hasfailures
22+
registrylist: $registries
23+
}
24+
order_by: $order
25+
limit: $limit
26+
offset: $offset
27+
where: { _and: [{ fit: { _gte: 1 } }, $filter] }
28+
) {
29+
name
30+
owner
31+
slug
32+
license
33+
isapp
34+
score
35+
registrymap {
36+
version
37+
registryid
38+
status
39+
isapp
40+
isjsml
41+
__typename
42+
}
43+
metadata {
44+
docshosteduri
45+
versions
46+
description
47+
docslink
48+
repo
49+
owner
50+
tags
51+
starcount
52+
__typename
53+
}
54+
uuid
55+
installed
56+
failures {
57+
package_version
58+
__typename
59+
}
60+
__typename
61+
}
62+
package_search_aggregate(
63+
args: {
64+
search: $search
65+
matchtags: $matchtags
66+
licenses: $licenses
67+
isinstalled: $installed
68+
notinstalled: $notinstalled
69+
hasfailures: $hasfailures
70+
registrylist: $registries
71+
}
72+
where: { _and: [{ fit: { _gte: 1 } }, $filter] }
73+
) {
74+
aggregate {
75+
count
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)