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
18 changes: 17 additions & 1 deletion apps/expert/lib/expert/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,19 @@ defmodule Expert.Application do

@impl true
def start(_type, _args) do
argv = Burrito.Util.Args.argv()

# Handle engine subcommand first (before starting the LSP server)
case argv do
["engine" | engine_args] ->
Expert.Engine.run(engine_args)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make this return the exit code, and put the System.halt(exit_code) part here?

That'll make the unit tests easier to write, as well as pull the halting part up to the top level.

This way it's very clear that the program stops after this code runs.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.
Just a quick question. Would you like it to always return success (0), event though it fails deleting a file?

like if its fails the middle one of 3 files in expert clean --force it will continue after failing, logging something went wrong, and delete the last one and return 0. Or would like it to stop after failing and return 1?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it fails any, lets return a non zero exit code (1).


_ ->
:noop
end

{opts, _argv, _invalid} =
OptionParser.parse(Burrito.Util.Args.argv(),
OptionParser.parse(argv,
strict: [version: :boolean, help: :boolean, stdio: :boolean, port: :integer]
)

Expand All @@ -26,13 +37,18 @@ defmodule Expert.Application do
Source code: https://github.com/elixir-lang/expert

expert [flags]
expert engine <subcommand> [options]

#{IO.ANSI.bright()}FLAGS#{IO.ANSI.reset()}

--stdio Use stdio as the transport mechanism
--port <port> Use TCP as the transport mechanism, with the given port
--help Show this help message
--version Show Expert version

#{IO.ANSI.bright()}SUBCOMMANDS#{IO.ANSI.reset()}

engine Manage engine builds (use 'expert engine --help' for details)
"""

cond do
Expand Down
163 changes: 163 additions & 0 deletions apps/expert/lib/expert/engine.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
defmodule Expert.Engine do
@moduledoc """
Utilities for managing Expert engine builds.
When Expert builds the engine for a project using Mix.install, it caches
the build in the user data directory. If engine dependencies change (e.g.,
in nightly builds), Mix.install may not know to rebuild, causing errors.
This module provides functions to inspect and clean these cached builds.
"""

@doc """
Runs engine management commands based on parsed arguments.
Returns :ok and halts the system after executing the command.
"""
@spec run([String.t()]) :: no_return()
def run(args) do
{opts, subcommand, _invalid} =
OptionParser.parse(args,
strict: [force: :boolean],
aliases: [f: :force]
)

case subcommand do
["ls"] -> list_engines()
["clean"] -> clean_engines(opts)
_ -> print_help()
end
end

@spec list_engines() :: no_return()
defp list_engines do
case get_engine_dirs() do
[] ->
IO.puts("No engine builds found.")
print_location_info()

dirs ->
Enum.each(dirs, &IO.puts/1)
end

System.halt(0)
end

@spec clean_engines(keyword()) :: no_return()
defp clean_engines(opts) do
case get_engine_dirs() do
[] ->
IO.puts("No engine builds found.")
print_location_info()
System.halt(0)

dirs ->
if opts[:force] do
clean_all_force(dirs)
else
clean_interactive(dirs)
end
end
end

defp base_dir do
base = :filename.basedir(:user_data, ~c"Expert")
to_string(base)
end

defp get_engine_dirs do
base = base_dir()

if File.exists?(base) do
base
|> File.ls!()
|> Enum.map(&Path.join(base, &1))
|> Enum.filter(&File.dir?/1)
|> Enum.sort()
else
[]
end
end

@spec clean_all_force([String.t()]) :: no_return()
defp clean_all_force(dirs) do
Enum.each(dirs, fn dir ->
case File.rm_rf(dir) do
{:ok, _} ->
IO.puts("Deleted #{dir}")

{:error, reason, file} ->
IO.puts(:stderr, "Error deleting #{file}: #{inspect(reason)}")
end
end)

System.halt(0)
end

@spec clean_interactive([String.t()]) :: no_return()
defp clean_interactive(dirs) do
Enum.each(dirs, fn dir ->
answer = prompt_delete(dir)

if answer do
case File.rm_rf(dir) do
{:ok, _} ->
:ok

{:error, reason, file} ->
IO.puts(:stderr, "Error deleting #{file}: #{inspect(reason)}")
end
end
end)

System.halt(0)
end

defp prompt_delete(dir) do
IO.puts(["Delete #{dir}", IO.ANSI.red(), "?", IO.ANSI.reset(), " [Yn] "])

input =
""
|> IO.gets()
|> String.trim()
|> String.downcase()

case input do
"" -> true
"y" -> true
"yes" -> true
_ -> false
end
end

defp print_location_info do
IO.puts("\nEngine builds are stored in: #{base_dir()}")
end

@spec print_help() :: no_return()
defp print_help do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also add help text to each subcommand, and the options can be presented for the relevant subcommand.

So in the existing help text here, we won't have any options, and the --force option will show up in the help text for the clean subcommand

IO.puts("""
Expert Engine Management
Manage cached engine builds created by Mix.install. Use these commands
to resolve dependency errors or free up disk space.
USAGE:
expert engine <subcommand> [options]
SUBCOMMANDS:
ls List all engine build directories
clean Interactively delete engine build directories
OPTIONS:
-f, --force Delete all builds without prompting (clean only)
EXAMPLES:
expert engine ls
expert engine clean
expert engine clean --force
""")

System.halt(0)
end
end
Loading
Loading