Delete Useless Apps
Mix.install([
{:req, "~> 0.5.0"},
{:kino, "~> 0.17.0"},
{:kino_clipboard, "~> 0.1.0"}
])
Build Combined Token
# Step 1: Read all LB_FLYIO_ env vars
raw_tokens =
System.get_env()
|> Enum.filter(fn {key, _} -> String.starts_with?(key, "LB_FLYIO_") end)
|> Enum.sort_by(fn {key, _} -> key end)
IO.puts("Found #{length(raw_tokens)} LB_FLYIO_* env vars")
# Step 2: Strip "FlyV1 " prefix if present, then validate
processed_tokens =
Enum.map(raw_tokens, fn {key, value} ->
stripped =
if String.starts_with?(value, "FlyV1 ") do
String.replace_prefix(value, "FlyV1 ", "")
else
value
end
{key, stripped}
end)
# Step 3: Raise if any token does not start with fm2_
invalid =
processed_tokens
|> Enum.filter(fn {_key, value} -> not String.starts_with?(value, "fm2_") end)
|> Enum.map(fn {key, _} -> key end)
if invalid != [] do
raise "The following environment variables have invalid tokens: #{Enum.join(invalid, ", ")}"
end
IO.puts("All #{length(processed_tokens)} tokens are valid ✓")
# Step 4: Build combined token — FlyV1 fm2_token1,fm2_token2,...
combined_token =
processed_tokens
|> Enum.map(fn {_key, value} -> value end)
|> Enum.join(",")
|> then(&"FlyV1 #{&1}")
IO.puts("Combined token built from #{length(processed_tokens)} tokens")
Fetch Organizations
# Step 5 & 6: Query organizations via GraphQL
gql_response =
Req.post!(
"https://api.fly.io/graphql",
headers: [{"authorization", combined_token}],
json: %{
"query" => "query($admin: Boolean!) { organizations(admin: $admin) { nodes { rawSlug name } } }",
"variables" => %{"admin" => false}
}
)
if errors = get_in(gql_response.body, ["errors"]) do
raise "GraphQL errors: #{inspect(errors)}"
end
orgs = get_in(gql_response.body, ["data", "organizations", "nodes"])
IO.puts("Found #{length(orgs)} organizations:")
for %{"name" => name, "rawSlug" => slug} <- orgs, do: IO.puts(" • #{name} (#{slug})")
orgs
List All Apps
# Step 7: Fetch apps for every org and display a unified table
all_apps =
orgs
|> Enum.flat_map(fn %{"name" => org_name, "rawSlug" => org_slug} ->
%{body: body} =
Req.get!(
url: "https://api.machines.dev/v1/apps?org_slug=#{org_slug}",
headers: [{"authorization", combined_token}]
)
apps = body["apps"] || []
Enum.map(apps, fn app ->
%{
"name" => app["name"],
"organization" => org_name,
"machine_count" => app["machine_count"]
}
end)
end)
|> Enum.sort_by(fn app -> {app["organization"], app["name"]} end)
IO.puts("Total apps: #{length(all_apps)}")
Kino.DataTable.new(all_apps, keys: ["name", "organization", "machine_count"])
Configure Deletion Prefixes
{:ok, prefix_agent} = Agent.start_link(fn -> ["hello-fly-"] end)
frame = Kino.Frame.new()
render_ui = fn render_ui, prefixes ->
Agent.update(prefix_agent, fn _ -> prefixes end)
add_form =
Kino.Control.form(
[new_prefix: Kino.Input.text("New prefix")],
submit: "Add"
)
remove_btns =
Enum.map(prefixes, fn prefix ->
{prefix, Kino.Control.button("✕")}
end)
list_widget =
if prefixes == [] do
Kino.Markdown.new("_No prefixes added yet._")
else
rows =
Enum.map(remove_btns, fn {prefix, btn} ->
Kino.Layout.grid([Kino.Markdown.new("`#{prefix}`"), btn], columns: 2)
end)
Kino.Layout.grid(rows)
end
Kino.Frame.render(
frame,
Kino.Layout.grid([
Kino.Markdown.new("**Prefixes to match for deletion:**"),
list_widget,
add_form
])
)
Kino.listen(add_form, fn %{data: %{new_prefix: raw}} ->
new_prefix = String.trim(raw)
current = Agent.get(prefix_agent, & &1)
if new_prefix != "" and new_prefix not in current do
render_ui.(render_ui, current ++ [new_prefix])
end
end)
for {prefix, btn} <- remove_btns do
Kino.listen(btn, fn _ ->
current = Agent.get(prefix_agent, & &1)
render_ui.(render_ui, List.delete(current, prefix))
end)
end
end
render_ui.(render_ui, ["hello-fly-"])
frame
# Read current prefixes from the form above
deletion_prefixes = Agent.get(prefix_agent, & &1)
destroy_commands =
all_apps
|> Enum.filter(fn %{"name" => name} ->
Enum.any?(deletion_prefixes, &String.starts_with?(name, &1))
end)
|> Enum.map_join("\n", fn %{"name" => name} -> "fly apps destroy #{name} --yes" end)
IO.puts(destroy_commands)
KinoClipboard.new(fn -> destroy_commands end, label: "Copy destroy commands")