Current section

Files

Jump to
saas_kit lib mix tasks saaskit.feature.list.ex
Raw

lib/mix/tasks/saaskit.feature.list.ex

defmodule Mix.Tasks.Saaskit.Feature.List do
@moduledoc """
Lists all available SaaS Kit features and their installation status.
Usage:
mix saaskit.feature.list
mix saaskit.feature.list --filter "auth,billing"
mix saaskit.feature.list --json
Options:
--filter Comma-separated words to filter features by name, slug, or description.
--json Emit machine-readable JSON instead of human-friendly text.
JSON shape:
{
"schema_version": 1,
"ok": true,
"app": { "name": "MyApp", "slug": "my_app" },
"features": [
{
"slug": "auth",
"name": "Authentication",
"public_description": "...",
"packages": ["bcrypt_elixir"],
"dependencies": [],
"decisions": [],
"installed": false
}
]
}
"""
use Mix.Task
alias SaasKit.API
alias SaasKit.Task.Helpers
@impl Mix.Task
def run(args) do
{opts, _} = Helpers.parse_opts(args, filter: :string)
Helpers.enter_json_mode(opts)
if is_nil(API.token()) do
Helpers.fail!(
:not_configured,
"boilerplate_token is not set. See mix saaskit.status for setup instructions.",
opts,
1
)
end
case API.fetch_features() do
{:ok, boilerplate, features} ->
filtered = apply_filter(features, opts[:filter])
payload = %{
app: %{
name: boilerplate["app_name"],
slug: boilerplate["app_name_lower"]
},
features: Enum.map(filtered, &normalize/1)
}
Helpers.emit(payload, opts, &print_human/1)
{:error, :not_found} ->
Helpers.fail!(:api_unreachable, "Boilerplate not found at #{API.base_url()}.", opts, 1)
{:error, :api_unreachable} ->
Helpers.fail!(:api_unreachable, "Failed to reach #{API.base_url()}.", opts, 1)
end
end
defp apply_filter(features, nil), do: features
defp apply_filter(features, ""), do: features
defp apply_filter(features, filter_str) do
words =
filter_str
|> String.downcase()
|> String.split(",", trim: true)
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
case words do
[] ->
features
_ ->
Enum.filter(features, fn f ->
haystack =
"#{f["slug"]} #{f["name"]} #{f["public_description"]}"
|> String.downcase()
Enum.any?(words, &String.contains?(haystack, &1))
end)
end
end
defp normalize(f) do
%{
slug: f["slug"],
name: f["name"],
public_description: f["public_description"],
packages: f["packages"] || [],
dependencies: f["dependencies"] || [],
decisions: f["decisions"] || [],
installed: f["installed"] == true
}
end
defp print_human(%{app: app, features: features}) do
Mix.shell().info("#{IO.ANSI.blue()}* App:#{IO.ANSI.reset()} #{app.name} (#{app.slug})")
Enum.each(features, fn f ->
status =
if f.installed,
do: "#{IO.ANSI.green()}[installed]#{IO.ANSI.reset()}",
else: "[ ] "
desc =
if f.public_description && f.public_description != "",
do: " — #{f.public_description}",
else: ""
Mix.shell().info(" #{status} #{f.slug}#{desc}")
Enum.each(f.decisions, fn decision ->
options = Enum.map_join(decision["options"] || [], ", ", & &1["slug"])
Mix.shell().info(" requires #{decision["key"]}: #{options}")
end)
end)
end
end