Skip to content

feat(extensions): add generic CLI commands - #888

Merged
benvinegar merged 7 commits into
mainfrom
feat/generic-extension-cli
Aug 29, 2026
Merged

benvinegar merged 7 commits into
mainfrom
feat/generic-extension-cli

Conversation

@benvinegar

Copy link
Copy Markdown
Member

Summary

  • publish Extension API v10 with generic top-level CLI command trees, raw argument ownership, cancellable streaming I/O, validated exits, and one-time built-in delegation
  • preserve built-in fast paths while resolving unknown commands through existing extension precedence, trust, staged-root, rollback, and bounded-retirement policies
  • ship dependency-free cli-tools and github-pr examples, including an installable hunk gh 123 workflow that fetches GitHub PR diffs directly and delegates them to hunk patch
  • allow an external initial patch to refresh through exact-file authority without granting access to its temporary directory or unrelated files

Why

Extensions can currently contribute review-time UI and VCS behavior, but the closed top-level CLI switch prevents them from owning preprocessing workflows. This adds one generic command seam rather than introducing service-specific core commands. The GitHub PR example exercises the complete lifecycle without requiring the gh CLI or a parallel review path.

Design notes

  • known built-ins and aliases remain unshadowable and keep extension-free startup paths
  • only unknown, valid lowercase-kebab command names trigger extension discovery
  • --no-extensions remains a hard stop; explicit paths, config/global providers, and trusted repo providers keep their existing precedence
  • handlers receive frozen raw tokens, cwd, AbortSignal, streaming stdin, and leased backpressure-aware writers
  • delegation is built-in-only and rejected after stdout output or any stdin read
  • delegated interactive startup reuses the loaded registry, so factories do not execute twice
  • bare help remains static; unloaded extension command listing is intentionally out of scope
  • the GitHub example supports github.com only, refuses redirects, bounds response size, sanitizes credential failures, and documents POSIX modes versus inherited Windows temporary-directory ACLs

Validation

  • bun run typecheck
  • bun run lint
  • bun run deps:check
  • bun run check:docs
  • bun run check:pack
  • focused parser, runtime, startup, reload-boundary, extension, GitHub example, and black-box CLI suites: 265 passing
  • bun run test:integration: 132 passing, 1 macOS-only skip
  • bun run test:tty-smoke: 9 passing
  • bun run build:bin
  • HUNK_TEST_EXECUTABLE=./dist/hunk bun test test/cli/compiled-headless-native-lib.test.ts: 7 passing, including installed-shape literal hunk gh --help
  • real tmux run of hunk gh 123 --repo modem-dev/hunk -- --pager: fetched PR Include untracked files in working-tree diff reviews #123, mounted the TUI, refreshed with r, quit with q, and left no temporary patch directory
  • two fresh-context subagent review passes followed by fix/re-review loops; final reviews reported no actionable findings

bun run test reaches 1,626 passing tests and 2 skips, but its wrapper exits nonzero on this machine because registerHostRuntimeModules > does not claim bare specifiers outside registered extension directories fails when the temporary outsider can resolve the repository's React. The focused test reproduces unchanged on origin/main at d96a860c.

Visual evidence

No renderer or interaction model changes. Delegated reviews enter the existing patch UI; the real tmux validation above exercised the terminal handoff and refresh path.

This PR description was generated by Pi using gpt-5.6-sol

@vercel

vercel Bot commented Aug 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hunk-web Ignored Ignored Preview Aug 29, 2026 9:36pm

Request Review

@greptile-apps

greptile-apps Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR publishes Extension API v10 with generic top-level CLI command trees, raw streaming I/O, cancellation, validated exits, and one-time delegation into built-in commands.

  • Adds parsing, registration, collision handling, execution leases, and extension-only startup discovery.
  • Preserves loaded extension registries across delegated interactive startup and expands cleanup for startup failures.
  • Adds exact-file reload authority for external patch files.
  • Includes dependency-free CLI and GitHub pull-request extension examples with comprehensive tests and documentation.

Confidence Score: 4/5

The PR appears safe to merge, with one non-blocking repository-convention issue in the GitHub example's environment access.

The extension command, delegation, registry-lifecycle, stream-ownership, and exact-file reload paths include coordinated validation and tests; the remaining accepted concern is direct process.env access in the new example.

Files Needing Attention: examples/extensions/github-pr/index.ts

Important Files Changed

Filename Overview
src/app/cli.ts Adds leading extension-bootstrap flag parsing and routes valid unknown top-level commands to extension discovery while preserving built-in parsing.
src/extensions/cliCommandRuntime.ts Implements frozen arguments, cancellable leased streams, result validation, and delegation ownership checks.
src/app/startup.ts Integrates extension command discovery and execution with built-in delegation and registry ownership transfer.
src/app/extensionCliBootstrap.ts Adds staged extension-only discovery with VCS-root refinement and provisional-load cleanup.
src/app/session/reloadBounds.ts Allows reload of only the exact initial external patch while continuing to reject unrelated files and repository-backed reloads.
examples/extensions/github-pr/index.ts Adds a bounded, redirect-refusing GitHub diff workflow and temporary-patch lifecycle, but directly accesses process.env contrary to repository guidance.
src/extension-api/types.ts Publishes the API-v10 CLI command metadata, context, writer, exit, and delegation contracts.
src/ui/runInteractiveApp.tsx Adds cleanup and extension retirement for renderer creation and mounting failures.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Raw argv] --> B{Built-in command?}
  B -- Yes --> C[Existing built-in planner]
  B -- No --> D{Valid extension command name?}
  D -- No --> E[Unknown-command error]
  D -- Yes --> F[Resolve config, trust, and extension registry]
  F --> G[Run command with leased stdin/stdout/stderr]
  G --> H{Handler result}
  H -- Exit --> I[Retire registry and return exit code]
  H -- Delegate --> J[Validate built-in-only argv]
  J --> K{Interactive review?}
  K -- No --> L[Retire registry and run headless plan]
  K -- Yes --> M[Reuse registry in AppBootstrap]
  M --> N[Interactive app owns retirement]
Loading
Prompt To Fix All With AI
### Issue 1
examples/extensions/github-pr/index.ts:404
**Direct environment access**

The new GitHub PR extension defaults both its fetch helper and runtime configuration to `process.env`, bypassing the repository's required validated environment-variable mechanism for `GH_TOKEN` and `GITHUB_TOKEN` and establishing an unsupported configuration-access pattern.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat(extensions): add generic CLI comman..." | Re-trigger Greptile

export async function fetchGitHubPullRequestDiff(
target: ResolvedGitHubPullRequest,
signal: AbortSignal,
env: NodeJS.ProcessEnv = process.env,

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.

P2 Direct environment access

The new GitHub PR extension defaults both its fetch helper and runtime configuration to process.env, bypassing the repository's required validated environment-variable mechanism for GH_TOKEN and GITHUB_TOKEN and establishing an unsupported configuration-access pattern.

Context Used: guidelines.mdc Cursor rule (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: examples/extensions/github-pr/index.ts
Line: 404

Comment:
**Direct environment access**

The new GitHub PR extension defaults both its fetch helper and runtime configuration to `process.env`, bypassing the repository's required validated environment-variable mechanism for `GH_TOKEN` and `GITHUB_TOKEN` and establishing an unsupported configuration-access pattern.

**Context Used:** guidelines.mdc Cursor rule ([source](https://github.com/modem-dev/modem/blob/main/.cursor/rules/guidelines.mdc))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@benvinegar benvinegar Aug 28, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks — I checked this against Hunk's actual extension contract and repository conventions. The cited rule belongs to modem-dev/modem, not this repository, and Hunk does not expose a validated environment/secrets API to extensions. This example follows Hunk's existing injectable ProcessEnv = process.env boundary, while its tests cover token precedence, malformed header values, and credential-safe errors. I'm keeping the implementation as-is rather than inventing a parallel extension configuration mechanism.

Responded by Pi using gpt-5.6-sol.

This comment was generated by Pi using gpt-5.6-sol

benvinegar and others added 7 commits August 29, 2026 17:35
`createLeasedWriter` returned a bare rejected promise when a write arrived
after settlement or carried an unsupported chunk type. The normal write path
already attaches a handler to its pending promise, but these two revocation
paths did not, so a handler that fired a write it never awaited — a
`setTimeout` progress line landing after the command returned is the usual
shape — produced an unhandled rejection. That crashed the process with a stack
trace pointing at Hunk internals and replaced the command's real exit status
with 1, defeating the point of validated exits.

Reject through a helper that marks the promise handled. Callers that await a
revoked write still see the rejection; callers that discard it no longer take
down the process.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hoisting `--extension` and `--no-extensions` ahead of the command token made
them parse successfully for every built-in, but only review commands and
extension CLI commands actually consume them. `hunk --extension ./x session
list` and `hunk --no-extensions markup guide` silently discarded the flag and
ran as if it were never passed. Before the hoist these spellings failed loudly
with `Unknown command: --extension`, so the seam turned a hard error into a
silent no-op.

Reject them the way a misplaced `--fast` is already rejected, naming the
command that cannot use the flag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`registerCliCommand` required `summary` and accepted `usage`, then stored both
where nothing ever read them: bare `hunk --help` stays static by design, and
`hunk ext list` reports installed extensions rather than the commands they
register. A required field with no consumer is a shape we would inherit into
API v11 without ever having seen it rendered.

Give both fields the discovery surface they were written for. When an unknown
top-level token reaches extension resolution and no extension claims it, the
registry has already been loaded, so the failure can name what the loaded
extensions do offer instead of only repeating the bad token. Both strings come
from extension code, so `describeExtensionCliCommands` sanitizes them and
collapses each to a single line; otherwise a summary carrying newlines and
escape sequences could forge host output around a legitimate error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ctx.cwd` is the spawned process's own working directory, which macOS reports
through the `/var` -> `/private/var` symlink that `tmpdir()` hands back
unresolved. The assertion compared it against the unresolved `mkdtempSync`
path, so the test failed on macOS while passing wherever `tmpdir()` is already
canonical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@benvinegar
benvinegar force-pushed the feat/generic-extension-cli branch from 189f399 to ed1b060 Compare August 29, 2026 21:36
@benvinegar
benvinegar merged commit 79fd010 into main Aug 29, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant