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
3 changes: 3 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,6 @@ Tar = "1.10"
UUIDs = "1.11"
julia = "1.12"
p7zip_jll = "17.5"

[apps]
pkg = {}
155 changes: 155 additions & 0 deletions src/Pkg.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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 <command>" - 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.
Expand Down
2 changes: 1 addition & 1 deletion src/precompile.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
Loading