feat(cli/dir): dirctl install list, agents, outdated, and pin/unpin - #2132
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
🟡 Changes recommended
No-op installs fail to reconcile the manifest, and recorded uninstall has selection, digest, and failure-handling correctness issues.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds manifest-driven package inspection, update checks, pinning, and offline global uninstall behavior to dirctl.
Changes:
- Adds
install agents,list,outdated,pin, andunpin. - Introduces package update comparison and artifact inspection.
- Makes global uninstall manifest-driven and simplifies no-op reporting.
File summaries
| File | Description |
|---|---|
docs/content/dir/dir-features-scenarios.md |
Updates install command guidance. |
docs/content/dir/dir-cli-reference.md |
Documents package-management commands. |
cli/util/reference/reference.go |
Simplifies resolver errors. |
cli/internal/pkgupdate/pkgupdate.go |
Adds update comparison engine. |
cli/internal/pkgupdate/pkgupdate_test.go |
Tests update statuses. |
cli/internal/pkgstate/pkgstate.go |
Adds context and artifact-kind metadata. |
cli/internal/pkgstate/pkgstate_test.go |
Tests new manifest fields. |
cli/internal/agentinstall/remove.go |
Adds manifest-driven removal. |
cli/internal/agentinstall/remove_test.go |
Tests recorded removal. |
cli/internal/agentinstall/installed.go |
Inspects recorded artifacts. |
cli/internal/agentinstall/installed_test.go |
Tests artifact inspection. |
cli/internal/agentcfg/summary.go |
Omits unchanged summary lines. |
cli/internal/agentcfg/summary_test.go |
Tests summary filtering. |
cli/internal/agentcfg/selection.go |
Adds agent lookup by ID. |
cli/internal/agentcfg/result.go |
Adds change detection helpers. |
cli/internal/agentcfg/plan.go |
Omits unchanged plan lines. |
cli/internal/agentcfg/plan_test.go |
Tests plan filtering. |
cli/cmd/root.go |
Configures offline commands. |
cli/cmd/install/uninstall.go |
Routes global uninstall through manifest. |
cli/cmd/install/uninstall_recorded.go |
Implements recorded uninstall flow. |
cli/cmd/install/uninstall_recorded_test.go |
Tests recorded uninstall behavior. |
cli/cmd/install/pin.go |
Implements pin and unpin. |
cli/cmd/install/pin_test.go |
Tests pin state changes. |
cli/cmd/install/output.go |
Adds table and JSON output helpers. |
cli/cmd/install/outdated.go |
Implements outdated reporting. |
cli/cmd/install/outdated_test.go |
Tests outdated output. |
cli/cmd/install/manifest.go |
Records context and adds manifest helpers. |
cli/cmd/install/list.go |
Implements installed-package listing. |
cli/cmd/install/list_test.go |
Tests list output. |
cli/cmd/install/install.go |
Registers commands and skips no-op prompts. |
cli/cmd/install/install_test.go |
Tests command registration. |
cli/cmd/install/context.go |
Resolves active context names. |
cli/cmd/install/batch.go |
Skips no-op batch confirmation. |
cli/cmd/install/agents.go |
Preserves the former agent listing. |
CHANGELOG.md |
Records features and breaking changes. |
.skill/SKILL.md |
Updates connectivity guidance. |
.skill/references/install.md |
Updates install workflows. |
Review details
Suppressed comments (1)
cli/cmd/install/outdated.go:253
- Raw string inequality does not mean the upstream is older. Equivalent versions such as installed
v1.0.0and upstream1.0.0compare equal inpkgupdate, but this renders the latter in parentheses as a downgrade. Use the semantic version comparison here and parenthesize only when the latest version is strictly lower.
if row.Status == pkgupdate.StatusUpToDate && row.Latest != row.Entry.Version {
return "(" + row.Latest + ")"
- Files reviewed: 37/37 changed files
- Comments generated: 9
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
af1b784 to
c175e18
Compare
fc883ad to
d35eecc
Compare
52a62c2 to
cd3cd1b
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Context provenance and edge-case status reporting can produce incorrect outdated results.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 21/21 changed files
- Comments generated: 4
- Review effort level: Balanced
0d2c634 to
b0cf409
Compare
The read side of package management, on top of the manifest refactor. `dirctl install list` lists installed packages from the manifest — name, agent, kind, scope, version, flags — with `-o json`. `install list <name>` mirrors `brew list <formula>`: that package's skill files and MCP server keys, which answers "what did this actually put on my machine?", otherwise unanswerable without reading agent config by hand. Both stat what they report, since the manifest is the only provenance an installed artifact has. `dirctl install agents` is the old `install list` under its new name, which is a breaking change: `list` now means what it means in every other package manager. `dirctl install outdated [name...]` compares each row against the Directory it was installed from. Only rows needing attention are listed, `--all` shows the rest. Two groups qualify: upgradable, and rows that could not be assessed at all — the second because a silently omitted `missing` or `not found` row reads as "fine". `--exit-code` gates CI, on the unfiltered set so `--all` cannot change it, and on upgradable alone since no upgrade fixes a missing row. Also `--pre` and `--include-pinned`. `cli/internal/pkgupdate` is the comparison engine and a pure unit: resolver, stat, the binary's own version, the active context and the row filters are all injected, so every status is reachable in a unit test with no server and no filesystem. It resolves once per distinct name, not once per row. A resolver failure is a per-row status, so one unreachable name cannot hide the verdict on the others. `builtin` rows compare against skill.RecordVersion() and issue no RPC. The manifest gains a `context` field, recorded on every install: a row pulled from a colleague's Directory says nothing about the one configured now, so it is `skipped` rather than compared. An empty name on either side means "no claim" and matches anything, so older rows and unnamed contexts stay checkable. `pin` and `unpin` hold a package at its installed version and release the hold, across every agent it is installed into. Install-time pinning alone cannot hold a version you already have. Signed-off-by: András Jáky <ajaky@cisco.com>
…tory Eight review findings from Copilot. **A no-op install skipped the manifest.** Both `runApplyCmd` and the piped path returned as soon as the plan moved nothing on disk, before recording. That defeats the release note telling users to re-install pre-v1.8 packages to get a row, and it kept `--pin` from taking hold without a version change. The plan is now recorded and only the prompt is skipped. **`--agents` could split a shared skill folder.** Claude Code and Claude Desktop share one folder, so removing it for one took the artifact from the other and stranded its row. The manifest can see every claimant, which the old record-derived path could not, so the selection is rejected with the agents that have to go together. **Named digests were not verified.** `uninstall name@digest-A` could remove a row recording digest-B. Every part the reference carries now has to agree. **A failed removal read as "already gone".** `!HasChanges` is also false for skipped and failed, so an unreadable config printed "already gone" and "Cleared 1 row" while the row survived and the error was never shown. It now needs every outcome to be `ActionUnchanged`. **`forgetRemoved` swallowed save failures.** Dropping the row is what uninstall was asked to do, so it uses `editManifest` and propagates. **The recorded context could name the wrong Directory.** `--server-addr` and `DIRECTORY_CLIENT_SERVER_ADDRESS` replace a context's endpoint while leaving its name in place, so an install against an overridden endpoint was recorded under a context it never used. `Entry.Context` becomes `Entry.Directory` and holds the effective server address, which also makes two contexts pointing at one Directory compare equal. **An unorderable version could hide.** Installed `main` with only prerelease upstreams returned `up to date` before the installed version was validated, so the row vanished from the default view. It is checked first. **Two reporting bugs.** `outdated` claimed "All packages are up to date" when the only rows were pinned ones holding a newer version back, and it dropped the `only prereleases published` hint because the row was assessed. Also `install list` documented its columns in the pre-rename order. Signed-off-by: András Jáky <ajaky@cisco.com>
233c547 to
de2615f
Compare
ramizpolic
left a comment
There was a problem hiding this comment.
LGTM, great work!
i would suggest to consider pulling these common methods into a dedicated package (at least the list locally installed operation). we could potentially leverage it inside the Runtime component
The read side of package management: see what is installed, and see what is stale.
Builds on #2134, now merged, which made the manifest the single source of truth.
install listandinstall agentsdirctl install listlists installed packages from the manifest — name, agent, kind, scope, version, flags — with-o json.dirctl install list <name>mirrorsbrew list <formula>: that package's skill files and MCP server keys. It answers "what did this actually put on my machine?", otherwise unanswerable without reading every agent config by hand.Both stat what they report, since the manifest is the only provenance an installed artifact has and a user can delete one behind our back. Anything a row names but disk does not have is marked
missing.dirctl install agentsis the oldinstall listunder its new name — detected agents and the files install would touch.install outdatedCompares each row against the Directory it was installed from.
Only rows needing attention are listed, with
--allfor the full table. Two groups qualify: upgradable, and rows that could not be assessed at all. The second is there because a silently omittedmissingornot foundrow reads as "fine".upgradablecontent changed)up to datepinnedmissingnot foundnon-semverskipped--exit-codegates CI. It is computed on the unfiltered set, so--allcannot change it, and onupgradablealone, since no upgrade would fix amissingrow. Also--preand--include-pinned.Its first four columns match
install list, so the two tables read as views of one manifest.install pin/unpinHold a package at its installed version, and release the hold, across every agent it is installed into.
They exist as verbs because install-time pinning alone cannot hold a version you already have: you upgraded, something broke, you want to sit still.
How the comparison works
cli/internal/pkgupdateis a pure unit. The resolver, the stat, the binary's own version, the active context and the row filters are all injected, so every status above is reachable in a unit test with no server and no filesystem.It resolves once per distinct name, not once per row, through
namingv1.Resolve(name, ""), which returns every{name, version, cid}without pulling record bodies. A package in three agents costs one call.A resolver failure is a per-row status, so one unreachable name cannot hide the verdict on the others.
builtinrows compare againstskill.RecordVersion()and issue no RPC.The manifest gains a
contextfield, recorded on every install. A row pulled from a colleague's Directory says nothing about the one configured now, so it isskippedrather than compared. An empty name on either side means "no claim" and matches anything, so older rows and unnamed contexts stay checkable.Everything here except
outdatedruns without a client.Breaking change
dirctl install listshipped in v1.6.2 showing detected agents. It now lists installed packages, and that view isdirctl install agents. Docs and skill references are updated here, and the release notes carry a BREAKING entry.Out of scope
install upgrade, manifest rows fordirctl init's built-in install, and bare-name resolution by highest version are #2030.Closes #2029.
Validation
cd cli && go test ./...andtask lint:go(pinned golangci-lint 2.13.2): pass, 0 issues../cmd/install/... ./internal/...also pass at-count=2, which catches the in-processoptssingleton trap.task test—server/store/ociintegration andcli/cmd/authat-count=2— reproduce on unmodifiedmain.pkgupdatestatus has a unit test with an injected resolver and stat, including the once-per-name call count, the built-in path issuing no RPC, and the lexical trap that ranks1.10.0below1.9.0.list,list <name>,-o json,agents,outdated --all,pin/unpinanduninstallall behave as described.