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
11 changes: 11 additions & 0 deletions Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,17 @@ on every commit, and again on every commit about to be pushed):
$ mix setup
----

Run the development checkout directly from the repository root with `mix lc`.
It compiles and starts the application as needed, then forwards arguments,
interactive input, output, and exit status to the same CLI entry point used by
the release binary:

[source,sh]
----
$ mix lc whoami
$ mix lc issue list --output json
----

The project uses ExUnit and `mix format`. Run tests with:

[source,sh]
Expand Down
12 changes: 7 additions & 5 deletions app/usage-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@
- There's no escript build (removed - NIF-backed deps like `exqlite`
can't load from inside an escript archive, so it never actually
worked once `exqlite` was added). Without a full `mix release lc`
build, invoke `lc` from `app/` via `mix run`:
build, invoke the development checkout from the repo root through its
proxy task:

mix run -e 'LinearCli.CLI.main(["issue", "list"])'
mix lc issue list

`main/2`'s default `halt` (`System.halt/1`) is fine to leave as-is —
only error paths call it, so a successful command just returns and
`mix run` exits 0 normally with real output and real exit codes.
`mix lc` compiles and starts the `app/` project as needed, forwards all
arguments and standard streams to `LinearCli.CLI.main/1` (including
interactive prompt input), and preserves its exit code. It also suppresses
child Mix progress messages so `--output json` stays machine-readable.

## Accessibility

Expand Down
86 changes: 86 additions & 0 deletions lib/mix/tasks/lc.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
defmodule Mix.Tasks.Lc do
@shortdoc "Runs this checkout's Linear CLI"

@moduledoc """
#{@shortdoc}.

mix lc [LC_ARGS...]

Proxies every argument to `LinearCli.CLI.main/1` in the sibling `app/`
project. The child `mix run` compiles and starts the application when
needed, so contributors can exercise the development checkout from the
repository root with the same arguments they would give an installed
`lc` binary:

mix lc issue list
mix lc whoami --output json

Standard input, standard output, and standard error are inherited by the
child, so interactive prompts work normally and the two output streams
remain separate. The child CLI's exit status is preserved. Child Mix
progress messages are suppressed so they do not corrupt `--output json`;
diagnostics from `lc` itself are not suppressed.
"""

use Mix.Task

@entrypoint "LinearCli.CLI.main(System.argv())"

@impl Mix.Task
def run(argv) do
run(argv, &run_child/3, &System.halt/1)
end

@doc false
def run(argv, command, halt) do
args = ["run", "-e", @entrypoint, "--" | argv]

command_opts = [
cd: Path.expand("app"),
env: [{"MIX_QUIET", "1"}],
stdio: :inherit
]

status = command.("mix", args, command_opts)

if status != 0 do
halt.(status)
end

:ok
end

@doc false
def run_child(executable, args, opts) do
:inherit = Keyword.fetch!(opts, :stdio)

executable =
System.find_executable(executable) ||
Mix.raise("could not find #{executable} on PATH")

port =
Port.open(
{:spawn_executable, executable},
[
# Keep file descriptors 0, 1, and 2 attached to the caller. The
# port uses its auxiliary descriptors only to report lifecycle
# events, which preserves prompts and keeps stdout/stderr separate.
:nouse_stdio,
:exit_status,
args: args,
cd: Keyword.fetch!(opts, :cd),
env: port_env(Keyword.fetch!(opts, :env))
]
)

receive do
{^port, {:exit_status, status}} -> status
end
end

defp port_env(environment) do
Enum.map(environment, fn {name, value} ->
{String.to_charlist(name), String.to_charlist(value)}
end)
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defmodule RepoTasks.MixProject do
# things app/'s own mix.exs has no business knowing about). Kept
# dependency-free on purpose: every task here just orchestrates other
# already-existing tools (mix release inside app/, the ci/*.sh scripts)
# via System.cmd/3, never runs anything in-process.
# as child OS processes, never runs them in-process.
def project do
[
app: :repo_tasks,
Expand Down
51 changes: 51 additions & 0 deletions test/mix/tasks/lc_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
defmodule Mix.Tasks.LcTest do
use ExUnit.Case, async: true

alias Mix.Tasks.Lc

test "forwards lc arguments unchanged through the app project" do
caller = self()

command = fn executable, args, opts ->
send(caller, {:command, executable, args, opts})
0
end

halt = fn status -> flunk("unexpected halt with status #{status}") end

assert :ok = Lc.run(["issue", "create", "title with spaces"], command, halt)

assert_receive {:command, "mix",
[
"run",
"-e",
"LinearCli.CLI.main(System.argv())",
"--",
"issue",
"create",
"title with spaces"
], opts}

assert opts[:cd] == Path.expand("app")
assert opts[:env] == [{"MIX_QUIET", "1"}]
assert opts[:stdio] == :inherit
end

test "preserves the child lc exit status" do
caller = self()
command = fn _executable, _args, _opts -> 66 end
halt = fn status -> send(caller, {:halt, status}) end

assert :ok = Lc.run(["issue", "view", "missing"], command, halt)
assert_receive {:halt, 66}
end

test "returns the exact exit status from an inherited-stdio child" do
assert 37 ==
Lc.run_child("sh", ["-c", "exit 37"],
cd: File.cwd!(),
env: [],
stdio: :inherit
)
end
end