Skip to content

Install Graph on demand with visible plugin installation progress - #2324

Merged
Soph merged 9 commits into
mainfrom
feat/graph-plugin-install-on-demand
Sep 11, 2026
Merged

Soph merged 9 commits into
mainfrom
feat/graph-plugin-install-on-demand

Conversation

@ashtom

@ashtom ashtom commented Sep 8, 2026 •

Copy link
Copy Markdown
Member

https://entire.io/gh/entireio/cli/trails/1267

Summary

Install the Graph external command on demand when a user runs entire graph and entire-graph is missing.

  • Resolve graph through the configured plugin index and show the repository URL in a Yes-default confirmation.
  • Bind the accepted repository entry to the install, then execute the managed plugin with the original arguments unchanged.
  • Keep non-interactive runs safe by printing an explicit entire plugin install graph hint instead of prompting.
  • Run an existing managed Graph plugin directly when it is temporarily unreachable through PATH; diagnose dangling links or directory entries with an actionable --force reinstall command.

Install experience

Remote plugin installs now report index lookup, release discovery, metadata fetch, download and verification, placement, and dependency-planning stages on stderr. Styled terminals get a spinner; accessibility mode and non-terminal writers get plain status lines. Stdout remains available for plugin output and JSON pipelines.

Confirmations read from the controlling terminal rather than stdin, preserving piped plugin input. If stderr is redirected, the prompt is rendered on the terminal where the answer is read. Cancellation is context-aware, and accessible-mode EOF fails closed.

The post-install handoff announces only Running entire-graph; user arguments are never echoed to stderr.

Reliability

  • Preserve ordinary plugin exit codes.
  • Re-raise signals delivered only to the plugin, including SIGPIPE and targeted termination.
  • When Entire itself receives a signal, preserve that initiating signal rather than the SIGINT used internally to cancel the child.
  • Update Ultraviolet for the inline-renderer cursor fix so completed confirmation prompts are erased cleanly.

Validation

  • go test ./cmd/entire/cli ./cmd/entire/cli/uiform ./cmd/entire
  • Signal-focused integration tests, including parent SIGTERM precedence
  • Windows cross-compilation of the affected packages
  • PTY coverage for prompt rendering, cleanup, redirected stdin, cancellation, and signal handling

Show installation progress and announce the forwarded command. Update the terminal renderer so completed confirmation prompts are cleared, with regression coverage for terminal cleanup and plugin install/dispatch behavior.

Entire-Checkpoint: 01M211S73J1V1YPNB123826DE8
Copilot AI lite review requested due to automatic review settings September 8, 2026 17:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new stderr “Running plugin with command …” line prints user-supplied arguments and can expose secrets and add noisy output.

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

Pull request overview

This PR enhances the CLI’s external-command dispatch to support on-demand installation of the entire-graph plugin when users run entire graph ... and the plugin is not present, while also adding visible install progress reporting (on stderr) throughout the managed plugin install flow.

Changes:

  • Special-cases missing entire-graph during plugin resolution to offer an interactive install (or a non-interactive install hint) and then runs the plugin with the original args.
  • Adds context-driven, stderr-only install progress stages (spinner on styled terminals; plain lines for non-TTY/accessibility).
  • Adds documentation and test coverage for the on-demand install path and progress output, plus bumps ultraviolet.
File summaries
File Description
go.mod Bumps github.com/charmbracelet/ultraviolet indirect dependency.
go.sum Updates checksums for the ultraviolet bump.
docs/architecture/external-commands.md Documents the new “missing graph offers installation” rule and stderr progress behavior.
cmd/entire/cli/plugin.go Enables the on-demand install flow when graph is missing and dispatches after install.
cmd/entire/cli/plugin_progress.go Introduces context-carried progress writer + startPluginStep helper.
cmd/entire/cli/plugin_progress_test.go Verifies immediate plain progress output and that stages land on stderr while stdout stays clean.
cmd/entire/cli/plugin_on_demand.go Implements the interactive install prompt + managed-install handoff for missing plugins.
cmd/entire/cli/plugin_on_demand_test.go Tests non-interactive hinting, prompt behavior, install/run sequencing, and arg forwarding.
cmd/entire/cli/plugin_install_remote.go Adds progress stages around tag lookup, metadata fetch, and install step.
cmd/entire/cli/plugin_group.go Wires progress reporting into remote installs and dependency planning.
cmd/entire/cli/plugin_fetch.go Adds progress stages for locating assets, downloading, and checksum verification.
cmd/entire/cli/uiform/prompt_terminal_test.go Ensures terminal prompt rendering clears answered confirmations (PTY-based test).
Review details
  • Files reviewed: 11/12 changed files
  • Comments generated: 1
  • Review effort level: Lite

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

Comment thread cmd/entire/cli/plugin.go Outdated

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 5c185dd. Configure here.

Comment thread cmd/entire/cli/plugin.go Outdated
ashtom and others added 7 commits September 9, 2026 14:12
Entire-Checkpoint: 01M231BF0DXEPSR8G9AWF7NCMC
Entire-Checkpoint: 01M23DNRDKQ7Z7SYTXZ4PBT5SB
Two defects, each with a regression test verified red without its fix.

An `entire-graph` already in the managed directory but unreachable through
PATH — a local-dev symlink whose target moved, or a managed bin dir that
could not be prepended at startup — dead-ended: the user was prompted,
accepted, and after three network round-trips got "already installed; use
--force to replace", because the on-demand path passes no --force. The
managed entry is now found before the prompt and executed, which is what
installMissingPlugin's own return contract already promised.

A confirmation rendered to the supplied writer even when that writer was not
a terminal. `entire graph 2>log` therefore put zero bytes on the terminal
while holding it in raw mode waiting for a keypress, and with Yes as the
default an Enter authorized a download-and-exec nobody was shown. The opener
now keeps the TTY's output handle instead of closing it, and renders there
when the caller's writer is not itself a terminal.

Also narrows the plugin signal exit. It fired whenever any signal had reached
this process, which discarded the plugin's own exit code — Ctrl-C reaches the
whole foreground process group, so a TUI plugin that handles it and exits 0
was reported as killed. It is now gated on the plugin's outcome via
ExitPluginSignalled, which incidentally fixes os.Exit(-1) truncating to 255
for a signalled child.

Smaller: the announcement names the command rather than only its arguments
(a bare `entire graph` printed "Running plugin with command:" and stopped);
`plugin upgrade` opts into the install progress its own startPluginStep calls
were emitting nowhere; Browse cancellation follows its prompt to stderr; the
graph name gets a constant; the progress docs move out of the managed-install
-directory section; and the prompt-clearing assertion stops pinning the row
count of a huh form.

TestMaybeRunPlugin_MissingGraphNonInteractive gains plugin-dir isolation:
consulting the managed directory before the prompt made it read the
developer's real plugins, and pluginParentDir is not covered by the testdirs
fallback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M25T41HTVXGY36EF3QXAF9YP
…uments

Two review findings on the on-demand install path.

FindInstalledPlugin reports what Lstat finds, so a local-dev symlink whose
target moved is listed like any other install. Short-circuiting to it
exec'd the missing target and failed with a fork/exec ENOENT naming a path
the user never chose — and the comment and docs claimed such an entry was
executed, which was true only when the target still existed. The entry is
now checked with os.Stat first; a missing target or a directory in its place
is reported with the entry's path and an 'entire plugin install <name>
--force' remedy. Reinstalling automatically is deliberately not done:
replacing a developer's symlink with a released binary is their call.

The remedy is offered only for the two conditions identified positively. A
stat failure that is not ENOENT gets the error and no advice — reinstalling
into the managed directory does not fix a permission failure on it, and a
remedy hung off an unmatched error is how advice comes to name the wrong
cause, as writeEntireDirRemedy documents for `.entire`.

The post-install announcement echoed every argument verbatim to stderr. They
are the user's own command line, already on screen, so they add nothing —
and they carry whatever they hold into stderr and anything capturing it. The
regression test drives a token, an ESC sequence and an embedded newline
through it; before the fix stderr read:

    Running entire graph --token s3cr3t \x1b[1A\x1b[2Kforged line
    break

The announcement is now the binary's name alone: "Running entire-graph".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M25WMATX7WCBP326ZXSRZGSY
A signalled child reports ExitPluginSignalled either way, so the previous
commit re-raised only when this process had also received a signal and exited
1 otherwise — losing the signal exactly when the plugin was the only one to
get it. Ctrl-C reaches the whole foreground process group, but `kill -TERM`
aimed at the plugin does not, and neither does a SIGPIPE from `entire graph |
head -1`. Both are outcomes kubectl-style dispatch has to pass through.

runPlugin now reads the signal off the child's wait status and MaybeRunPlugin
returns it, so main.go re-raises the child's own signal, preferring it over
one we received. `kill -TERM` inside a plugin, measured end to end:

    PR head          exit=255   (os.Exit(-1), truncated)
    previous commit  exit=1     (signal lost)
    now              exit=143   and bash reports "Terminated: 15"

pluginTerminatingSignal is a build-tagged pair rather than a runtime.GOOS
branch: syscall.WaitStatus on Windows is a bare struct with an ExitCode field
and no Signaled/Signal methods, so a GOOS branch would not compile. Windows
reports a killed child as an ordinary exit code, so it never reaches this path
at all; the remaining "-1 with no signal on either side" case exits 1 rather
than inventing a signal to raise.

MaybeRunPlugin's third result is what unparam flagged as unused, correctly:
nothing in the package checked that the signal survived the trip out of the
dispatcher, which is the half main.go depends on. Both levels are covered now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M25YSY75V465CHD9DXK30WG5
The prompt is the only human checkpoint before a remote binary is downloaded
and executed — an index-listed install never prompts inside runRemoteInstall,
because the catalog is the trust decision — and it named neither its catalog
nor its repository while defaulting to Yes. confirmInstallOrCancel names
redactURL(repoURL) for an unlisted repo and defaults to No, so the one
unprompted-command-to-exec path described itself least. It now resolves the
index entry first and asks:

    Install the entire-graph plugin from https://github.com/entireio/entire-graph?

Resolving first also removes a prompt-then-fail: a name the index does not
carry cannot be installed at all, so it is now reported instead of offered,
the same shape as the already-installed dead end. Not an extra round-trip —
SyncPluginIndex touches a freshness marker and runRemoteInstall's own call
moments later reads the clone without fetching (pluginIndexTTL) — and the
progress step stops before the prompt.

The docs claimed "EOF declines" for both prompt modes. Measured, with an
input that EOFs immediately:

    accessible        returns at once, ok=false err=nil        (declines)
    default (tea)     blocks to the deadline, "cancelled"      (never answers)

Both fail closed, so this was doc accuracy rather than a hole, but only one
of them declines. The sentence now says which does what and why.

The on-demand tests gain a local file:// index, since the prompt text now
depends on a resolved entry: without it they would consult the real published
catalog over the network.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M25ZHG577Q1PD3S30XEV1P5Q
… repo

Two review findings, both on commits earlier in this branch.

The signal precedence was backwards. runPlugin's cmd.Cancel sends the child
SIGINT whatever Entire was sent, so when Entire is signalled the child's
signal is Entire's own signal laundered — and laundered lossily. Preferring
the child's therefore reported the wrong thing for exactly the case
dieFromSignal's doc comment calls out: "a SIGTERM (from a supervisor /
container stop) must exit 143, not masquerade as a SIGINT 130". Measured with
a plugin that ignores SIGINT and so outlives WaitDelay:

    before   supervisor SIGTERM -> parent dies of SIGKILL (137)
    after    supervisor SIGTERM -> parent dies of SIGTERM (143)

The child's signal is now used only when Entire received none, which is
precisely when nothing laundered it: `kill -TERM` at the plugin still exits
143, SIGPIPE from a closed pipe still exits 141.

Naming the repository in the prompt introduced a TOCTOU on the trust
decision it exists to make. runRemoteInstall re-resolved the name through a
second SyncPluginIndex, and the two reads can disagree: a refresh that fails
leaves the freshness marker untouched, so the next call retries the fetch and
may succeed with different content, and a concurrent `plugin index update
--force` rewrites the clone under the lock either way. The prompt could name
repository A while repository B was downloaded and executed. installSource
gains a Resolved entry that travels with the request, so nothing is
re-resolved after the user has agreed. An idxErr stops being fatal on that
path, since the caller has already done the lookup and the index is needed
afterwards only for dependency planning, which already degrades to a warning.

TestExternalCommand_ParentsSignalOutranksTheChilds covers the precedence
against the real binary — main.go had no automated coverage for this at all —
and fails with "parent died of killed, want SIGTERM" against the old order.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M261WZ3GJH3BBPSSY3TJ0TNX
@Soph
Soph marked this pull request as ready for review September 10, 2026 17:11
@Soph
Soph requested a review from a team as a code owner September 10, 2026 17:11
gtrrz-victor
gtrrz-victor previously approved these changes Sep 11, 2026
Entire-Checkpoint: 01M27VYXMNDNPV2B2JGFW182H2
@Soph
Soph merged commit 2a1656e into main Sep 11, 2026
24 of 25 checks passed
@Soph
Soph deleted the feat/graph-plugin-install-on-demand branch September 11, 2026 09:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants