From b79dab81cac269ec3b61e4822235e4127fb9261a Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Mon, 27 Oct 2025 20:37:17 +0100 Subject: [PATCH] make an app interface to Pkg --- Project.toml | 3 + src/Pkg.jl | 155 ++++++++++++++++++++++++++++++++++++++++++++++ src/precompile.jl | 2 +- 3 files changed, 159 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 5447cca47c..64532aa276 100644 --- a/Project.toml +++ b/Project.toml @@ -49,3 +49,6 @@ Tar = "1.10" UUIDs = "1.11" julia = "1.12" p7zip_jll = "17.5" + +[apps] +pkg = {} diff --git a/src/Pkg.jl b/src/Pkg.jl index 1a992788c1..5b299326b5 100644 --- a/src/Pkg.jl +++ b/src/Pkg.jl @@ -1012,6 +1012,161 @@ function _auto_precompile(ctx::Types.Context, pkgs::Vector{PackageSpec} = Packag end end +############# +# CLI Entry # +############# + +function print_help() + println("pkg - Julia package manager command-line interface\n") + println("Full documentation available at https://pkgdocs.julialang.org/\n") + + printstyled("OPTIONS:\n", bold=true) + println(" --project=PATH Set the project environment (default: current project)") + println(" --offline Work in offline mode") + println(" --help Show this help message") + println(" --version Show Pkg version\n") + + printstyled("SYNOPSIS:\n", bold=true) + println(" pkg [opts] cmd [args]\n") + println("Multiple commands can be given on the same line by interleaving a ; between") + println("the commands. Some commands have an alias, indicated below.\n") + + printstyled("COMMANDS:\n", bold=true) + + # Group commands by category + for (category_name, category_title) in [ + ("package", "Package management commands"), + ("registry", "Registry commands"), + ("app", "App commands") + ] + category_specs = get(REPLMode.SPECS, category_name, nothing) + category_specs === nothing && continue + + println() + printstyled(" $category_title\n", bold=true) + + # Get unique specs for this category and sort them + specs_dict = Dict{String, Any}() + for (name, spec) in category_specs + specs_dict[spec.canonical_name] = spec + end + + for cmd_name in sort(collect(keys(specs_dict))) + spec = specs_dict[cmd_name] + # For non-package commands, prefix with category + full_cmd = category_name == "package" ? cmd_name : "$category_name $cmd_name" + print(" ") + printstyled(full_cmd, color=:cyan) + if spec.short_name !== nothing + print(", ") + printstyled(spec.short_name, color=:cyan) + end + println(": $(spec.description)") + end + end +end + +function (@main)(ARGS) + # Disable interactivity warning (pkg should be used interactively) + if isdefined(REPLMode, :PRINTED_REPL_WARNING) + REPLMode.PRINTED_REPL_WARNING[] = true + end + + # Reset LOAD_PATH to allow normal Julia project resolution + # The shim sets JULIA_LOAD_PATH to the app environment, but we want + # to respect the user's current directory for project resolution + empty!(LOAD_PATH) + append!(LOAD_PATH, Base.DEFAULT_LOAD_PATH) + + # Parse options before the command + project_path = nothing + offline_mode = false + idx = 1 + + while idx <= length(ARGS) + arg = ARGS[idx] + + if startswith(arg, "--project=") + project_path = arg[length("--project=")+1:end] + idx += 1 + elseif arg == "--project" && idx < length(ARGS) + idx += 1 + project_path = ARGS[idx] + idx += 1 + elseif arg == "--offline" + offline_mode = true + idx += 1 + elseif arg == "--help" + print_help() + return 0 + elseif arg == "--version" + println("Pkg version $(Types.TOML.parsefile(joinpath(@__DIR__, "..", "Project.toml"))["version"])") + return 0 + elseif startswith(arg, "--") + println(stderr, "Error: Unknown option: $arg") + println(stderr, "Use --help for usage information") + return 1 + else + # Found the command, stop parsing options + break + end + end + + # Get the remaining arguments (the Pkg command) + remaining_args = ARGS[idx:end] + + if isempty(remaining_args) + print_help() + return 0 + end + + # Handle help command + if remaining_args[1] == "help" + if length(remaining_args) == 1 + # Just "help" with no arguments - show our CLI help + print_help() + return 0 + else + # "help " - convert to "?" for REPL compatibility + # e.g., "help registry add" -> "? registry add" + remaining_args[1] = "?" + end + end + + pkg_command = join(remaining_args, " ") + + # Set project if specified, otherwise use Julia's default logic + if project_path !== nothing + Pkg.activate(project_path; io=devnull) + else + # Look for Project.toml in pwd or parent directories + current_proj = Base.current_project(pwd()) + if current_proj !== nothing + Pkg.activate(current_proj; io=devnull) + else + # No project found, use default environment + Pkg.activate(; io=devnull) + end + end + + # Set offline mode if requested + if offline_mode + Pkg.offline(true) + end + + # Execute the Pkg REPL command + try + REPLMode.pkgstr(pkg_command) + return 0 + catch e + if e isa InterruptException + return 130 # Standard exit code for SIGINT + end + println(stderr, "Error: ", sprint(showerror, e)) + return 1 + end +end + include("precompile.jl") # Reset globals that might have been mutated during precompilation. diff --git a/src/precompile.jl b/src/precompile.jl index 5334456cad..44694220db 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -148,7 +148,7 @@ let Base.rm(tmp; recursive = true) catch end - + Base.precompile(Tuple{typeof(Pkg.main), Vector{String}}) Base.precompile(Tuple{typeof(Pkg.API.status)}) Base.precompile(Tuple{typeof(Pkg.Types.read_project_compat), Base.Dict{String, Any}, Pkg.Types.Project}) Base.precompile(Tuple{typeof(Pkg.Versions.semver_interval), Base.RegexMatch})