diff --git a/CLAUDE.md b/CLAUDE.md index f07f15c..bd32ad4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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) @@ -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 @@ -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//auth.toml`) -- Package server configuration (`JULIA_PKG_SERVER`) -- Project activation (`--project=.`) +- Installs latest stable Julia version + +### Julia Credentials +- **Authentication file**: Automatically creates `~/.julia/servers//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//auth.toml` with current credentials +- Does not start Julia +- Useful for explicitly updating credentials ## Development Notes @@ -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 \ No newline at end of file +- 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//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//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 \ No newline at end of file diff --git a/README.md b/README.md index f9d638f..e896a05 100644 --- a/README.md +++ b/README.md @@ -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`) @@ -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//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 diff --git a/auth.go b/auth.go index ad4dd7f..f8e2528 100644 --- a/auth.go +++ b/auth.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "os" + "path/filepath" "strings" "time" ) @@ -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 { diff --git a/main.go b/main.go index 48ddecb..71b31c2 100644 --- a/main.go +++ b/main.go @@ -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) + } }, } @@ -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) + } }, } @@ -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//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//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//auth.toml without starting Julia. + +This command: +1. Ensures you have valid JuliaHub authentication +2. Creates/updates Julia authentication files (~/.julia/servers//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", @@ -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) diff --git a/run.go b/run.go index 0e44125..764c6ed 100644 --- a/run.go +++ b/run.go @@ -26,14 +26,6 @@ func createJuliaAuthFile(server string, token *StoredToken) error { return fmt.Errorf("failed to decode JWT token: %w", err) } - // Create auth.toml file - authFilePath := filepath.Join(serverDir, "auth.toml") - file, err := os.Create(authFilePath) - if err != nil { - return fmt.Errorf("failed to create auth.toml file: %w", err) - } - defer file.Close() - // Calculate refresh URL var authServer string if server == "juliahub.com" { @@ -67,15 +59,49 @@ name = "%s" token.Name, ) - _, err = file.WriteString(content) + // Use atomic write: write to temp file, then rename + authFilePath := filepath.Join(serverDir, "auth.toml") + tempFile, err := os.CreateTemp(serverDir, ".auth.toml.tmp.*") if err != nil { + return fmt.Errorf("failed to create temporary auth file: %w", err) + } + tempPath := tempFile.Name() + + // Clean up temp file on error + defer func() { + if tempFile != nil { + tempFile.Close() + os.Remove(tempPath) + } + }() + + // Write content to temp file + if _, err := tempFile.WriteString(content); err != nil { return fmt.Errorf("failed to write auth.toml content: %w", err) } + // Sync to ensure data is written to disk + if err := tempFile.Sync(); err != nil { + return fmt.Errorf("failed to sync auth.toml file: %w", err) + } + + // Close temp file before rename + if err := tempFile.Close(); err != nil { + return fmt.Errorf("failed to close temporary auth file: %w", err) + } + tempFile = nil // Prevent defer cleanup + + // Atomically rename temp file to final location + if err := os.Rename(tempPath, authFilePath); err != nil { + return fmt.Errorf("failed to rename auth.toml file: %w", err) + } + return nil } -func runJulia() error { +// setupJuliaCredentials ensures Julia authentication files are created +// This should be called after successful authentication or token refresh +func setupJuliaCredentials() error { // Read server configuration server, err := readConfigFile() if err != nil { @@ -93,6 +119,21 @@ func runJulia() error { return fmt.Errorf("failed to create Julia auth file: %w", err) } + return nil +} + +func runJulia(args []string) error { + // Setup Julia credentials + if err := setupJuliaCredentials(); err != nil { + return err + } + + // Read server for environment setup + server, err := readConfigFile() + if err != nil { + return fmt.Errorf("failed to read configuration: %w", err) + } + // Check if Julia is available if _, err := exec.LookPath("julia"); err != nil { return fmt.Errorf("Julia not found in PATH. Please install Julia first using 'jh julia install'") @@ -103,13 +144,14 @@ func runJulia() error { env = append(env, fmt.Sprintf("JULIA_PKG_SERVER=https://%s", server)) env = append(env, "JULIA_PKG_USE_CLI_GIT=true") - // Prepare Julia command with --project flag - cmd := exec.Command("julia", "--project=.") + // Prepare Julia command with user-provided arguments + // Do not automatically add --project=. - let user control this + cmd := exec.Command("julia", args...) cmd.Env = env cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - // Execute Julia and replace current process + // Execute Julia return cmd.Run() }