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
17 changes: 11 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,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, registrator) with REST API integration
- **packages.go**: Package operations (search, dependency) with REST API primary path (`/packages/info`), GraphQL fallback, and documentation API (`/docs/{registry}/{package}/stable/pkg.json`)
- **packages.go**: Package operations (search, dependency) with REST API primary path (`/api/v1/ui/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 All @@ -36,7 +36,7 @@ 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`, `/api/v1/registry/config/registry/{name}`, `/api/v1/registry/config/registrator/{name}`), package search/info primary path (`/packages/info`), token management (`/app/token/activelist`), user management (`/app/config/features/manage`), admin group management (`/app/config/groups`), registry credential management (old API tried first, new API fallback), landing page management (`/app/homepage` GET, `/app/config/homepage` POST/DELETE), and vulnerability scanning (`/api/v1/ui/vulnerabilities/packages/{name}`)
- **REST API**: Used for dataset operations (`/api/v1/datasets`, `/datasets/{uuid}/url/{version}`), registry operations (`/api/v1/registry/registries/descriptions`, `/api/v1/registry/config/registry/{name}`, `/api/v1/registry/config/registrator/{name}`), package search/info primary path (`/api/v1/ui/packages/info`), token management (`/app/token/activelist`), user management (`/app/config/features/manage`), admin group management (`/app/config/groups`), registry credential management (old API tried first, new API fallback), landing page management (`/app/homepage` GET, `/app/config/homepage` POST/DELETE), and vulnerability scanning (`/api/v1/ui/vulnerabilities/packages/{name}`)
- **GraphQL API**: Used for projects, user info, user list (`public_users`), group list, and package search/info fallback (`/v1/graphql`)
- **Headers**: All GraphQL requests require `Authorization: Bearer <id_token>`, `X-Hasura-Role: jhuser`, and `X-Juliahub-Ensure-JS: true`
- **Authentication**: Uses ID tokens (`token.IDToken`) for API calls
Expand All @@ -48,7 +48,7 @@ The application follows a command-line interface pattern using the Cobra library
- `jh registry config`: Show registry JSON config by name; subcommands add/update accept JSON via stdin or `--file`
- `jh registry permission`: Registry permission management (list, set, remove)
- `jh registry registrator`: Show registrator config by name; subcommand update accepts JSON via stdin or `--file`
- `jh package`: Package search and dependency (REST primary via `/packages/info`, GraphQL fallback; dependency data from `/docs/{registry}/{package}/stable/pkg.json`)
- `jh package`: Package search and dependency (REST primary via `/api/v1/ui/packages/info`, GraphQL fallback; dependency data from `/docs/{registry}/{package}/stable/pkg.json`)
- `jh project`: Project management (list with GraphQL, supports user filtering)
- `jh user`: User information (info, list via GraphQL `public_users`)
- `jh group`: Group information (list via GraphQL)
Expand Down Expand Up @@ -122,6 +122,10 @@ go run . package info Plots --registries General
go run . package dependency DataFrames
go run . package dependency DataFrames --indirect
go run . package dependency CSV --registry General

# Get package dependents (reverse dependencies)
go run . package dependents JuMP
go run . package dependents JuMP --indirect
```

### Test registry operations
Expand Down Expand Up @@ -327,7 +331,7 @@ The application uses OAuth2 device flow:
- **User management**: `/app/config/features/manage` endpoint for listing all users
- **Token management**: `/app/token/activelist` endpoint for listing all API tokens
- **Registry credentials**: `GET /api/v1/sysconfig/credentials` to fetch (returns `CredentialsInfo` object directly); `POST` to add tokens/apps; `PUT` to update tokens/apps or replace all SSH credentials; `DELETE` with `{tokens:[...], githubApps:[...]}` to delete
- **Package search/info primary**: `/packages/info` endpoint with `name`, `registries`, `tags`, `licenses`, `limit`, `offset` query params; returns `{packages: [...], meta: {total: N}}`
- **Package search/info primary**: `/api/v1/ui/packages/info` (the web UI's package listing) with `name`, `registries` (comma-separated names), `tags`, `licenses`, `sorts[0]` (`-score` when searching by name) and page-based pagination `pagination[type]=offset&pagination[page]=N&pagination[per_page]=M` (M ≤ 100); returns `{packages: [...], meta: {total: N}}`. The CLI's `--limit`/`--offset` window is mapped onto pages by `planRESTPages`. (The older `/packages/info` path is not routed by current JuliaHub: it falls through to the git `/packages/` handler and 404s.)
- **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 @@ -507,15 +511,16 @@ 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 (`FilteredPackages` / `FilteredPackagesCount` 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 (`/api/v1/ui/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`, `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.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)
- `jh package dependents` reads `reversedeps` from the same `pkg.json` (`fetchPackageDocs`). Entries carry `version` (singular), and a dependent can appear once as direct and once as indirect; `splitDependents` collapses them to one per UUID (direct wins), sorted by name
- `getPackageDependencies` locates the package's registry with `findPackageRegistry` (REST listing, GraphQL fallback), then fetches `/docs/{registry}/{package}/stable/pkg.json` for dependency data; no REST fallback (docs endpoint is authoritative)
- `jh vuln` uses two REST endpoints: vulnerabilities at `/api/v1/ui/vulnerabilities/packages/{name}?version=<ver>` and latest version at `/docs/<registry>/<package>/versions.json` (first entry is latest); no GraphQL fallback
- When no `--version` is given, `fetchLatestVersion` calls the versions.json endpoint (registry defaults to `General`, overridable with `--registry`)
- By default only advisories where `is_affected == true` are shown; `--all` overrides this
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ go build -o jh .
- Default: Shows up to 10 direct dependencies (NAME, REGISTRY, UUID, VERSIONS)
- `jh package dependency --indirect` - Include both direct and indirect dependencies
- `jh package dependency --registry General` - Specify registry to use
- `jh package dependents <package-name>` - List packages that depend on a package (reverse dependencies)
- Default: Prints direct/indirect/total counts and lists the direct dependents (NAME, REGISTRY, UUID)
- `jh package dependents --indirect` - Also list indirect dependents
- `jh package dependents --registry General` - Specify registry to use

### Registry Management (`jh registry`)

Expand Down Expand Up @@ -347,6 +351,10 @@ jh package info Plots --registries General
jh package dependency DataFrames # Shows up to 10 direct dependencies
jh package dependency DataFrames --indirect # Includes indirect dependencies
jh package dependency CSV --registry General # Use specific registry

# List reverse dependencies (dependents)
jh package dependents JuMP # Counts, plus the direct dependents
jh package dependents JuMP --indirect # Also lists indirect dependents
```

### Registry Operations
Expand Down
38 changes: 37 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,39 @@ the --registry flag.`,
},
}

var packageDependentsCmd = &cobra.Command{
Use: "dependents <package-name>",
Short: "List packages that depend on a package",
Long: `List the reverse dependencies of a Julia package: the packages that
depend on it.

Prints the number of direct and indirect dependents and lists the direct
ones. Use --indirect to list the indirect dependents as well.

The data comes from the same package documentation JSON endpoint as
jh package dependency, for the package's stable version. 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 dependents JuMP\n jh package dependents --indirect DataFrames\n jh package dependents --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 := getPackageDependents(server, packageName, registryName, showIndirect); err != nil {
fmt.Printf("Failed to get package dependents: %v\n", err)
os.Exit(1)
}
},
}

var registryCmd = &cobra.Command{
Use: "registry",
Short: "Registry management commands",
Expand Down Expand Up @@ -2458,6 +2491,9 @@ func init() {
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")
packageDependentsCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
packageDependentsCmd.Flags().String("registry", "", "Specify registry name (uses first registry if not specified)")
packageDependentsCmd.Flags().Bool("indirect", false, "Also list indirect dependents")
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 Down Expand Up @@ -2500,7 +2536,7 @@ func init() {
authCmd.AddCommand(authLoginCmd, authRefreshCmd, authStatusCmd, authEnvCmd)
jobCmd.AddCommand(jobListCmd, jobStartCmd)
datasetCmd.AddCommand(datasetListCmd, datasetDownloadCmd, datasetUploadCmd, datasetStatusCmd)
packageCmd.AddCommand(packageSearchCmd, packageInfoCmd, packageDependencyCmd)
packageCmd.AddCommand(packageSearchCmd, packageInfoCmd, packageDependencyCmd, packageDependentsCmd)
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
Loading