Skip to content
Open
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
11 changes: 9 additions & 2 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
- **projects.go**: Project management using GraphQL API with user filtering
- **user.go**: User information retrieval using GraphQL API
- **user.go**: User information retrieval using GraphQL API and REST API for listing users
- **git.go**: Git integration (clone, push, fetch, pull) with JuliaHub authentication
- **julia.go**: Julia installation and management
- **run.go**: Julia execution with JuliaHub configuration
Expand All @@ -29,7 +29,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}`)
- **REST API**: Used for dataset operations (`/api/v1/datasets`, `/datasets/{uuid}/url/{version}`) and user management (`/app/config/features/manage`)
- **GraphQL API**: Used for projects and user info (`/v1/graphql`)
- **Headers**: All GraphQL requests require `X-Hasura-Role: jhuser` header
- **Authentication**: Uses ID tokens (`token.IDToken`) for API calls
Expand All @@ -39,6 +39,8 @@ The application follows a command-line interface pattern using the Cobra library
- `jh dataset`: Dataset operations (list, download, upload, status)
- `jh project`: Project management (list with GraphQL, supports user filtering)
- `jh user`: User information (info with GraphQL)
- `jh admin`: Administrative commands (user management)
- `jh admin user`: User management (list all users with REST API, supports verbose mode)
- `jh clone`: Git clone with JuliaHub authentication and project name resolution
- `jh push/fetch/pull`: Git operations with JuliaHub authentication
- `jh git-credential`: Git credential helper for seamless authentication
Expand Down Expand Up @@ -90,6 +92,8 @@ go run . project list
go run . project list --user
go run . project list --user john
go run . user info
go run . admin user list
go run . admin user list --verbose
```

### Test Git operations
Expand Down Expand Up @@ -163,6 +167,7 @@ The application uses OAuth2 device flow:

### REST API Integration
- **Dataset operations**: Use presigned URLs for upload/download
- **User management**: `/app/config/features/manage` endpoint for listing all users
- **Authentication**: Bearer token with ID token
- **Upload workflow**: 3-step process (request presigned URL, upload to URL, close upload)

Expand Down Expand Up @@ -273,6 +278,8 @@ jh run setup
- Clone command automatically resolves `username/project` format to project UUIDs
- Folder naming conflicts are resolved with automatic numbering (project-1, project-2, etc.)
- Credential helper follows Git protocol: responds only to JuliaHub URLs, ignores others
- Admin user list command (`jh admin user list`) uses REST API endpoint `/app/config/features/manage` which requires appropriate permissions
- User list output is concise by default (Name and Email only); use `--verbose` flag for detailed information (UUID, groups, features)

## Implementation Details

Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ A command-line interface for interacting with JuliaHub, a platform for Julia com
- **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
- **User Management**: Display user information and profile details
- **User Management**: Display user information and view profile details
- **Administrative Commands**: Manage users and system resources (requires admin permissions)

## Installation

Expand Down Expand Up @@ -176,6 +177,12 @@ go build -o jh .

- `jh user info` - Show detailed user information

### Administrative Commands (`jh admin`)

- `jh admin user list` - List all users (requires appropriate permissions)
- Default: Shows only Name and Email
- `jh admin user list --verbose` - Show detailed user information including UUID, groups, and features

### Update (`jh update`)

- `jh update` - Check for updates and automatically install the latest version
Expand Down
61 changes: 59 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,10 @@ Available command categories:
dataset - Dataset operations (list, download, upload, status)
project - Project management (list, filter by user)
user - User information and profile
admin - Administrative commands (user management)
clone - Clone projects with automatic authentication
push - Push changes with authentication
fetch - Fetch updates with authentication
fetch - Fetch updates with authentication
pull - Pull changes with authentication
julia - Julia installation and management
run - Run Julia with JuliaHub configuration
Expand Down Expand Up @@ -669,6 +670,37 @@ Displays comprehensive information about the current user including:
},
}

var userListCmd = &cobra.Command{
Use: "list",
Short: "List all users",
Long: `List all users from JuliaHub.

By default, displays only Name and Email for each user.
Use --verbose flag to display comprehensive information including:
- UUID and email addresses
- Names
- JuliaHub groups and site groups
- Feature flags

This command uses the /app/config/features/manage endpoint which requires
appropriate permissions to view all users.`,
Example: " jh admin user list\n jh admin user list --verbose",
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)
}

verbose, _ := cmd.Flags().GetBool("verbose")

if err := listUsers(server, verbose); err != nil {
fmt.Printf("Failed to list users: %v\n", err)
os.Exit(1)
}
},
}

var cloneCmd = &cobra.Command{
Use: "clone <username/project> [local-path]",
Short: "Clone a project from JuliaHub",
Expand Down Expand Up @@ -953,6 +985,27 @@ without needing to use the 'jh' wrapper commands.`,
},
}

var adminCmd = &cobra.Command{
Use: "admin",
Short: "Administrative commands",
Long: `Administrative commands for JuliaHub.

These commands provide administrative functionality for managing JuliaHub
resources such as users, groups, and system configuration.

Note: Some commands may require administrative permissions.`,
}

var adminUserCmd = &cobra.Command{
Use: "user",
Short: "User management commands",
Long: `Administrative commands for managing users on JuliaHub.

Provides commands to list and manage users across the JuliaHub instance.

Note: These commands require appropriate administrative permissions.`,
}

var updateCmd = &cobra.Command{
Use: "update",
Short: "Update jh to the latest version",
Expand Down Expand Up @@ -985,6 +1038,8 @@ func init() {
projectListCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
projectListCmd.Flags().String("user", "", "Filter projects by user (leave empty to show only your own projects)")
userInfoCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
userListCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
userListCmd.Flags().Bool("verbose", false, "Show detailed user information")
cloneCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
pushCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
fetchCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server")
Expand All @@ -996,11 +1051,13 @@ func init() {
datasetCmd.AddCommand(datasetListCmd, datasetDownloadCmd, datasetUploadCmd, datasetStatusCmd)
projectCmd.AddCommand(projectListCmd)
userCmd.AddCommand(userInfoCmd)
adminUserCmd.AddCommand(userListCmd)
adminCmd.AddCommand(adminUserCmd)
juliaCmd.AddCommand(juliaInstallCmd)
runCmd.AddCommand(runSetupCmd)
gitCredentialCmd.AddCommand(gitCredentialHelperCmd, gitCredentialGetCmd, gitCredentialStoreCmd, gitCredentialEraseCmd, gitCredentialSetupCmd)

rootCmd.AddCommand(authCmd, jobCmd, datasetCmd, projectCmd, userCmd, juliaCmd, cloneCmd, pushCmd, fetchCmd, pullCmd, runCmd, gitCredentialCmd, updateCmd)
rootCmd.AddCommand(authCmd, jobCmd, datasetCmd, projectCmd, userCmd, adminCmd, juliaCmd, cloneCmd, pushCmd, fetchCmd, pullCmd, runCmd, gitCredentialCmd, updateCmd)
}

func main() {
Expand Down
102 changes: 102 additions & 0 deletions user.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,105 @@ func showUserInfo(server string) error {

return nil
}

// ManageUser represents a user from the /app/config/features/manage endpoint
type ManageUser struct {
Email string `json:"email"`
Name *string `json:"name"`
UUID string `json:"uuid"`
Features json.RawMessage `json:"features"` // Will be parsed from JSON string
JuliaHubGroups string `json:"juliahub_groups"`
SiteGroups string `json:"site_groups"`
ParsedFeatures map[string]interface{} `json:"-"` // Parsed features
}

// ManageUsersResponse represents the response from /app/config/features/manage
type ManageUsersResponse struct {
Users []ManageUser `json:"users"`
Features json.RawMessage `json:"features"`
}

func listUsers(server string, verbose bool) error {
token, err := ensureValidToken()
if err != nil {
return fmt.Errorf("authentication required: %w", err)
}

url := fmt.Sprintf("https://%s/app/config/features/manage", server)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}

req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.IDToken))
req.Header.Set("Accept", "application/json")

client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body))
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}

var response ManageUsersResponse
if err := json.Unmarshal(body, &response); err != nil {
return fmt.Errorf("failed to parse response: %w", err)
}

// Parse features JSON string for each user (only needed in verbose mode)
if verbose {
for i := range response.Users {
var features map[string]interface{}
if err := json.Unmarshal(response.Users[i].Features, &features); err == nil {
response.Users[i].ParsedFeatures = features
}
}
}

// Display users
fmt.Printf("Users (%d total):\n\n", len(response.Users))

if verbose {
// Verbose mode: show all details
for _, user := range response.Users {
fmt.Printf("UUID: %s\n", user.UUID)
fmt.Printf("Email: %s\n", user.Email)
if user.Name != nil {
fmt.Printf("Name: %s\n", *user.Name)
}
if user.JuliaHubGroups != "" {
fmt.Printf("JuliaHub Groups: %s\n", user.JuliaHubGroups)
}
if user.SiteGroups != "" {
fmt.Printf("Site Groups: %s\n", user.SiteGroups)
}
if len(user.ParsedFeatures) > 0 {
fmt.Printf("Features: %v\n", user.ParsedFeatures)
}
fmt.Println()
}
} else {
// Default mode: show only Name and Email
for _, user := range response.Users {
if user.Name != nil {
fmt.Printf("Name: %s\n", *user.Name)
} else {
fmt.Printf("Name: (not set)\n")
}
fmt.Printf("Email: %s\n", user.Email)
fmt.Println()
}
}

return nil
}