Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
60 changes: 52 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
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.

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

## Architecture

Expand All @@ -13,7 +14,7 @@ The application follows a command-line interface pattern using the Cobra library
- **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, config, add, update) with REST API integration
- **packages.go**: Package search with REST API primary path (`/packages/info`) and GraphQL fallback
- **packages.go**: Package operations (search, dependency) with REST API primary path (`/packages/info`), GraphQL fallback, and documentation API (`/docs/{registry}/{package}/stable/pkg.json`)
- **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 Down Expand Up @@ -42,9 +43,8 @@ The application follows a command-line interface pattern using the Cobra library
- `jh dataset`: Dataset operations (list, download, upload, status)
- `jh registry`: Registry operations (list, config — all via REST API)
- `jh registry config`: Show registry JSON config by name; subcommands add/update accept JSON via stdin or `--file`
- `jh package`: Package search and info (REST primary via `/packages/info`, GraphQL fallback; supports filtering by registry)
- `jh package`: Package search and dependency (REST primary via `/packages/info`, GraphQL fallback; dependency data from `/docs/{registry}/{package}/stable/pkg.json`)
- `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)
- `jh admin`: Administrative commands (user management, token management, landing page)
- `jh admin user`: User management (list all users with REST API, supports verbose mode)
Expand Down Expand Up @@ -103,6 +103,12 @@ 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

# Get package dependencies
go run . package dependency DataFrames
go run . package dependency DataFrames --indirect
go run . package dependency DataFrames --all --indirect
go run . package dependency CSV --registry General
```

### Test registry operations
Expand Down Expand Up @@ -230,9 +236,11 @@ The application uses OAuth2 device flow:

### REST API Integration
- **Dataset operations**: Use presigned URLs for upload/download
- **Registry operations**: `/api/v1/registry/registries/descriptions` for listing registries
- **User management**: `/app/config/features/manage` endpoint for listing all users
- **Token management**: `/app/token/activelist` endpoint for listing all API tokens
- **Package search primary**: `/packages/info` endpoint with `name`, `registries`, `tags`, `licenses`, `limit`, `offset` query params; returns `{packages: [...], meta: {total: N}}`
- **Package search/info primary**: `/packages/info` endpoint with `name`, `registries`, `tags`, `licenses`, `limit`, `offset` query params; returns `{packages: [...], meta: {total: N}}`
- **Package dependencies**: `/docs/{registry}/{package}/stable/pkg.json` for dependency information
- **Authentication**: Bearer token with ID token
- **Upload workflow**: 3-step process (request presigned URL, upload to URL, close upload)

Expand Down Expand Up @@ -286,6 +294,41 @@ git clone https://github.com/user/repo.git # Ignored by
- **Token management**: Stores and refreshes tokens per server automatically
- **Error handling**: Graceful fallback to other credential helpers for non-JuliaHub URLs

## Package Management

The CLI provides comprehensive package discovery and dependency analysis:

### Package Search and Info
- **Search**: `jh package search` uses GraphQL API to search packages across registries
- **Info**: `jh package info` retrieves detailed package metadata
- **Filtering**: Supports filtering by registry, installation status, and failures

### Package Dependency (`jh package dependency`)
- **Endpoint**: Uses package documentation API at `/docs/{registry}/{package}/stable/pkg.json`
- **Registry resolution**: Automatically uses first registry package belongs to, or specific registry via `--registry` flag
- **Dependency types**: Distinguishes between direct and indirect dependencies via `direct` field in API response
- **Display limits**:
- Default: Shows up to 10 direct dependencies
- With `--indirect`: Shows up to 10 direct and 50 indirect dependencies
- With `--all`: Shows all dependencies without limits
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"all" means "direct" could be confusing for the users?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you elaborate, didnt get what you mean

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was bit confused about "all" which I thought implies "direct" and "indirect" but here it implies there is no pagination? Can we call it "--no-limit"? Maybe we could have a larger discussion on this on a call

- **Output format**:
- Direct-only mode: Single table with columns: NAME, REGISTRY, UUID, VERSIONS
- Indirect mode: Separate sections for direct and indirect dependencies with columns: NAME, REGISTRY, UUID, VERSIONS
- Registry column shows which registry each dependency belongs to (empty for stdlib packages)

#### Implementation Details (`packages.go`)
- `getPackageDependencies()`: Main function for dependency retrieval
1. Fetches all registries to get registry IDs for GraphQL query
2. Searches for package using GraphQL to get registry information
3. Determines target registry (first registry or user-specified)
4. Fetches package documentation JSON from docs endpoint
5. Filters and limits dependencies based on flags
6. Displays results in formatted tables with separate sections

#### Data Structures
- `PackageDependency`: Represents a single dependency with fields for direct/indirect status, name, UUID, versions, registry, and slug
- `PackageDocsResponse`: Response from documentation API containing package metadata and dependencies array

## Julia Integration

The CLI provides Julia installation and execution with JuliaHub configuration:
Expand Down Expand Up @@ -362,14 +405,15 @@ jh run setup
- Landing page commands (`jh admin landing-page`) use REST API: GET `/app/homepage` (show), POST `/app/config/homepage` (update), DELETE `/app/config/homepage` (remove); require appropriate permissions
- Landing page `update` command accepts content inline as an argument, from a file via `--file`, or piped via stdin (priority: `--file` > arg > stdin)
- Landing page response uses custom JSON unmarshaling (`homepageResponse`) to handle `message` being either an object or a string
- 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
- Package search (`jh package search`) and info (`jh package info`) both try REST API (`/packages/info`) first, then fall back to GraphQL (`FilteredPackages` / `FilteredPackagesCount` 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)
- `fetchRegistries` in `registries.go` is used by `listRegistries`, `packageSearchCmd`, `packageInfoCmd`, and `packageDependencyCmd` 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)
- GraphQL fallback uses `package_search.gql` (`FilteredPackages`) for the package list and `package_search_count.gql` (`FilteredPackagesCount`) for the aggregate count as separate requests
- `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`
- `getPackageDependencies` uses GraphQL (`fetchGraphQLPackages`) to locate the package, then fetches `/docs/{registry}/{package}/stable/pkg.json` for dependency data; no REST fallback (docs endpoint is authoritative)

## Implementation Details

Expand Down
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ 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
<<<<<<< HEAD
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unresolved conflict

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

- **Package Management**: Search and explore Julia packages across registries via REST API (GraphQL fallback), with dependency analysis
- **Registry Management**: List and manage Julia package registries
=======
- **Package Management**: Search and explore Julia packages across registries via REST API (GraphQL fallback)
- **Registry Management**: List, add, and update Julia package registries
>>>>>>> 4c422aed3b5e0e488e6e479143bd5bec64385463
- **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 @@ -171,6 +176,13 @@ go build -o jh .
- `jh registry config add` - Add a new registry (JSON payload via stdin or `--file`)
- `jh registry config update` - Update an existing registry (same JSON schema as add, same flags)

- `jh package dependency <package-name>` - List package dependencies
- Default: Shows up to 10 direct dependencies (NAME, REGISTRY, UUID, VERSIONS)
- `jh package dependency --indirect` - Include indirect dependencies (up to 10 direct, 50 indirect)
- `jh package dependency --all` - Show all dependencies without limits
- `jh package dependency --registry General` - Specify registry to use


### Project Management (`jh project`)

- `jh project list` - List all accessible projects
Expand Down Expand Up @@ -274,6 +286,12 @@ 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

# List package dependencies
jh package dependency DataFrames # Shows up to 10 direct dependencies
jh package dependency DataFrames --indirect # Includes indirect dependencies
jh package dependency DataFrames --all --indirect # Shows all dependencies
jh package dependency CSV --registry General # Use specific registry
```

### Registry Operations
Expand Down Expand Up @@ -419,7 +437,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/info (primary); GraphQL API for projects, user info, and package search/info fallback (single request returns results + total count)
- **APIs**: REST API for datasets and package search/info (primary); GraphQL API for projects, user info, package search/info fallback, and package dependency lookup
- **Git Integration**: Seamless authentication via HTTP headers or credential helper
- **Cross-platform**: Supports Windows, macOS, and Linux

Expand Down
37 changes: 36 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,38 @@ The package name must match exactly (case-insensitive).`,
},
}

var packageDependencyCmd = &cobra.Command{
Use: "dependency <package-name>",
Short: "List package dependencies",
Long: `List dependencies for a specific Julia package.

By default, shows all direct dependencies. Use --indirect flag to include
both direct and indirect dependencies.

The command fetches dependency information from the package documentation
JSON endpoint. If a package exists in multiple registries, it uses the
first registry by default. You can specify a different registry using
the --registry flag.`,
Example: " jh package dependency DataFrames\n jh package dependency --indirect Plots\n jh package dependency --registry General 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]
registryName, _ := cmd.Flags().GetString("registry")
showIndirect, _ := cmd.Flags().GetBool("indirect")

if err := getPackageDependencies(server, packageName, registryName, showIndirect); err != nil {
fmt.Printf("Failed to get package dependencies: %v\n", err)
os.Exit(1)
}
},
}

var registryCmd = &cobra.Command{
Use: "registry",
Short: "Registry management commands",
Expand Down Expand Up @@ -1618,6 +1650,9 @@ func init() {
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')")
packageDependencyCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
packageDependencyCmd.Flags().String("registry", "", "Specify registry name (uses first registry if not specified)")
packageDependencyCmd.Flags().Bool("indirect", false, "Include indirect dependencies")
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 @@ -1640,7 +1675,7 @@ func init() {
authCmd.AddCommand(authLoginCmd, authRefreshCmd, authStatusCmd, authEnvCmd)
jobCmd.AddCommand(jobListCmd, jobStartCmd)
datasetCmd.AddCommand(datasetListCmd, datasetDownloadCmd, datasetUploadCmd, datasetStatusCmd)
packageCmd.AddCommand(packageSearchCmd, packageInfoCmd)
packageCmd.AddCommand(packageSearchCmd, packageInfoCmd, packageDependencyCmd)
registryConfigCmd.Flags().StringP("server", "s", "", "JuliaHub server")
registryConfigAddCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
registryConfigAddCmd.Flags().StringP("file", "f", "", "Path to JSON config file (reads from stdin if omitted)")
Expand Down
27 changes: 27 additions & 0 deletions package_search_count.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
query FilteredPackagesCounts(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes supposed to be part of this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really, but it was last of the package prs and the query needed to be split, so i added it here only. if you want i can make a separate one

Copy link
Member

@vdayanand vdayanand Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer if it was a separate one

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, can you signoff this PR first then #31

$search: String
$matchtags: _text
$registries: _int8
$hasfailures: Boolean
$installed: Boolean
$notinstalled: Boolean
$licenses: _text
$filter: package_rank_vw_bool_exp = {}
) {
package_search_aggregate(
args: {
search: $search
matchtags: $matchtags
licenses: $licenses
isinstalled: $installed
notinstalled: $notinstalled
hasfailures: $hasfailures
registrylist: $registries
}
where: { _and: [{ fit: { _gte: 1 } }, $filter] }
) {
aggregate {
count
}
}
}
78 changes: 0 additions & 78 deletions package_search_with_count.gql

This file was deleted.

Loading
Loading