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: 32 additions & 0 deletions Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,38 @@ You can find your API key in your https://linear.app/settings/api[Linear Setting

=== Commands

==== Command Aliases

Short aliases are available for the top-level commands and some subcommands,
so you don't have to type the full name every time:

[cols="1,2"]
|===
|Command |Aliases

|`whoami` |`me`, `w`, `who`, `whodat`
|`version` |`v`
|`issue` |`i`, `issues`
|`team` |`t`, `teams`
|`project` |`p`, `projects`
|`issue create` |`c`, `new`, `add`
|`issue develop` |`d`, `dev`
|`issue list` |`l`, `ls`
|`issue update` |`u`
|`issue pr` |`pull-request`
|`team list` |`l`, `ls`
|`project list` |`l`, `ls`
|===

[source,sh]
----
$ lc w --teams
$ lc i ls
$ lc i dev CRY-1234
----

`issue take` has no alias.

==== Help

You can get help/usage for any command or subcommand by using the `--help` flag.
Expand Down
64 changes: 63 additions & 1 deletion app/lib/linear_cli/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ defmodule LinearCli.CLI do
alias LinearCli.CLI.Commands

def main(argv, halt \\ &System.halt/1) do
argv = argv |> normalize_aliases() |> normalize_help() |> default_to_issue_list()
argv =
argv
|> normalize_aliases()
|> normalize_subcommand_aliases()
|> normalize_help()
|> default_to_issue_list()

# Optimus.parse!/3 returns *either* {subcommand_path, parse_result}
# (a subcommand matched) *or* a bare %Optimus.ParseResult{} (nothing
Expand Down Expand Up @@ -58,6 +63,63 @@ defmodule LinearCli.CLI do
Enum.map(argv, &Map.get(@flag_aliases, &1, &1))
end

# Optimus's subcommand spec has no alias mechanism either (verified:
# vendor/optimus/lib/optimus/subcommand.ex), but Ruby registers every one
# of these via each command module's `ALIASES` constant (cli.rb's
# `register`/`register_sub!`) - real, user-facing shortcuts people type
# (`lc i dev CRY-37`). Sourced from those constants directly, not the
# Readme's prose list, which was missing several (`new`/`add`/`me`/`who`/
# `teams`/`projects`/`issues`/`t`/`p`/`v`). `completion` (dry-cli-specific)
# and `console`/`pry` (Ruby dev tooling) have no equivalent here, so
# they're intentionally not included.
@command_aliases %{
"me" => "whoami",
"w" => "whoami",
"who" => "whoami",
"whodat" => "whoami",
"v" => "version",
"i" => "issue",
"issues" => "issue",
"t" => "team",
"teams" => "team",
"p" => "project",
"projects" => "project"
}

# `take` has no alias in Ruby either - every issue subcommand not listed
# here (just `take`) is only ever reachable by its canonical name there
# too, so this port doesn't need to invent one.
@subcommand_aliases %{
"issue" => %{
"c" => "create",
"new" => "create",
"add" => "create",
"d" => "develop",
"dev" => "develop",
"l" => "list",
"ls" => "list",
"u" => "update",
"pull-request" => "pr"
},
"team" => %{"l" => "list", "ls" => "list"},
"project" => %{"l" => "list", "ls" => "list"}
}

@doc false
def normalize_subcommand_aliases([first | rest]) do
canonical_first = Map.get(@command_aliases, first, first)

case {Map.fetch(@subcommand_aliases, canonical_first), rest} do
{{:ok, sub_aliases}, [second | more]} ->
[canonical_first, Map.get(sub_aliases, second, second) | more]

_ ->
[canonical_first | rest]
end
end

def normalize_subcommand_aliases(argv), do: argv
Comment on lines +108 to +121

# Ported from exe/scripts/lc.sh's own `[ "$#" -eq 0 ]` branch exactly
# (including its stderr text) - a bare `lc` invocation defaults to
# `issue list` rather than dumping top-level help.
Expand Down
48 changes: 48 additions & 0 deletions app/test/linear_cli/cli_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,54 @@ defmodule LinearCli.CLITest do
assert LinearCli.CLI.normalize_aliases(["whoami"]) == ["whoami"]
end

test "subcommand aliases (#14) rewrite to their canonical top-level/subcommand names" do
# Sourced from Ruby's own ALIASES constants (cli.rb/commands/*.rb), not
# the Readme's prose list. Direct unit tests on the pure rewrite, same
# rationale as the flag-alias test above.
assert LinearCli.CLI.normalize_subcommand_aliases(["i", "dev", "CRY-37"]) ==
["issue", "develop", "CRY-37"]

assert LinearCli.CLI.normalize_subcommand_aliases(["issues", "l"]) == ["issue", "list"]
assert LinearCli.CLI.normalize_subcommand_aliases(["t", "ls"]) == ["team", "list"]
assert LinearCli.CLI.normalize_subcommand_aliases(["p", "l"]) == ["project", "list"]
assert LinearCli.CLI.normalize_subcommand_aliases(["w", "-t"]) == ["whoami", "-t"]
assert LinearCli.CLI.normalize_subcommand_aliases(["v"]) == ["version"]

assert LinearCli.CLI.normalize_subcommand_aliases(["i", "pull-request", "CRY-1"]) ==
["issue", "pr", "CRY-1"]

# `take` has no alias in Ruby either - unaliased subcommands pass through.
assert LinearCli.CLI.normalize_subcommand_aliases(["issue", "take", "CRY-1"]) ==
["issue", "take", "CRY-1"]

assert LinearCli.CLI.normalize_subcommand_aliases([]) == []
end

test "aliased subcommands actually dispatch end to end" do
assert capture_io(fn -> LinearCli.CLI.main(["w"]) end) =~ "Ada"
assert capture_io(fn -> LinearCli.CLI.main(["i", "ls"]) end) =~ "CRY-1"
assert capture_io(fn -> LinearCli.CLI.main(["t", "l"]) end) =~ "Engineering"
assert capture_io(fn -> LinearCli.CLI.main(["p", "ls"]) end) =~ "Manhattan"
end

test "an aliased subcommand composes correctly with --help" do
# normalize_subcommand_aliases/1 has to run before normalize_help/1 -
# Optimus's `help <path>` only recognizes canonical subcommand names,
# not aliases, so "i dev --help" must become "help issue develop", not
# a broken "help i dev". Same fake-halt/CaseClauseError artifact as the
# "issue list --help" test above.
output =
capture_io(fn ->
try do
LinearCli.CLI.main(["i", "dev", "--help"], fn _code -> :ok end)
rescue
CaseClauseError -> :ok
end
end)

assert output =~ "Start or update development status of an issue"
end

test "a catch-all error halts with exit code 88" do
# A malformed API response (neither "data" nor "errors") makes
# LinearCli.Api return {:error, {:unexpected_response, body}}, which Ash
Expand Down
Loading