Skip to content

Commit 5c071f2

Browse files
feat: package info cmd (#22)
* registry list cmd * added verbose for detailed view * package search cmd with flags * fixed gql query issues * refactoring * display refactoring * refactoring * display registries the package belongs to * added openapi endpoints for package search, with fallback logic * refactorings * refactorings * fixed error * fixed error * refactorings
1 parent 24db724 commit 5c071f2

File tree

5 files changed

+343
-126
lines changed

5 files changed

+343
-126
lines changed

CLAUDE.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,16 @@ 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/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`)
34+
- **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`)
35+
- **GraphQL API**: Used for projects, user info, and package search/info 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

3939
3. **Command Structure**:
4040
- `jh auth`: Authentication commands (login, refresh, status, env)
4141
- `jh dataset`: Dataset operations (list, download, upload, status)
4242
- `jh registry`: Registry operations (list with REST API, supports verbose mode)
43+
- `jh package`: Package search and info (REST primary via `/packages/info`, GraphQL fallback; supports filtering by registry)
4344
- `jh project`: Project management (list with GraphQL, supports user filtering)
4445
- `jh package`: Package search (REST primary via `/packages/info`, GraphQL fallback)
4546
- `jh user`: User information (info with GraphQL)
@@ -91,12 +92,14 @@ go run . dataset download <dataset-name>
9192
go run . dataset upload --new ./file.tar.gz
9293
```
9394

94-
### Test package search operations
95+
### Test package operations
9596
```bash
9697
go run . package search dataframes
9798
go run . package search --verbose plots
9899
go run . package search --limit 20 ml
99100
go run . package search --registries General optimization
101+
go run . package info DataFrames
102+
go run . package info Plots --registries General
100103
```
101104

102105
### Test registry operations
@@ -318,13 +321,14 @@ jh run setup
318321
- 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)
319322
- Token dates are formatted in human-readable format and converted to local timezone (respects system timezone or TZ environment variable)
320323
- 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
324+
- 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
325+
- REST API passes `--registries` as comma-separated registry names to the `registries` query param; GraphQL fallback passes registry IDs to the `registries` variable
326+
- `fetchRegistries` in `registries.go` is used by `listRegistries`, `packageSearchCmd`, and `packageInfoCmd` to resolve registry names to IDs (for GraphQL) and names (for REST)
327+
- 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
325328
- 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
326329
- 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)
327330
- `executeGraphQL(server, token, req)` in `packages.go` is a shared helper for GraphQL POST requests (sets Authorization, Content-Type, Accept, X-Hasura-Role headers)
331+
- `getPackageInfo` in `packages.go` implements exact name-match lookup using REST-first (`getPackageInfoREST`), GraphQL fallback (`getPackageInfoGraphQL`); `packageInfoCmd` in `main.go` resolves registries via `fetchRegistries`
328332

329333
## Implementation Details
330334

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +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)
9+
- **Package Management**: Search and explore Julia packages across registries via REST API (GraphQL fallback)
1010
- **Registry Management**: List and manage Julia package registries
1111
- **Project Management**: List and filter projects using GraphQL API
1212
- **Git Integration**: Clone, push, fetch, and pull with automatic JuliaHub authentication
@@ -151,14 +151,16 @@ go build -o jh .
151151
- `jh dataset upload [dataset-id] <file-path>` - Upload a dataset
152152
- `jh dataset status <dataset-id> [version]` - Show dataset status
153153

154-
### Package Search (`jh package`)
154+
### Package Management (`jh package`)
155155

156156
- `jh package search [search-term]` - Search for Julia packages
157157
- Default: Shows Name, Registry, Owner, Version, and Description
158158
- `jh package search --verbose` - Show detailed package information including UUID, repository, tags, stars, docs, and license
159159
- `--registries <names>` - Filter by registry names (comma-separated, e.g. `General,MyRegistry`)
160160
- `--limit <n>` - Maximum results to return (default: 10)
161161
- `--offset <n>` - Number of results to skip
162+
- `jh package info <package-name>` - Get detailed information about a specific package (exact name match, case-insensitive)
163+
- `jh package info --registries General` - Search in specific registries only
162164

163165
### Registry Management (`jh registry`)
164166

@@ -244,7 +246,7 @@ jh dataset upload --new ./my-data.tar.gz
244246
jh dataset upload my-dataset ./updated-data.tar.gz
245247
```
246248

247-
### Package Search
249+
### Package Operations
248250

249251
```bash
250252
# Search for packages by name
@@ -258,6 +260,10 @@ jh package search --registries General optimization
258260

259261
# Limit and paginate results
260262
jh package search --limit 20 --offset 0 ml
263+
264+
# Get detailed info about a specific package
265+
jh package info DataFrames
266+
jh package info Plots --registries General
261267
```
262268

263269
### Registry Operations
@@ -359,7 +365,7 @@ Note: Arguments after `--` are passed directly to Julia. The `jh run` command:
359365

360366
- **Built with Go** using the Cobra CLI framework
361367
- **Authentication**: OAuth2 device flow with JWT token management
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)
368+
- **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)
363369
- **Git Integration**: Seamless authentication via HTTP headers or credential helper
364370
- **Cross-platform**: Supports Windows, macOS, and Linux
365371

main.go

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,78 @@ Use --verbose flag for comprehensive output, or get a concise summary by default
659659
},
660660
}
661661

662+
var packageInfoCmd = &cobra.Command{
663+
Use: "info <package-name>",
664+
Short: "Get detailed information about a package",
665+
Long: `Display detailed information about a specific Julia package by exact name match.
666+
667+
Shows comprehensive package information including:
668+
- Package name, UUID, and owner
669+
- Version information and status
670+
- Description and repository
671+
- Tags and star count
672+
- License information
673+
- Documentation links
674+
675+
The package name must match exactly (case-insensitive).`,
676+
Example: " jh package info DataFrames\n jh package info Plots\n jh package info CSV",
677+
Args: cobra.ExactArgs(1),
678+
Run: func(cmd *cobra.Command, args []string) {
679+
server, err := getServerFromFlagOrConfig(cmd)
680+
if err != nil {
681+
fmt.Printf("Failed to get server config: %v\n", err)
682+
os.Exit(1)
683+
}
684+
685+
packageName := args[0]
686+
registryNamesStr, _ := cmd.Flags().GetString("registries")
687+
688+
// Fetch all registries from the API
689+
allRegistries, err := fetchRegistries(server)
690+
if err != nil {
691+
fmt.Printf("Failed to fetch registries: %v\n", err)
692+
os.Exit(1)
693+
}
694+
695+
var registryIDs []int
696+
var registryNames []string
697+
if registryNamesStr != "" {
698+
requestedNames := strings.Split(registryNamesStr, ",")
699+
for _, requestedName := range requestedNames {
700+
requestedName = strings.TrimSpace(requestedName)
701+
if requestedName == "" {
702+
continue
703+
}
704+
705+
found := false
706+
for _, reg := range allRegistries {
707+
if strings.EqualFold(reg.Name, requestedName) {
708+
registryIDs = append(registryIDs, reg.RegistryID)
709+
registryNames = append(registryNames, reg.Name)
710+
found = true
711+
break
712+
}
713+
}
714+
715+
if !found {
716+
fmt.Printf("Registry not found: '%s'\n", requestedName)
717+
os.Exit(1)
718+
}
719+
}
720+
} else {
721+
for _, reg := range allRegistries {
722+
registryIDs = append(registryIDs, reg.RegistryID)
723+
registryNames = append(registryNames, reg.Name)
724+
}
725+
}
726+
727+
if err := getPackageInfo(server, packageName, registryIDs, registryNames); err != nil {
728+
fmt.Printf("Failed to get package info: %v\n", err)
729+
os.Exit(1)
730+
}
731+
},
732+
}
733+
662734
var registryCmd = &cobra.Command{
663735
Use: "registry",
664736
Short: "Registry management commands",
@@ -1230,6 +1302,8 @@ func init() {
12301302
packageSearchCmd.Flags().Int("offset", 0, "Number of results to skip")
12311303
packageSearchCmd.Flags().String("registries", "", "Filter by registry names (comma-separated, e.g., 'General,CustomRegistry')")
12321304
packageSearchCmd.Flags().Bool("verbose", false, "Show detailed package information")
1305+
packageInfoCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
1306+
packageInfoCmd.Flags().String("registries", "", "Filter by registry names (comma-separated, e.g., 'General,CustomRegistry')")
12331307
registryListCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
12341308
registryListCmd.Flags().Bool("verbose", false, "Show detailed registry information")
12351309
projectListCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
@@ -1248,7 +1322,7 @@ func init() {
12481322
authCmd.AddCommand(authLoginCmd, authRefreshCmd, authStatusCmd, authEnvCmd)
12491323
jobCmd.AddCommand(jobListCmd, jobStartCmd)
12501324
datasetCmd.AddCommand(datasetListCmd, datasetDownloadCmd, datasetUploadCmd, datasetStatusCmd)
1251-
packageCmd.AddCommand(packageSearchCmd)
1325+
packageCmd.AddCommand(packageSearchCmd, packageInfoCmd)
12521326
registryCmd.AddCommand(registryListCmd)
12531327
projectCmd.AddCommand(projectListCmd)
12541328
userCmd.AddCommand(userInfoCmd)

package_search.gql

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
query FilteredPackages(
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+
}

0 commit comments

Comments
 (0)