Skip to content

refactor(cli/dir): install by pipe, uninstall from the manifest, and project-scope tracking - #2134

Merged
akijakya merged 3 commits into
mainfrom
refactor/dirctl-install
Sep 15, 2026
Merged

akijakya merged 3 commits into
mainfrom
refactor/dirctl-install

Conversation

@akijakya

@akijakya akijakya commented Sep 10, 2026 •

Copy link
Copy Markdown
Member

Makes the install manifest the single source of truth for what is installed, and stops dirctl install carrying a second copy of dirctl search's filter surface.

Groundwork for #2029, which stacks on this and adds the commands that read the manifest.

Batch install becomes a pipe

--name, --module, --skill, --domain, --locator, --author, --version and --limit come off install and uninstall. Filtering is search's job.

dirctl search --module integration/mcp -o raw | dirctl install --agents all --yes

One reference per line. Blanks and # comments ignored, quotes stripped so -o jsonl works, plain names accepted. At most 1000 per run. A reference that will not resolve is reported and skipped rather than aborting the run.

A piped run cannot prompt, since stdin is the list, so it needs --yes or --dry-run. Without either it errors naming the flag, instead of reading a CID as the answer.

Only the highest version per name is installed, with no flag to override. The skill slug and the MCP key both derive from the record name, so two versions resolve to the same folder and the same config key — only one can be live.

uninstall takes one reference. Removing several is a loop over the manifest, which is what knows what you have.

Uninstall reads the manifest

It re-pulled the record to derive what to remove. That needed a reachable Directory to delete something local, and it computed a plan for every detected agent, so agents that never had the package appeared as unchanged lines naming their config paths.

It now makes no Directory call at either scope, and touches only the agents whose rows name the package — including one no longer detected here, since the files it was given are still on disk.

A :version narrows to rows at that version, a bare CID matches the exact record installed, and a row whose artifacts are already gone is cleared so a package cannot get stuck. A reference with no row is reported as not installed, naming the scope searched and saying where the package is when it exists at another one.

With batch gone, uninstall needs no client and joins the root.go skip list.

Project installs are recorded

scope was the literal project, which could never hold more than one repository: the key is (name, agent, scope), so the same package installed at project scope in two repositories collided and the second overwrote the first.

It is now global or the repository's absolute path, and every install is recorded. A row's paths resolve against the repository it names rather than the current directory, so a project install can be listed and removed from anywhere.

This drops the plan of a committed ./.dirctl/installed.json. That file would carry one developer's absolute home paths, useless to a teammate. The manifest is a local record of what happened; a file pinning what a team should have is declarative, and a separate thing. #2027 is updated to match.

dirctl install prune

Drops rows whose recorded artifacts are all gone, with --dry-run. Rows go stale when a skill folder is deleted by hand or a repository is moved.

It touches nothing on disk, and drops a row only when every artifact it names is gone — a package that lost one of two is still installed.

Plans name only what changes

An unchanged line puts an agent's config path next to the record's name, which reads as a claim that the package is installed there. Those lines go. Skips and failures stay, the tally still counts everything, and a no-op plan no longer asks for confirmation.

Worth a reviewer's eye: a shared skill location reports ActionUnchanged for the agent that shares it — Claude Code and Claude Desktop, Zed and Codex CLI — so those agents drop out of the lines too, even though the artifact serves them. Their manifest rows are still written.

Also

-o raw now prints one value per line for list results, instead of Go's [a b] debug form that nothing can split. The search docs already advertised it as the pipeable format. Affects search, delete, and routing list.

Breaking changes

Four, all in the release notes.

Change Migration
Batch install/uninstall by search filters removed dirctl search -o raw | dirctl install; uninstall takes one reference
--all-versions removed The highest version was always the only one that survived
uninstall no longer pulls the record Packages installed by v1.7.0 or earlier have no row; re-install to record one
scope is global or a repository path Replaces the project literal

Closes #2133.

Validation

  • cd cli && go test ./... and task lint:go (pinned golangci-lint 2.13.2): pass, 0 issues. ./cmd/install/... ./internal/... also pass at -count=2, which catches the in-process opts singleton trap.
  • Two failures elsewhere in task test — server/store/oci integration and cli/cmd/auth at -count=2 — reproduce on unmodified main.
  • Manual, against a local daemon with two versions of one record pushed: search -o raw | install --yes installs the highest once and records one row; a piped run without --yes errors naming the flag; -o jsonl pipes just as well.
  • Manual: the same package in two repositories plus globally gives three rows that do not collide, all correctly stat'd from a third directory. uninstall --project in one repository removes only its row with the daemon stopped. Moving the other repository away leaves a row that prune drops.

@akijakya
akijakya requested a review from a team as a code owner September 10, 2026 16:16
@akijakya akijakya added kind/refactor Categorizes issue or PR as related to cleaning up code, process, or technical debt. area/cli area/cli/dir labels Sep 10, 2026
@github-actions github-actions Bot added the size/XL Denotes a PR that changes 2000+ lines label Sep 10, 2026
@codecov

codecov Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Manifest pruning, reference matching, no-op handling, and piped pinning contain unresolved correctness issues.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Refactors CLI package management around piped installs, manifest-driven uninstalls, and repository-specific project tracking.

Changes:

  • Accepts piped install references and removes duplicated search filters.
  • Uses the local manifest for uninstall and adds stale-row pruning.
  • Improves raw output and suppresses unchanged plan lines.
File summaries
File Description
docs/content/dir/dir-cli-reference.md Documents revised package workflows.
cli/presenter/output.go Prints raw lists line-by-line.
cli/presenter/output_test.go Tests raw list formatting.
cli/internal/pkgstate/pkgstate.go Adds repository-path scopes.
cli/internal/pkgstate/pkgstate_test.go Tests scope key uniqueness.
cli/internal/agentinstall/remove.go Removes manifest-recorded artifacts.
cli/internal/agentinstall/remove_test.go Tests recorded removal.
cli/internal/agentinstall/installed.go Inspects recorded artifact presence.
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 filtered summaries.
cli/internal/agentcfg/selection.go Adds agent lookup by ID.
cli/internal/agentcfg/result.go Adds change detection helpers.
cli/internal/agentcfg/plan.go Filters unchanged plan entries.
cli/internal/agentcfg/plan_test.go Tests plan filtering.
cli/cmd/root.go Skips clients for local-state commands.
cli/cmd/install/uninstall.go Routes uninstall through the 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/scope.go Maps repository paths to scopes.
cli/cmd/install/prune.go Adds manifest pruning.
cli/cmd/install/prune_test.go Tests pruning behavior.
cli/cmd/install/piped.go Parses piped references.
cli/cmd/install/piped_test.go Tests piped input handling.
cli/cmd/install/output.go Adds tabular output helpers.
cli/cmd/install/options.go Removes batch-search options.
cli/cmd/install/multi.go Implements multi-record piped installs.
cli/cmd/install/multi_test.go Updates multi-install tests.
cli/cmd/install/manifest.go Records all scopes and supports edits.
cli/cmd/install/manifest_test.go Tests project manifest records.
cli/cmd/install/install.go Adds pipe and prune command routing.
cli/cmd/install/install_test.go Tests revised command surface.
cli/cmd/install/helpers_test.go Adds shared manifest test helpers.
cli/cmd/install/flags.go Removes filters and retains version selection.
CHANGELOG.md Records additions and breaking changes.
.skill/SKILL.md Updates client requirements.
.skill/references/install.md Updates package-management guidance.
Review details

Suppressed comments (1)

cli/cmd/install/multi.go:128

  • Resolving each piped reference to a record here discards whether the user explicitly supplied :version. Later applyTargets sets pinned only from opts.pin, so printf 'pkg:v1.0.0\n' | dirctl install --yes is not pinned, unlike the same explicit reference passed positionally. Preserve pin intent through target selection and record it for the selected record.
  • Files reviewed: 37/37 changed files
  • Comments generated: 8
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cli/cmd/install/prune.go
Comment thread cli/cmd/install/uninstall_recorded.go Outdated
Comment thread cli/cmd/install/uninstall_recorded.go Outdated
Comment thread cli/internal/agentcfg/result.go
Comment thread cli/cmd/install/prune.go
Comment thread cli/cmd/install/uninstall.go Outdated
Comment thread cli/cmd/install/uninstall.go Outdated
Comment thread docs/content/dir/dir-cli-reference.md Outdated
@akijakya
akijakya force-pushed the refactor/dirctl-install branch from fc883ad to d35eecc Compare September 11, 2026 14:32
@akijakya

Copy link
Copy Markdown
Member Author

All eight applied in 0d2c634, and seven of them turned out to be one bug wearing different hats: a lookup that could not be made was being read as a negative answer.

Present conflated absent with unreadable (prune.go:59). A permission error, a broken mount, an unparseable agent config, and an unknown agent all became "gone", and prune deletes the only provenance a package has. Installed gains an Unchecked state, agentcfg.MCPEntryChecked reports whether the lookup completed, and Present is now the negation of confirmed gone. Doubt keeps the row.

Same bug in the recorded uninstall (uninstall_recorded.go:98). !HasChanges is also false for skipped and failed, so an unreadable config printed "already gone" and "Cleared 1 row" while forgetRemoved quietly kept the row and the failure was never shown. It now needs every outcome to be ActionUnchanged.

Digest on a named reference was ignored (uninstall_recorded.go:191). uninstall name@digest-A could remove a row recording digest-B. Every part the reference carries now has to agree.

runApplyCmd still gated on len(plan) == 0 (result.go:49). Correct catch — an all-unchanged plan is not an empty one, so a single install still asked to confirm a no-op. It uses HasChanges like the piped path.

The remaining three were wording that had drifted from behaviour: both uninstall short helps said "detected agents", prune's empty case claimed every artifact was on disk when Present only promises one survivor, and the docs offered dirctl install list as a source of pipeable names, which it does not produce until #2029.

New tests cover each: an unreadable config keeps its row through both prune and uninstall, a mismatched digest matches nothing, and install list <name> now prints (could not check) rather than (missing) for an artifact it could not look at.

@akijakya
akijakya enabled auto-merge (squash) September 15, 2026 08:20
Makes the install manifest the single source of truth for what is installed,
and stops `dirctl install` carrying a second copy of `dirctl search`'s filter
surface. Four breaking changes, all in the release notes.

**Batch by search filters is gone.** `--name`, `--module`, `--skill`,
`--domain`, `--locator`, `--author`, `--version` and `--limit` come off
install and uninstall. Filtering is search's job, duplicating it was the only
reason `uninstall` ever needed a Directory, and the filters answered the
wrong question there anyway: batch uninstall searched for records matching
`--author X` and removed what those records *currently* derive, not what was
installed. `dirctl search -o raw | dirctl install` replaces the install half.
References are read one per line, blanks and `#` comments ignored, quotes
stripped so `-o jsonl` works too. `--all-versions` survives the cull, since a
pipe carries every matching version and installing all of them into one agent
means repeated writes to the same slug. A piped run cannot prompt — stdin is
the list — so it errors without `--yes` rather than reading a CID as the
answer. `uninstall` takes one reference.

**`-o raw` prints one value per line** for list results. It was emitting Go's
`[a b]` debug form, which nothing can split, while the search docs already
advertised it as the pipeable format.

**Uninstall reads the manifest.** No Directory call at either scope, so it
works with the server down or after the record has been deleted upstream, and
only the agents whose rows name the package appear in the plan — including
one no longer detected here, since the files it was given are still on disk.
A `:version` narrows to rows at that version, a bare CID matches the exact
record installed, and a row whose artifacts are already gone is cleared so a
package cannot get stuck. A reference with no row is reported as not
installed, naming the scope searched and saying where the package is when it
exists at another one. With batch gone, uninstall joins the client-setup skip.

**Project installs are recorded, keyed by repository path.** `scope` was the
literal `project`, which could never hold more than one repository: the key
is (name, agent, scope), so the same package installed at project scope in
two repositories collided and the second overwrote the first. It is now
`global` or the repository's absolute path, and a row's paths resolve against
the repository it names, so a project install can be listed and removed from
anywhere. This drops the plan of a committed `./.dirctl/installed.json`: that
file would carry one developer's absolute home paths, useless to a teammate.

**`dirctl install prune`** drops rows whose artifacts are all gone, with
`--dry-run`. Rows go stale when a skill folder is deleted by hand or a
repository is moved. It touches nothing on disk.

**Plans and summaries name only what changes.** An `unchanged` line puts an
agent's config path next to the record's name, which reads as a claim that
the package is installed there. Skips and failures stay, the tally still
counts everything, and a no-op plan no longer asks for confirmation.

Signed-off-by: András Jáky <ajaky@cisco.com>
It claimed to install every piped version of a name. It could not: the skill
slug and the MCP server key both derive from the record name alone, so two
versions resolve to the same folder and the same config key. Piping 1.9.0 and
1.10.0 with the flag set reported "1 added, 1 updated", left one folder
holding 1.10.0, and wrote one manifest row — the key is (name, agent, scope),
so the second install overwrote the first there too.

Only one version of a package can be live and reachable by an agent, so
install picks the highest rather than offering a flag to pretend otherwise.
The dedup that was behind the flag stays, unconditionally.

Signed-off-by: András Jáky <ajaky@cisco.com>
Eight review findings, seven of them one theme: a lookup that could not be
made was being read as a negative answer.

`Present` turned a permission error, a broken mount, an unparseable agent
config, and an unknown agent all into "gone". `prune` deletes the only
provenance a package has, so it was one malformed `.claude.json` away from
dropping rows for artifacts that were still on disk. Inspection now carries
an `Unchecked` state, `agentcfg.MCPEntryChecked` reports whether the lookup
completed, and `Present` is the negation of "confirmed gone" — doubt keeps
the row.

The recorded uninstall had the same bug in a different shape. It took the
"nothing to remove, clear the rows" path on `!HasChanges`, which is also
false for skipped and failed outcomes, so an unreadable config printed
"already gone" and "Cleared 1 row" while `forgetRemoved` quietly kept the
row and the failure was never shown. It now needs every outcome to be
ActionUnchanged.

`rowMatches` ignored the digest on a named reference, so `uninstall
name@digest-A` could remove a row recording digest-B. Every part the
reference carries now has to agree.

`runApplyCmd` still gated on `len(plan) == 0`, so a single install whose
artifacts were all already correct asked the user to confirm a no-op,
contrary to the documented behaviour. It uses `HasChanges` like the piped
path.

Also: both `uninstall` help strings said "detected agents", the opposite of
what manifest-driven removal does; prune's empty case claimed every artifact
was on disk when `Present` only promises one survivor; and the docs offered
`dirctl install list` as a source of pipeable names, which it does not
produce until #2029.

Signed-off-by: András Jáky <ajaky@cisco.com>
@akijakya
akijakya force-pushed the refactor/dirctl-install branch from 0d2c634 to b0cf409 Compare September 15, 2026 08:21
@akijakya
akijakya merged commit ee6ef02 into main Sep 15, 2026
11 checks passed
@akijakya
akijakya deleted the refactor/dirctl-install branch September 15, 2026 08:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/cli/dir area/cli kind/refactor Categorizes issue or PR as related to cleaning up code, process, or technical debt. size/XL Denotes a PR that changes 2000+ lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(cli): install by pipe, uninstall from the manifest, and project-scope tracking

3 participants