Skip to content

Commit d7c348c

Browse files
rmand97rma97mhanberg
authored
feat: add engine subcommands to expert (#254)
closes #209 Taking a stab at the issue. Hoping I understood it correctly. --------- Co-authored-by: Rolf Malthe Andersen <raflnatorr@gmail.com> Co-authored-by: Mitchell Hanberg <mitch@mitchellhanberg.com>
1 parent a9ee0ec commit d7c348c

File tree

3 files changed

+569
-1
lines changed

3 files changed

+569
-1
lines changed

apps/expert/lib/expert/application.ex

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,29 @@ defmodule Expert.Application do
1212

1313
@impl true
1414
def start(_type, _args) do
15+
argv = Burrito.Util.Args.argv()
16+
17+
# Handle engine subcommand first (before starting the LSP server)
18+
case argv do
19+
["engine" | engine_args] ->
20+
engine_args
21+
|> Expert.Engine.run()
22+
|> System.halt()
23+
24+
[subcommand | _] ->
25+
if not String.starts_with?(subcommand, "-") do
26+
IO.puts(:stderr, """
27+
Error: Unknown subcommand '#{subcommand}'
28+
29+
Run 'expert --help' for usage information.
30+
""")
31+
32+
System.halt(1)
33+
end
34+
end
35+
1536
{opts, _argv, _invalid} =
16-
OptionParser.parse(Burrito.Util.Args.argv(),
37+
OptionParser.parse(argv,
1738
strict: [version: :boolean, help: :boolean, stdio: :boolean, port: :integer]
1839
)
1940

@@ -26,13 +47,18 @@ defmodule Expert.Application do
2647
Source code: https://github.com/elixir-lang/expert
2748
2849
expert [flags]
50+
expert engine <subcommand> [options]
2951
3052
#{IO.ANSI.bright()}FLAGS#{IO.ANSI.reset()}
3153
3254
--stdio Use stdio as the transport mechanism
3355
--port <port> Use TCP as the transport mechanism, with the given port
3456
--help Show this help message
3557
--version Show Expert version
58+
59+
#{IO.ANSI.bright()}SUBCOMMANDS#{IO.ANSI.reset()}
60+
61+
engine Manage engine builds (use 'expert engine --help' for details)
3662
"""
3763

3864
cond do

apps/expert/lib/expert/engine.ex

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
defmodule Expert.Engine do
2+
@moduledoc """
3+
Utilities for managing Expert engine builds.
4+
5+
When Expert builds the engine for a project using Mix.install, it caches
6+
the build in the user data directory. If engine dependencies change (e.g.,
7+
in nightly builds), Mix.install may not know to rebuild, causing errors.
8+
9+
This module provides functions to inspect and clean these cached builds.
10+
"""
11+
12+
@doc """
13+
Runs engine management commands based on parsed arguments.
14+
15+
Returns the exit code for the command. Clean operations will stop at the
16+
first deletion error and return exit code 1.
17+
"""
18+
19+
@success_code 0
20+
@error_code 1
21+
22+
@spec run([String.t()]) :: non_neg_integer()
23+
def run(args) do
24+
{opts, subcommand, _invalid} =
25+
OptionParser.parse_head(args,
26+
strict: [help: :boolean],
27+
aliases: [h: :help]
28+
)
29+
30+
if opts[:help] do
31+
print_help()
32+
else
33+
case subcommand do
34+
["ls" | ls_opts] -> list_engines(ls_opts)
35+
["clean" | clean_opts] -> clean_engines(clean_opts)
36+
[unknown | _] -> print_unknown_subcommand(unknown)
37+
[] -> print_help()
38+
end
39+
end
40+
end
41+
42+
@spec list_engines([String.t()]) :: non_neg_integer()
43+
defp list_engines(ls_options) do
44+
{opts, _rest, _invalid} =
45+
OptionParser.parse_head(ls_options,
46+
strict: [help: :boolean],
47+
aliases: [h: :help]
48+
)
49+
50+
if opts[:help] do
51+
print_ls_help()
52+
else
53+
print_engine_dirs()
54+
end
55+
end
56+
57+
@spec print_engine_dirs() :: non_neg_integer()
58+
defp print_engine_dirs do
59+
dirs = get_engine_dirs()
60+
61+
case dirs do
62+
[] ->
63+
print_no_engines_found()
64+
65+
dirs ->
66+
Enum.each(dirs, &IO.puts/1)
67+
end
68+
69+
@success_code
70+
end
71+
72+
@spec clean_engines([String.t()]) :: non_neg_integer()
73+
defp clean_engines(clean_options) do
74+
{opts, _rest, _invalid} =
75+
OptionParser.parse_head(clean_options,
76+
strict: [force: :boolean, help: :boolean],
77+
aliases: [f: :force, h: :help]
78+
)
79+
80+
dirs = get_engine_dirs()
81+
82+
cond do
83+
opts[:help] ->
84+
print_clean_help()
85+
86+
dirs == [] ->
87+
print_no_engines_found()
88+
89+
opts[:force] ->
90+
clean_all_force(dirs)
91+
92+
true ->
93+
clean_interactive(dirs)
94+
end
95+
end
96+
97+
@spec base_dir() :: String.t()
98+
defp base_dir do
99+
base = :filename.basedir(:user_data, ~c"Expert")
100+
to_string(base)
101+
end
102+
103+
@spec get_engine_dirs() :: [String.t()]
104+
defp get_engine_dirs do
105+
base = base_dir()
106+
107+
if File.exists?(base) do
108+
base
109+
|> File.ls!()
110+
|> Enum.map(&Path.join(base, &1))
111+
|> Enum.filter(&File.dir?/1)
112+
|> Enum.sort()
113+
else
114+
[]
115+
end
116+
end
117+
118+
@spec clean_all_force([String.t()]) :: non_neg_integer()
119+
# Deletes all directories without prompting. Stops on first error and returns 1.
120+
defp clean_all_force(dirs) do
121+
result =
122+
Enum.reduce_while(dirs, :ok, fn dir, _acc ->
123+
case File.rm_rf(dir) do
124+
{:ok, _} ->
125+
IO.puts("Deleted #{dir}")
126+
{:cont, :ok}
127+
128+
{:error, reason, file} ->
129+
IO.puts(:stderr, "Error deleting #{file}: #{inspect(reason)}")
130+
{:halt, :error}
131+
end
132+
end)
133+
134+
case result do
135+
:ok -> @success_code
136+
:error -> @error_code
137+
end
138+
end
139+
140+
@spec clean_interactive([String.t()]) :: non_neg_integer()
141+
# Prompts the user for each directory deletion. Stops on first error and returns 1.
142+
defp clean_interactive(dirs) do
143+
result =
144+
Enum.reduce_while(dirs, :ok, fn dir, _acc ->
145+
answer = prompt_delete(dir)
146+
147+
if answer do
148+
case File.rm_rf(dir) do
149+
{:ok, _} ->
150+
{:cont, :ok}
151+
152+
{:error, reason, file} ->
153+
IO.puts(:stderr, "Error deleting #{file}: #{inspect(reason)}")
154+
{:halt, :error}
155+
end
156+
else
157+
{:cont, :ok}
158+
end
159+
end)
160+
161+
case result do
162+
:ok -> @success_code
163+
:error -> @error_code
164+
end
165+
end
166+
167+
@spec prompt_delete(dir :: [String.t()]) :: boolean()
168+
defp prompt_delete(dir) do
169+
IO.puts(["Delete #{dir}", IO.ANSI.red(), "?", IO.ANSI.reset(), " [Yn] "])
170+
171+
input =
172+
""
173+
|> IO.gets()
174+
|> String.trim()
175+
|> String.downcase()
176+
177+
case input do
178+
"" -> true
179+
"y" -> true
180+
"yes" -> true
181+
_ -> false
182+
end
183+
end
184+
185+
@spec print_no_engines_found() :: non_neg_integer()
186+
defp print_no_engines_found do
187+
IO.puts("No engine builds found.")
188+
IO.puts("\nEngine builds are stored in: #{base_dir()}")
189+
190+
@success_code
191+
end
192+
193+
@spec print_unknown_subcommand(String.t()) :: non_neg_integer()
194+
defp print_unknown_subcommand(subcommand) do
195+
IO.puts(:stderr, """
196+
Error: Unknown subcommand '#{subcommand}'
197+
198+
Run 'expert engine --help' for usage information.
199+
""")
200+
201+
@error_code
202+
end
203+
204+
@spec print_help() :: non_neg_integer()
205+
defp print_help do
206+
IO.puts("""
207+
Expert Engine Management
208+
209+
Manage cached engine builds created by Mix.install. Use these commands
210+
to resolve dependency errors or free up disk space.
211+
212+
USAGE:
213+
expert engine <subcommand>
214+
215+
SUBCOMMANDS:
216+
ls List all engine build directories
217+
clean Interactively delete engine build directories
218+
219+
Use 'expert engine <subcommand> --help' for more information on a specific command.
220+
221+
EXAMPLES:
222+
expert engine ls
223+
expert engine clean
224+
""")
225+
226+
@success_code
227+
end
228+
229+
@spec print_ls_help() :: non_neg_integer()
230+
defp print_ls_help do
231+
IO.puts("""
232+
List Engine Builds
233+
234+
List all cached engine build directories.
235+
236+
USAGE:
237+
expert engine ls
238+
239+
EXAMPLES:
240+
expert engine ls
241+
""")
242+
243+
@success_code
244+
end
245+
246+
@spec print_clean_help() :: non_neg_integer()
247+
defp print_clean_help do
248+
IO.puts("""
249+
Clean Engine Builds
250+
251+
Interactively delete cached engine build directories. By default, you will
252+
be prompted to confirm deletion of each build. Use --force to skip prompts.
253+
254+
USAGE:
255+
expert engine clean [options]
256+
257+
OPTIONS:
258+
-f, --force Delete all builds without prompting
259+
260+
EXAMPLES:
261+
expert engine clean
262+
expert engine clean --force
263+
""")
264+
265+
@success_code
266+
end
267+
end

0 commit comments

Comments
 (0)