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
111 changes: 107 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ The application follows a command-line interface pattern using the Cobra library
- `jh git-credential`: Git credential helper for seamless authentication
- `jh julia`: Julia installation management
- `jh run`: Julia execution with JuliaHub configuration
- `jh run setup`: Setup Julia credentials without starting Julia

4. **Data Models**:
- UUID strings for most entity IDs (projects, datasets, resources)
Expand Down Expand Up @@ -111,6 +112,27 @@ echo -e "protocol=https\nhost=juliahub.com\npath=git/projects/test/test\n" | go
git clone https://juliahub.com/git/projects/username/project.git
```

### Test Julia integration
```bash
# Install Julia (if not already installed)
go run . julia install

# Setup Julia credentials only
go run . run setup

# Run Julia REPL with credentials setup
go run . run

# Run Julia with credentials setup
go run . run -- -e "println(\"Hello from JuliaHub!\")"

# Run Julia script with project
go run . run -- --project=. script.jl

# Run Julia with multiple flags
go run . run -- --project=. --threads=4 -e "println(Threads.nthreads())"
```

## Dependencies

- `github.com/spf13/cobra`: CLI framework
Expand Down Expand Up @@ -196,10 +218,43 @@ git clone https://github.com/user/repo.git # Ignored by
## Julia Integration

The CLI provides Julia installation and execution with JuliaHub configuration:

### Julia Installation (`jh julia install`)
- Cross-platform installation (Windows via winget, Unix via official installer)
- Authentication file creation (`~/.julia/servers/<server>/auth.toml`)
- Package server configuration (`JULIA_PKG_SERVER`)
- Project activation (`--project=.`)
- Installs latest stable Julia version

### Julia Credentials
- **Authentication file**: Automatically creates `~/.julia/servers/<server>/auth.toml`
- **Atomic writes**: Uses temporary file + rename for safe credential updates
- **Automatic updates**: Credentials are automatically refreshed when:
- User runs `jh auth login`
- User runs `jh auth refresh`
- Token is refreshed via `ensureValidToken()`
- User runs `jh run` or `jh run setup`

### Julia Commands

#### `jh run [-- julia-args...]` - Run Julia with JuliaHub configuration
```bash
jh run # Start Julia REPL
jh run -- script.jl # Run a script
jh run -- -e "println(\"Hello\")" # Execute code
jh run -- --project=. --threads=4 script.jl # Run with flags
```
- Sets up credentials, then starts Julia
- Arguments after `--` are passed directly to Julia without modification
- User controls all Julia flags (including `--project`, `--threads`, etc.)
- Environment variables set:
- `JULIA_PKG_SERVER`: Points to your JuliaHub server
- `JULIA_PKG_USE_CLI_GIT`: Set to `true` for Git integration

#### `jh run setup` - Setup credentials only (no Julia execution)
```bash
jh run setup
```
- Creates/updates `~/.julia/servers/<server>/auth.toml` with current credentials
- Does not start Julia
- Useful for explicitly updating credentials

## Development Notes

Expand All @@ -209,10 +264,58 @@ The CLI provides Julia installation and execution with JuliaHub configuration:
- Token refresh is automatic via `ensureValidToken()`
- File uploads use multipart form data with proper content types
- Julia auth files use TOML format with `preferred_username` from JWT claims
- Julia auth files use atomic writes (temp file + rename) to prevent corruption
- Julia credentials are automatically updated after login and token refresh
- Git commands use `http.extraHeader` for authentication and pass through all arguments
- Git credential helper provides seamless authentication for standard Git commands
- Multi-server authentication handled automatically via credential helper
- Project filtering supports `--user` parameter for showing specific user's projects or own projects
- 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
- Credential helper follows Git protocol: responds only to JuliaHub URLs, ignores others

## Implementation Details

### Julia Credentials Management (`run.go`)

The Julia credentials system consists of three main functions:

1. **`createJuliaAuthFile(server, token)`**:
- Creates `~/.julia/servers/<server>/auth.toml` with TOML-formatted credentials
- Uses atomic writes: writes to temporary file, syncs, then renames
- Includes all necessary fields: tokens, expiration, refresh URL, user info
- Called by `setupJuliaCredentials()` and `updateJuliaCredentialsIfNeeded()`

2. **`setupJuliaCredentials()`**:
- Public function called by:
- `jh run` command (before starting Julia)
- `jh run setup` command
- `jh auth login` command (after successful login)
- `jh auth refresh` command (after successful refresh)
- Ensures valid token via `ensureValidToken()`
- Creates/updates Julia auth file
- Returns error if authentication fails

3. **`runJulia(args)`**:
- Sets up credentials via `setupJuliaCredentials()`
- Configures environment variables (`JULIA_PKG_SERVER`, `JULIA_PKG_USE_CLI_GIT`)
- Executes Julia with user-provided arguments (no automatic flags)
- Streams stdin/stdout/stderr to maintain interactive experience

### Automatic Credential Updates (`auth.go`)

The `updateJuliaCredentialsIfNeeded(server, token)` function:
- Called automatically by `ensureValidToken()` after token refresh
- Checks if `~/.julia/servers/<server>/auth.toml` exists
- If exists, updates it with refreshed token
- If not exists, does nothing (user hasn't used Julia integration yet)
- Errors are silently ignored to avoid breaking token operations

This ensures Julia credentials stay in sync with the main auth tokens without requiring manual intervention.

### Command Structure

- **`jh run`**: Primary command - always starts Julia after setting up credentials
- **`jh run setup`**: Subcommand - only sets up credentials without starting Julia
- **`jh auth login`**: Automatically sets up Julia credentials after successful login
- **`jh auth refresh`**: Automatically sets up Julia credentials after successful refresh
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ go build -o jh .
### Julia Integration

- `jh julia install` - Install Julia programming language
- `jh run` - Start Julia with JuliaHub configuration
- `jh run [-- julia-args...]` - Run Julia with JuliaHub configuration
- `jh run setup` - Setup JuliaHub credentials for Julia without starting Julia

### User Information (`jh user`)

Expand Down Expand Up @@ -246,6 +247,33 @@ git push
Note: It's recommended to use the git-credential helper, but you can still
clone using `jh clone username/project-name`; otherwise you need the project's uuid

### Julia Workflow

```bash
# Install Julia (if not already installed)
jh julia install

# Setup JuliaHub credentials only
jh run setup

# Start Julia REPL with JuliaHub configuration
jh run

# Run a Julia script
jh run -- script.jl

# Execute Julia code directly
jh run -- -e "println(\"Hello from JuliaHub!\")"

# Run Julia with project and multiple threads
jh run -- --project=. --threads=4 script.jl
```

Note: Arguments after `--` are passed directly to Julia. The `jh run` command:
1. Sets up JuliaHub credentials in `~/.julia/servers/<server>/auth.toml`
2. Configures `JULIA_PKG_SERVER` environment variable
3. Starts Julia with your specified arguments

## Architecture

- **Built with Go** using the Cobra CLI framework
Expand Down
24 changes: 24 additions & 0 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
)
Expand Down Expand Up @@ -328,9 +329,32 @@ func ensureValidToken() (*StoredToken, error) {
}
}

// Update Julia credentials after token refresh
// We ignore errors here to avoid breaking token operations if Julia setup fails
_ = updateJuliaCredentialsIfNeeded(storedToken.Server, updatedToken)

return updatedToken, nil
}

// updateJuliaCredentialsIfNeeded updates Julia credentials if the auth file exists
// This is called after token refresh to keep credentials in sync
func updateJuliaCredentialsIfNeeded(server string, token *StoredToken) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return err
}

// Check if the auth.toml file exists
authFilePath := filepath.Join(homeDir, ".julia", "servers", server, "auth.toml")
if _, err := os.Stat(authFilePath); os.IsNotExist(err) {
// File doesn't exist, so user hasn't used Julia integration yet
return nil
}

// File exists, update it
return createJuliaAuthFile(server, token)
}

func formatTokenInfo(token *StoredToken) string {
claims, err := decodeJWT(token.AccessToken)
if err != nil {
Expand Down
60 changes: 51 additions & 9 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,11 @@ This command will:
}

fmt.Println("Successfully authenticated!")

// Setup Julia credentials after successful authentication
if err := setupJuliaCredentials(); err != nil {
fmt.Printf("Warning: Failed to setup Julia credentials: %v\n", err)
}
},
}

Expand Down Expand Up @@ -263,6 +268,11 @@ refresh them proactively.`,
}

fmt.Println("Token refreshed successfully!")

// Setup Julia credentials after successful refresh
if err := setupJuliaCredentials(); err != nil {
fmt.Printf("Warning: Failed to setup Julia credentials: %v\n", err)
}
},
}

Expand Down Expand Up @@ -795,30 +805,61 @@ This command must be run from within a cloned JuliaHub project directory.`,
}

var runCmd = &cobra.Command{
Use: "run",
Use: "run [-- julia-args...]",
Short: "Run Julia with JuliaHub configuration",
Long: `Start Julia with JuliaHub package server configuration.
Long: `Run Julia with JuliaHub configuration and credentials.

This command:
1. Ensures you have valid JuliaHub authentication
2. Creates Julia authentication files (~/.julia/servers/<server>/auth.toml)
3. Configures Julia to use JuliaHub as the package server
4. Starts Julia with --project=. flag for local project activation
1. Sets up JuliaHub credentials (~/.julia/servers/<server>/auth.toml)
2. Starts Julia with the specified arguments

Environment variables set:
Arguments after -- are passed directly to Julia without modification.
Use 'jh run setup' to only setup credentials without starting Julia.

Environment variables set when running Julia:
- JULIA_PKG_SERVER: Points to your JuliaHub server
- JULIA_PKG_USE_CLI_GIT: Enables CLI git usage

Requires Julia to be installed (use 'jh julia install' if needed).`,
Example: " jh run",
Example: ` jh run # Start Julia REPL
jh run -- script.jl # Run a script
jh run -- -e "println(\"Hi\")" # Execute code
jh run -- --project=. --threads=4 script.jl # Run with options`,
Run: func(cmd *cobra.Command, args []string) {
if err := runJulia(); err != nil {
// Setup credentials and run Julia
if err := runJulia(args); err != nil {
fmt.Printf("Failed to run Julia: %v\n", err)
os.Exit(1)
}
},
}

var runSetupCmd = &cobra.Command{
Use: "setup",
Short: "Setup JuliaHub credentials for Julia",
Long: `Setup JuliaHub credentials in ~/.julia/servers/<server>/auth.toml without starting Julia.

This command:
1. Ensures you have valid JuliaHub authentication
2. Creates/updates Julia authentication files (~/.julia/servers/<server>/auth.toml)

Credentials are automatically setup when:
- Running 'jh auth login'
- Running 'jh auth refresh'
- Running 'jh run' (before starting Julia)

This command is useful for explicitly updating credentials without starting Julia.`,
Example: ` jh run setup # Setup credentials only`,
Run: func(cmd *cobra.Command, args []string) {
// Only setup Julia credentials
if err := setupJuliaCredentials(); err != nil {
fmt.Printf("Failed to setup Julia credentials: %v\n", err)
os.Exit(1)
}
fmt.Println("Julia credentials setup complete")
},
}

var gitCredentialCmd = &cobra.Command{
Use: "git-credential",
Short: "Git credential helper commands",
Expand Down Expand Up @@ -956,6 +997,7 @@ func init() {
projectCmd.AddCommand(projectListCmd)
userCmd.AddCommand(userInfoCmd)
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)
Expand Down
Loading