diff --git a/AGENTS.md b/AGENTS.md index 2095aa1..41f063a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,8 @@ and best practices for agents to follow. - Phase 14 Plan: documents/phase-14-plan.adoc - Phase 15 Plan: documents/phase-15-plan.adoc - Phase 16 Plan: documents/phase-16-plan.adoc +- Phase 17 Plan: documents/phase-17-plan.adoc +- Phase 18 Plan: documents/phase-18-plan.adoc - Burrito distribution decision: documents/burrito-decision.adoc - Development and CI quality gates decision: documents/quality-gates-decision.adoc diff --git a/app/mix.lock b/app/mix.lock index 436fe87..31cc2ad 100644 --- a/app/mix.lock +++ b/app/mix.lock @@ -20,7 +20,7 @@ "glob_ex": {:hex, :glob_ex, "0.1.12", "7b2d9369c20e2697efcfd185d13d6e84c94cd3bfd2730fbde613141c2e015c00", [:mix], [], "hexpm", "2e2fac83f113514434c7eaf267b4c38af2f91766f1cab2c5db7053b7fc1ee0bb"}, "hex_core": {:hex, :hex_core, "0.15.0", "8eadc0ccb08e3742f2313073d04f39eaa7904617329039e9d3c402f5dd227673", [:rebar3], [], "hexpm", "c2093764c7af8ef0818c104fa141eba431e7be93f8374638c45c7037b26a52f8"}, "hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"}, - "igniter": {:hex, :igniter, "0.8.3", "9de74d3885efae43b0b58dc6f7b816963c4bbd391e6b6fe6922ee21c4e384c76", [:mix], [{:ex_ast, "~> 0.5", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.5", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "afc5e3848d885e680da5c3b65e5e7717555a08cd12305190ff2be76427af39ff"}, + "igniter": {:hex, :igniter, "0.8.4", "f79f1bbdc2fb7b9ca030a22d12a585b060cbf5b94b9d3f23b1148578a9e05d11", [:mix], [{:ex_ast, "~> 0.5", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.5", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "a9b1cbec996ccb100b4f7d8130129b2dd3f18eb4224ac9a0e907e428ca90dbd7"}, "iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"}, "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, "makeup": {:hex, :makeup, "1.2.2", "882d46dc0905e9ff7abf2aab61a7e6b3dcc555533977d8a23b06019e6c89ac94", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "9a1a24e5b343b8ae16abea0822c10a6f75da27af7fa802ada5251f7579bfccfa"}, @@ -57,7 +57,7 @@ "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, - "usage_rules": {:hex, :usage_rules, "1.2.7", "aaacfc9eda3b33d37703ed2321f32ad89845282513168f8592a1826eeabf50a7", [:mix], [{:igniter, ">= 0.6.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "8601999d754974f361f1fd816f0747dd7ea39aa04b1bf6244d158a9616be204e"}, + "usage_rules": {:hex, :usage_rules, "1.2.8", "ace34d6e7a2a300a03446387ffd6a2219418fb6dbe7ee5d02131535fc9b3d8db", [:mix], [{:igniter, ">= 0.6.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "f49a69c7219a5b97bfb7e97d4470d790a94ac2cf049d2b1ceda6bd2b243e9cc1"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "yaml_elixir": {:hex, :yaml_elixir, "2.12.2", "9dd1330fb4cd9a36a7b0f502e5b12486eff632792ee4a5f0eba52a4d4ec32c9c", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "e7c1b10122f973e6558462d51c39026ba0e14afbc6745318e990ea82cfe9e159"}, "ymlr": {:hex, :ymlr, "5.1.6", "247479328fd5ea1e222ee59ecd5571ab2885cf1a6329aea458d4944cd035cfc1", [:mix], [], "hexpm", "b36d7c72384d2cb2094d2a3c8b0c3b12303e0ccd1b7b12333b81d75b1fe0b53a"}, diff --git a/documents/phase-18-plan.adoc b/documents/phase-18-plan.adoc new file mode 100644 index 0000000..662c082 --- /dev/null +++ b/documents/phase-18-plan.adoc @@ -0,0 +1,412 @@ += {my-title} +Tj Vanderpoel (bougyman) +:revdate: Sep 11, 2026 +:my-title: Phase 18 plan: split issue command helpers by workflow +:icons: font +:env-github: +ifdef::env-github[] +:tip-caption: :bulb: +:note-caption: :information_source: +:important-caption: :heavy_exclamation_mark: +:caution-caption: :fire: +:warning-caption: :warning: +endif::[] +:toc: + +== Goal + +Replace the monolithic `LinearCli.CLI.IssueHelpers` and +`LinearCli.CLI.Commands` modules with small, workflow-oriented CLI modules. +Preserve every existing command, output, interactive prompt, error shape, and +return contract while making each issue workflow independently understandable +and testable. + +This is a structural refactor. It introduces no user-facing command, no +Linear GraphQL operation, no Ash resource/action/code interface, and no change +to the persisted profile or favorites format. + +== Context + +`app/lib/linear_cli/cli/issue_helpers.ex` is 618 lines and exposes fourteen +public operations. Its 916-line test file covers comment, close/cancel, +workflow-state selection, pull requests, project attachment, update dispatch, +creation, bare-ID expansion, and self-assignment. + +`app/lib/linear_cli/cli/commands.ex` is larger still at 1,440 lines. It owns +unrelated top-level commands (whoami/version, team, project, and profile) and +every issue command from list/view through relation management. Its +`issue_commands_test.exs` counterpart is 4,877 lines. Both files have become +the default landing place for unrelated command work simply because their +names are broad. + +The module started as a 401-line Phase 6 port of Ruby's single +`CLI::Issue` object. Subsequent phases correctly reused its available entry +points, but added distinct responsibilities: + +* Phase 9 added active-profile defaults to issue creation. +* Phase 11 added bare numeric-ID expansion through profiles, favorites, and a + team prompt. +* Phase 12 added project moves using a pre-resolved project. +* Later issue work added non-interactive creation and status resolution. + +The result is not a set of closely related low-level helpers. It is the CLI +orchestration boundary for several independent issue workflows. The generic +`IssueHelpers` name obscures that boundary and makes routine changes touch a +large, highly coupled source and test file. + +== Decisions + +=== Keep the behavior in the CLI layer + +The split remains under `LinearCli.CLI`. These functions resolve interactive +input through `WhatFor`/`Prompt`, print confirmations, and consult `Profiles` +and `Favorites`. Moving them into `LinearCli.Linear` would make the Ash domain +aware of terminal interaction and local CLI defaults, breaking the established +top-to-bottom layering. + +The domain continues to own only API-facing actions such as +`Linear.create_issue/4`, `Linear.assign_issue/3`, +`Linear.workflow_states_by_team/1`, and `Linear.attach_issue_to_project/2`. + +=== Split by durable user workflow, not one module per function + +Create these modules in `app/lib/linear_cli/cli/issue/`: + +[cols="1,2,3", options="header"] +|=== +| Module +| Public responsibility +| Existing `IssueHelpers` responsibilities moved there + +| `LinearCli.CLI.Issue.Identifiers` +| Normalize an issue identifier before lookup. +| `expand_issue_id/1`, including active-profile, favorite-team, and prompt + fallback. + +| `LinearCli.CLI.Issue.WorkflowStates` +| Resolve a completed, cancelled, or explicitly named workflow state for an + already-loaded issue. +| `cancelled_state_for/2`, `completed_state_for/2`, and the exact/unique-prefix + matching used by assignment status selection. + +| `LinearCli.CLI.Issue.Actions` +| Perform mutations on an already-loaded issue and coordinate `issue update`. +| Comment, close, cancel, description update, project attachment/move, and + update precedence/normalization. + +| `LinearCli.CLI.Issue.Creation` +| Create an issue interactively or under `--yes`. +| `make_da_issue!/1`, required-field validation, strict team resolution, and + optional-project parameter construction. + +| `LinearCli.CLI.Issue.Assignment` +| Look up an issue and assign it to the current user when needed. +| `gimme_da_issue!/2`, viewer resolution, status-ID resolution, and + already-assigned confirmation. + +| `LinearCli.CLI.Issue.PullRequest` +| Gather PR text and invoke `gh pr create`. +| `create_pr!/3` and `issue_pr/2`. +|=== + +`Actions` deliberately groups mutations on an already-resolved issue rather +than splitting comment, lifecycle, project, and update-dispatch code into +single-function modules. That is the smallest useful boundary: they share +confirmation/error conventions and `update_issue/2`'s documented precedence. + +=== Remove the generic facade after one coordinated call-site migration + +Do not retain `IssueHelpers` as a permanent delegating compatibility facade. +All production callers are internal to this application, so a facade would +leave the misleading abstraction in place and add one more indirection without +protecting a supported external API. + +Migrate `Commands` and the one incidental internal reference directly to the +new modules in the same change, then delete `issue_helpers.ex`. Use local +aliases in `Commands` so command bodies remain readable. The resulting names +retain the established operation names within an explicit workflow namespace: +`Identifiers.expand_issue_id/1`, `Creation.make_da_issue!/1`, and +`Assignment.gimme_da_issue!/2`, for example. Update their specs and docs in +the same change. + +The refactor must not leave duplicate implementations or compatibility wrappers +whose sole purpose is forwarding an old helper name. + +=== Preserve contracts exactly before improving them separately + +This phase preserves current quirks and contracts, including: + +* `Actions.update/2` returns `:ok | {:error, reason}`, while the other + mutation functions retain their existing `{:ok, value} | {:error, reason}` + shapes. +* Closing/cancelling remains idempotent; the same messages and `:trash`, + `:reason`, `:status`, and `:cancel` behavior remain intact. +* Workflow states continue to select a sole result directly, prompt among + several results without an explicit status, and accept case-insensitive + exact or unique-prefix status names when supplied. +* Bare numeric IDs retain the resolution order: active profile, one/many team + favorites, then `WhatFor.ask_for_team/0`. Non-numeric IDs still pass through + unchanged. +* Interactive creation and `--yes` creation keep their present handling of + labels, project defaults, strict project lookup, and tagged `:smells_bad` + errors. +* Pull-request invocation keeps its injectable runner and current stdout + behavior; this refactor must not change shell command arguments. + +Potential UX changes such as making unmatched project selection safe, changing +the `smells_bad` vocabulary, or redesigning `issue update` precedence require +their own approved plan and must not be folded into this move-only work. + +=== Split commands by CLI command family and remove the forwarding facade + +Create these modules in `app/lib/linear_cli/cli/commands/`: + +[cols="1,3", options="header"] +|=== +| Module +| Commands owned + +| `LinearCli.CLI.Commands.System` +| `whoami` and `version`. + +| `LinearCli.CLI.Commands.Teams` +| team list, favorite, and unfavorite. + +| `LinearCli.CLI.Commands.Projects` +| project list, favorite, unfavorite, and update. + +| `LinearCli.CLI.Commands.Profiles` +| profile create, list, use, show, clear, and delete. + +| `LinearCli.CLI.Commands.Issues.Read` +| issue list and view. + +| `LinearCli.CLI.Commands.Issues.Create` +| issue create. + +| `LinearCli.CLI.Commands.Issues.Development` +| issue develop, PR, and take. + +| `LinearCli.CLI.Commands.Issues.Mutations` +| issue update, comment, status, and assign. + +| `LinearCli.CLI.Commands.Issues.Move` +| issue move, including its one- and bulk-project modes. + +| `LinearCli.CLI.Commands.Issues.Relations` +| issue relation list, add, and remove, including their shared result + rendering and safe multi-target behavior. +|=== + +The goal is cohesive command-family files, not a file per leaf command. +`Relations` and `Move` are intentionally separate because each has substantial +command-specific resolution/reporting logic. `Mutations` keeps commands that +change an already-resolved issue but do not constitute a larger standalone +workflow. If any resulting module remains difficult to navigate, split it at a +real subcommand boundary in the same namespace rather than restoring a generic +catch-all module. + +`LinearCli.CLI` is the sole production dispatcher for the current +`Commands.*` functions. Update it to alias and call the family modules +directly, then delete `commands.ex`; do not retain a permanent +`LinearCli.CLI.Commands` forwarding facade. Update tests to call the owning +module directly. This is internal code, not a supported library API. + +== Verified call sites and dependency direction + +`LinearCli.CLI.Commands` is the substantive production consumer of +`IssueHelpers`; it expands identifiers for list/update/status-style flows, +creates issues, takes/develops issues through self-assignment, dispatches +updates, and opens pull requests. `LinearCli.CLI` is the sole production +consumer of `Commands`; it dispatches every parsed command through its public +functions. `LinearCli.Profiles` mentions creation only in documentation; it +has no runtime dependency requiring a compatibility facade. The existing test +suite is the other direct consumer and must be split with the code. + +The dependency direction after the change is: + +---- +Commands + -> CLI.Issue.{Identifiers, Creation, Assignment, Actions, PullRequest} + -> CLI.Issue.WorkflowStates (Actions and Assignment only) + -> CLI.{WhatFor, Projects, Prompt} and {Profiles, Favorites} + -> Linear (domain code interfaces) +---- + +After the command split, `CLI` replaces the first line with direct calls to +`CLI.Commands.{System, Teams, Projects, Profiles, Issues.*}`. Command-family +modules may depend on the focused `CLI.Issue.*` workflow modules, display and +prompt helpers, and `Linear`; they must not call one another through a deleted +`Commands` facade. + +`WorkflowStates` may depend on `Prompt` and `Linear`, but it must not depend +on `Actions` or `Assignment`. `Identifiers` must remain independent of issue +lookup and assignment. This keeps the modules acyclic and prevents a new +catch-all module from re-forming around shared private functions. + +== Building blocks + +=== 1. Establish the namespace and move pure orchestration boundaries + +Add the six modules under `cli/issue/` with focused moduledocs, specs, and only +the aliases each module needs. Move code without semantic edits first. Private +helpers move beside the public workflow that owns them; do not make private +helpers public merely to avoid moving a few lines. + +Where both lifecycle and assignment need state-name matching, expose one small, +well-named `WorkflowStates` operation rather than copying its +case-insensitive exact/unique-prefix algorithm. Keep the distinct lifecycle +selection rule (sole state/prompt/no matching state) in that module too, since +it is one workflow-state policy. + +=== 2. Migrate CLI command wiring + +Replace the `IssueHelpers` alias in `Commands` with aliases for the specific +workflow modules. Update every call site in one coherent change: + +* all bare-ID expansion uses `Identifiers`; +* issue create uses `Creation`; +* take/develop/PR preparation uses `Assignment`; +* update/comment/project/lifecycle work uses `Actions`; and +* PR creation uses `PullRequest`. + +Use the resulting explicit names in command moduledocs and nearby comments. +Update references in `display.ex`, `profiles.ex`, and any other internal +documentation so no stale `IssueHelpers` module reference remains. + +Delete `app/lib/linear_cli/cli/issue_helpers.ex` only after `rg` confirms no +production, test, or documentation reference remains, except the historical +context in this plan and prior plans. + +=== 3. Split helper tests by the same boundary + +Replace `issue_helpers_test.exs` with focused test modules: + +* identifiers and workflow-state resolution; +* actions/update dispatch; +* creation; +* assignment; and +* pull-request behavior. + +Extract only genuinely shared test fixtures and request stubbing into a small +test-support module. It may provide constructed issue/team/user records and +the existing operation-name-based GraphQL response stub, but it must not hide +the behavior each test is asserting. Tests keep their current async safety and +must not introduce shared mutable profile/favorites state outside the existing +profile-aware test setup. + +Do not duplicate command-level coverage. The refactor must retain the targeted +unit coverage for each workflow and the existing command tests prove that the +renamed wiring still reaches the same authoritative GraphQL boundary. + +=== 4. Extract command families and migrate the dispatcher + +Move each `Commands` function with its private helpers into the command-family +module that owns its user-facing command. Preserve injectable options used by +tests (`cwd`, `me`, `runner`, and `halt` where applicable), function arities, +and parser-result input shapes. Private helpers stay with their command family; +they do not become broad shared utilities merely to reduce a few lines of +repetition. + +Update `CLI`'s dispatch clauses directly to the new modules. Keep parsing, +dispatch selection, `run/3`, and central error handling in `CLI`; this phase +does not move Optimus specifications or alter command dispatch semantics. + +Split `issue_commands_test.exs` into focused command-family test modules: +read/create/development, mutations, move, and relations. Extract only shared +fixture and Req-test response helpers needed by more than one module. Also +move the smaller favorites command tests to the owning teams/projects modules +when doing so removes a stale `Commands` reference. Preserve profile-default +coverage in its existing focused test module while updating its aliases. + +Delete `app/lib/linear_cli/cli/commands.ex` and its old giant issue-command +test file only after all dispatch, production, test, and documentation +references have moved. + +== Tests and verification + +Run the app formatter and the focused replacement test modules while moving +each workflow. Before considering the work complete, run the complete app +suite and the repository's normal local gate. + +At the behavioral boundary, compare the pre- and post-refactor test coverage +for all of these cases: + +* comment success/error; close/cancel success, idempotency, state selection, + trash option, and error propagation; +* `issue update` comment-first precedence, every recognized action, description + update, and no-option result; +* interactive and `--yes` issue creation, required fields, profile defaults, + strict team/project failures, labels, and API errors; +* bare and already-expanded/UUID identifiers, each profile/favorites/prompt + fallback, self-assignment, explicit status ID, exact status, unique prefix, + unknown status, and ambiguous status; and +* injectable PR runner, generated title/body selection, and emitted output; + and +* every top-level/team/project/profile/issue dispatch route, including bulk + move and multi-target relation reporting, with unchanged human and JSON + output contracts. + +Use `git diff --check` and `rg` for stale `IssueHelpers` references. No live +Linear mutation is needed: the existing Req-test GraphQL stubs are the +authoritative boundary for this move-only refactor. + +== Documentation + +Add this Phase 18 document to `AGENTS.md`'s plan index as part of planning. +The implementation updates code moduledocs, stale cross-references, and the +current `AGENTS.md` CLI-layer diagram so it names `CLI.Commands.*` and +`CLI.Issue.*` rather than the deleted broad modules. It does not change the +README because CLI behavior and command syntax are unchanged. + +Do not update `documents/ash-domain-erd.adoc`: no Ash resource, action, code +interface, association, or shared domain helper changes in this phase. If +implementation discovers a necessary domain change, stop and plan that change +separately rather than silently expanding this refactor. + +== Linear issue decomposition + +1. *Introduce issue identifiers and workflow-state modules.* Move bare-ID + expansion and all workflow-state selection/matching code, update direct + callers, and split their focused tests. This establishes the shared + dependency used by later work. +2. *Extract issue lifecycle actions.* Move comment, close/cancel, description, + project move/attachment, and update-dispatch behavior into `Actions`; update + command wiring and tests. Depends on issue 1 for workflow-state resolution. +3. *Extract issue creation and assignment workflows.* Move both interactive and + `--yes` creation plus self-assignment/status handling into `Creation` and + `Assignment`; migrate callers and focused tests. Depends on issue 1; + otherwise independent of issue 2. +4. *Extract pull-request workflow and remove IssueHelpers.* Move PR behavior, + complete the coordinated helper call-site/documentation migration, delete + the old module and test file, and verify no stale helper references. Depends + on issues 2 and 3. +5. *Extract non-issue command families.* Move system, team, project, and + profile commands; rewire their `CLI` dispatch clauses and focused tests. + This is independent of issues 1-4. +6. *Extract ordinary issue command families.* Move read, create, development, + and mutation commands; rewire dispatch and split their tests. Depends on + issues 1-4 because the commands use the focused issue workflows. +7. *Extract issue move and relation commands; remove Commands.* Move the move + and relation families, migrate remaining dispatch/test/documentation + references, delete `commands.ex` and the giant issue-command test file, run + complete validation, and confirm no stale `Commands` references remain. + Depends on issues 5 and 6. + +== Acceptance criteria + +* `IssueHelpers`, `Commands`, and their monolithic test files are deleted; no + non-historical source, test, or documentation reference remains. +* Each replacement module has one workflow responsibility and the dependency + graph remains acyclic. +* Every existing CLI command keeps its command syntax, prompts, output, + GraphQL calls, return shapes, and error behavior. +* Workflow-state matching exists in exactly one implementation and is reused by + lifecycle and assignment flows. +* Focused tests retain all current behavior coverage without unnecessary + duplicated stubbing or command-level tests. +* The complete app suite and normal repository local gate pass, and + `git diff --check` is clean. +* `AGENTS.md` indexes this plan and describes the replacement CLI modules; no + Ash-domain document is changed unless the scope legitimately expands through + a separately approved plan. diff --git a/lib/mix/tasks/appdeps.update.ex b/lib/mix/tasks/appdeps.update.ex new file mode 100644 index 0000000..c3d920f --- /dev/null +++ b/lib/mix/tasks/appdeps.update.ex @@ -0,0 +1,30 @@ +defmodule Mix.Tasks.Appdeps.Update do + @shortdoc "Updates one or more app dependencies" + + @moduledoc """ + #{@shortdoc}. + + mix appdeps.update DEP [DEP ...] + + Runs `mix deps.update` for the named dependencies from the `app/` project. + """ + + use Mix.Task + + alias RepoTasks.Shell + + @impl Mix.Task + def run(argv) do + run(argv, &Shell.run!/3) + end + + @doc false + def run([], _shell) do + Mix.raise("Usage: mix appdeps.update DEP [DEP ...]") + end + + def run(dependencies, shell) do + shell.("mix", ["deps.update" | dependencies], cd: "app") + :ok + end +end diff --git a/test/mix/tasks/appdeps.update_test.exs b/test/mix/tasks/appdeps.update_test.exs new file mode 100644 index 0000000..217f29b --- /dev/null +++ b/test/mix/tasks/appdeps.update_test.exs @@ -0,0 +1,23 @@ +defmodule Mix.Tasks.Appdeps.UpdateTest do + use ExUnit.Case, async: true + + alias Mix.Tasks.Appdeps.Update + + test "updates the named dependencies from app" do + caller = self() + + shell = fn cmd, args, opts -> + send(caller, {:run, cmd, args, opts}) + :ok + end + + assert :ok = Update.run(["ash", "oban"], shell) + assert_received {:run, "mix", ["deps.update", "ash", "oban"], [cd: "app"]} + end + + test "requires at least one dependency" do + assert_raise Mix.Error, "Usage: mix appdeps.update DEP [DEP ...]", fn -> + Update.run([], fn _, _, _ -> :ok end) + end + end +end