Skip to content
Merged
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
32 changes: 17 additions & 15 deletions app/lib/linear_cli/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ defmodule LinearCli.CLI do
"""

alias LinearCli.CLI.Commands
alias LinearCli.CLI.Commands.{Profiles, Projects, Teams}
alias LinearCli.CLI.Commands.System, as: SystemCmds

@workflow_state_types ~w(triage backlog unstarted started completed canceled duplicate)

Expand Down Expand Up @@ -196,39 +198,39 @@ defmodule LinearCli.CLI do
end
end

defp dispatch([:whoami], result, halt), do: run(&Commands.whoami/1, result, halt)
defp dispatch([:version], result, halt), do: run(&Commands.version/1, result, halt)
defp dispatch([:team, :list], result, halt), do: run(&Commands.team_list/1, result, halt)
defp dispatch([:whoami], result, halt), do: run(&SystemCmds.whoami/1, result, halt)
defp dispatch([:version], result, halt), do: run(&SystemCmds.version/1, result, halt)
defp dispatch([:team, :list], result, halt), do: run(&Teams.team_list/1, result, halt)

defp dispatch([:team, :favorite], result, halt),
do: run(&Commands.team_favorite/1, result, halt)
do: run(&Teams.team_favorite/1, result, halt)

defp dispatch([:team, :unfavorite], result, halt),
do: run(&Commands.team_unfavorite/1, result, halt)
do: run(&Teams.team_unfavorite/1, result, halt)

defp dispatch([:project, :list], result, halt), do: run(&Commands.project_list/1, result, halt)
defp dispatch([:project, :list], result, halt), do: run(&Projects.project_list/1, result, halt)

defp dispatch([:project, :favorite], result, halt),
do: run(&Commands.project_favorite/1, result, halt)
do: run(&Projects.project_favorite/1, result, halt)

defp dispatch([:project, :unfavorite], result, halt),
do: run(&Commands.project_unfavorite/1, result, halt)
do: run(&Projects.project_unfavorite/1, result, halt)

defp dispatch([:project, :update], result, halt),
do: run(&Commands.project_update/1, result, halt)
do: run(&Projects.project_update/1, result, halt)

defp dispatch([:profile, :create], result, halt),
do: run(&Commands.profile_create/1, result, halt)
do: run(&Profiles.profile_create/1, result, halt)

defp dispatch([:profile, :list], result, halt), do: run(&Commands.profile_list/1, result, halt)
defp dispatch([:profile, :use], result, halt), do: run(&Commands.profile_use/1, result, halt)
defp dispatch([:profile, :show], result, halt), do: run(&Commands.profile_show/1, result, halt)
defp dispatch([:profile, :list], result, halt), do: run(&Profiles.profile_list/1, result, halt)
defp dispatch([:profile, :use], result, halt), do: run(&Profiles.profile_use/1, result, halt)
defp dispatch([:profile, :show], result, halt), do: run(&Profiles.profile_show/1, result, halt)

defp dispatch([:profile, :delete], result, halt),
do: run(&Commands.profile_delete/1, result, halt)
do: run(&Profiles.profile_delete/1, result, halt)

defp dispatch([:profile, :clear], result, halt),
do: run(&Commands.profile_clear/1, result, halt)
do: run(&Profiles.profile_clear/1, result, halt)

defp dispatch([:issue, :list], result, halt), do: run(&Commands.issue_list/1, result, halt)
defp dispatch([:issue, :view], result, halt), do: run(&Commands.issue_view/1, result, halt)
Expand Down
248 changes: 7 additions & 241 deletions app/lib/linear_cli/cli/commands.ex
Original file line number Diff line number Diff line change
@@ -1,254 +1,20 @@
defmodule LinearCli.CLI.Commands do
@moduledoc """
The logic behind each subcommand: fetch via `LinearCli.Linear`, display the
result. Ported from vendor/ruby-linear-cli/lib/linear/commands/**.
Issue command implementations: fetch via `LinearCli.Linear`, display the
result. Ported from vendor/ruby-linear-cli/lib/linear/commands/issue/**.

Non-issue command families live in their own focused modules:
`LinearCli.CLI.Commands.System`, `LinearCli.CLI.Commands.Teams`,
`LinearCli.CLI.Commands.Projects`, and `LinearCli.CLI.Commands.Profiles`.
"""

alias LinearCli.Browser
alias LinearCli.CLI.{Display, IssueHelpers, Projects, Prompt, WhatFor}
alias LinearCli.CLI.Issue.{Actions, Assignment, Creation, Identifiers}
alias LinearCli.{Favorites, Git, Linear, Profiles}
alias LinearCli.{Git, Linear, Profiles}

@max_concurrent_issue_updates 20

@doc "Ported from commands/whoami.rb."
def whoami(%{flags: flags, options: options}) do
with {:ok, user} <- Linear.me() do
Display.show(user, %{output: options.output, teams: flags.teams})
:ok
end
end

@doc """
Ported from commands/version.rb, extended to respect the global
`--output json` option like every other command does - previously
ignored it and always printed plain text. The hidden Markdown renders make
this command a complete release smoke test for the MDEx and Syntect NIFs,
Marcli's syntax-highlighting integration, and the application boot path.
"""
def version(%{options: options}) do
verify_markdown_runtime!()
version = to_string(Application.spec(:linear_cli, :vsn))

if options.output == "json" do
IO.puts(Jason.encode!(%{version: version}))
else
IO.puts(version)
end

:ok
end

defp verify_markdown_runtime! do
theme = Marcli.Theme.default()
elixir = Marcli.render("```elixir\ndef smoke, do: :ok\n```")
ruby = Marcli.render("```ruby\ndef smoke; :ok; end\n```")

elixir_keyword = theme.syntax.keyword_declaration <> "def" <> theme.reset
ruby_keyword = theme.syntax.keyword_type <> "def" <> theme.reset

unless String.contains?(elixir, elixir_keyword) and String.contains?(ruby, ruby_keyword) do
raise "Markdown syntax-highlighting runtime is unavailable"
end

:ok
end

@doc "Ported from commands/team/list.rb. Ruby's `--mine` defaults true."
def team_list(%{flags: flags, options: options}) do
result = if flags.no_mine, do: Linear.teams(), else: Linear.my_teams()

with {:ok, teams} <- result do
Display.show(filter_favorites(teams, flags.all, "team", & &1.key), %{
output: options.output
})

:ok
end
end

@doc "Ported from commands/project/list.rb. Ruby's `--mine` defaults false."
def project_list(%{flags: flags, options: options}) do
with {:ok, projects} <- projects_for(flags, options) do
Display.show(filter_favorites(projects, flags.all, "project", & &1.id), %{
output: options.output
})

:ok
end
end

defp projects_for(_flags, %{team: team_key}) when is_binary(team_key) do
with {:ok, team} <- Linear.find_team(team_key) do
Linear.projects_by_team(team.id)
end
end

defp projects_for(%{mine: true}, _options), do: Linear.my_projects()
defp projects_for(_flags, _options), do: Linear.projects()

@doc """
New in this port - Ruby has no equivalent. Favorites a team
(`LinearCli.Favorites`) - once any team is favorited, `team list`
defaults to showing just favorites (`--all` overrides).
"""
def team_favorite(%{args: %{team: key}}) do
with {:ok, team} <- Linear.find_team(key) do
Favorites.add("team", team.key)
Prompt.ok("Favorited team #{team.key}")
:ok
end
end

@doc "New in this port - Ruby has no equivalent. Un-favorites a team."
def team_unfavorite(%{args: %{team: key}}) do
with {:ok, team} <- Linear.find_team(key) do
Favorites.remove("team", team.key)
Prompt.ok("Un-favorited team #{team.key}")
:ok
end
end

@doc """
New in this port - Ruby has no equivalent. Favorites a project
(`LinearCli.Favorites`), resolved against the active team's projects,
prompting if ambiguous. Team is resolved via `--team`, the active
profile, or an interactive prompt. Once any project is favorited,
`project list` defaults to showing just favorites (`--all` overrides).
"""
def project_favorite(%{args: %{project: search}, options: options}) do
team = WhatFor.team_for(options.team || Profiles.default_team())

with {:ok, projects} <- Linear.projects_by_team(team.id, %{search: search}),
project when not is_nil(project) <- Projects.project_for(projects, search) do
Favorites.add("project", project.id)
Prompt.ok("Favorited project #{project.name}")
:ok
else
nil -> {:error, {:smells_bad, "No project found matching #{search}"}}
{:error, reason} -> {:error, reason}
end
end

@doc "New in this port - Ruby has no equivalent. Un-favorites a project."
def project_unfavorite(%{args: %{project: search}, options: options}) do
team = WhatFor.team_for(options.team || Profiles.default_team())

with {:ok, projects} <- Linear.projects_by_team(team.id, %{search: search}),
project when not is_nil(project) <- Projects.project_for(projects, search) do
Favorites.remove("project", project.id)
Prompt.ok("Un-favorited project #{project.name}")
:ok
else
nil -> {:error, {:smells_bad, "No project found matching #{search}"}}
{:error, reason} -> {:error, reason}
end
end

# Once any favorite of `kind` exists, narrows `records` down to just
# those (matched via `key_fun`) - invisible to anyone who's never
# favorited anything, since an empty favorites list leaves `records`
# untouched. `all?` (the new `--all` flag) always shows everything,
# bypassing the favorites lookup entirely.
defp filter_favorites(records, true, _kind, _key_fun), do: records

defp filter_favorites(records, _all?, kind, key_fun) do
case Favorites.list(kind) do
[] -> records
favorite_values -> Enum.filter(records, &(key_fun.(&1) in favorite_values))
end
end

@doc """
New in this port - Ruby has no equivalent. Posts a status update
(Linear's own "Project Update" feature - a journal-style status post,
not an edit to the project's own fields) via the projectUpdateCreate
mutation. `PROJECT` is resolved against the active team's projects,
prompting if ambiguous. Team is resolved via `--team`, the active
profile, or an interactive prompt.
"""
def project_update(%{args: %{project: search}, options: options}) do
team = WhatFor.team_for(options.team || Profiles.default_team())

with {:ok, projects} <- Linear.projects_by_team(team.id, %{search: search}),
project when not is_nil(project) <- Projects.project_for(projects, search),
{:ok, update} <-
Linear.post_project_update(project.id, options.body, %{health: options.health}) do
Display.show(update, %{output: options.output})
:ok
else
nil -> {:error, {:smells_bad, "No project found matching #{search}"}}
{:error, reason} -> {:error, reason}
end
end

@doc """
New in this port - Ruby has no equivalent. Saves a new named
team/project bundle (`LinearCli.Profiles.create/2`) that `profile use`
can later switch to.
"""
def profile_create(%{args: %{name: name}, options: options}) do
case Profiles.create(name, team: options.team, project: options.project) do
{:ok, profile} ->
Display.show(profile, %{output: options.output})
:ok

{:error, reason} ->
{:error, reason}
end
end

@doc "New in this port - Ruby has no equivalent. Lists every saved profile."
def profile_list(%{options: options}) do
Display.show(Profiles.list(), %{output: options.output})
:ok
end

@doc """
New in this port - Ruby has no equivalent. Switches the active profile -
its team/project become the defaults `issue create`/`issue list` fall
back to when `--team`/`--project` are omitted.
"""
def profile_use(%{args: %{name: name}}) do
case Profiles.activate(name) do
:ok ->
Prompt.ok("Switched to profile #{name}")
:ok

{:error, :not_found} ->
{:error, {:smells_bad, "No profile named #{name}"}}
end
end

@doc "New in this port - Ruby has no equivalent. Shows the active profile, if any."
def profile_show(%{options: options}) do
case Profiles.active() do
nil -> Prompt.warn("No active profile")
profile -> Display.show(profile, %{output: options.output})
end

:ok
end

@doc "New in this port - Ruby has no equivalent. Deactivates the active profile without deleting it."
def profile_clear(_result) do
Profiles.clear()
Prompt.ok("Cleared active profile")
:ok
end

@doc "New in this port - Ruby has no equivalent. Deletes a saved profile."
def profile_delete(%{args: %{name: name}}) do
case Profiles.delete(name) do
:ok ->
Prompt.ok("Deleted profile #{name}")
:ok

{:error, :not_found} ->
{:error, {:smells_bad, "No profile named #{name}"}}
end
end

@doc """
Ported from commands/issue/list.rb + operations/issue/list.rb.

Expand Down
Loading