Skip to content

feat: structured RPC for hook scripts (#47, phase 2)#54

Merged
Cannon07 merged 1 commit into
mainfrom
feat/structured-rpc
May 16, 2026
Merged

feat: structured RPC for hook scripts (#47, phase 2)#54
Cannon07 merged 1 commit into
mainfrom
feat/structured-rpc

Conversation

@Cannon07
Copy link
Copy Markdown
Owner

Phase 2 of #47 — replaces the string-interpolated "build Lua source in bash" pattern with structured JSON args dispatched through a single Lua entry point.

What changes

Before: Bash scripts built Lua source code as strings (\"require('...').show_diff('\$ESC_PATH', ...)\") and sent them via nvim --remote-expr. An escape_lua sed helper escaped every interpolated arg to avoid syntax errors when user data contained apostrophes, backslashes, or newlines. Easy to forget; one mistake = silent hook failure or quote breakout.

After: Bash builds a JSON args array with jq (which handles escaping correctly), writes it to a temp file, and calls nvim_call MOD FN JSON_ARGS. Inside nvim, code-preview.rpc.dispatch decodes the JSON and calls require(MOD)[FN](unpack(args)) with real Lua values. No user data ever enters a Lua source string — only the module name, function name, and temp-file path (all our own values) get interpolated.

Files

New

  • `bin/nvim-call.sh` — `nvim_call(mod, fn, json_args)`. Returns 0/1/2 (ok / no socket / dispatch failed).
  • `lua/code-preview/rpc.lua` — `dispatch(mod, fn, args_file)`. Errors wrap in pcall and route through `log.error` (vim.notify) so silent dispatch failures stay visible to the developer.

Removed

  • `bin/nvim-send.sh` — `escape_lua` and the unused `nvim_send` function.

Migrated (13 callsites)

  • `bin/core-pre-tool.sh` — `changes.set`, `neo_tree.refresh/reveal`, `hook_context`, `diff.show_diff` (Edit/Write/MultiEdit + ApplyPatch paths).
  • `bin/core-post-tool.sh` — `diff.close_for_file`, `changes.clear_by_statuses`.
  • `backends/{copilot,codex}/code-{preview,close}-diff.sh` — swapped six near-identical inline `vim.json.encode({debug=...})` queries for one `code-preview.log.state()` RPC.

Supporting Lua helpers

  • `log.state()` — bundled `{debug, log_file, servername, cwd}` for the dedup above.
  • `neo_tree.refresh_deferred(ms)` / `reveal_deferred(path, ms)` — so callers don't build `vim.defer_fn(function() ... end, ...)` as a string.
  • `health.lua` — swapped `nvim-send.sh` for `nvim-call.sh`; added a load check for `code-preview.rpc`.

Pre-existing bug fixes uncovered during testing

Both are exactly the category of bash-quoting brittleness this migration was designed to retire — found them because Test 3 (filenames with apostrophes) finally exercised the unhappy paths:

  1. rm path doubling. `detect_rm_paths` didn't strip surrounding quotes, so `rm "/abs/path"` failed the `[[ $p != /* ]]` test and got `$CWD/` prepended. Now strips outer ` ' ` / ` " ` and handles `~/` via a quoted case pattern.
  2. xargs silently zeroing path lists. `RM_PATHS="$(echo "$RM_PATHS" | xargs)"` does shell-style quote processing — a single apostrophe in a filename (`it's-mine.txt`) made xargs see an unbalanced quote and return empty. Replaced with parameter-expansion trim that doesn't reinterpret the input.

Tested manually

  • Edit / Write / MultiEdit / Create / Delete across Claude Code, Codex, OpenCode, Copilot CLI.
  • Filenames with apostrophes (the case that surfaced both bug fixes above).
  • `:checkhealth code-preview` passes clean, including the new dispatcher check.

Independent review

Asked a separate Claude Code session to audit the diff. Critical findings (silent dispatch failures, missing JSON-array validation, missing health check, undocumented field-shape contract) are all addressed in this PR. Reviewer's lower-priority findings (cosmetic / pre-existing / out-of-scope) deferred to follow-ups.

Out of scope

  • Phase 3 (port `core-pre-tool.sh` → Lua, removes `nvim-call.sh` along the way).
  • Phase 4 (same for `core-post-tool.sh`, removes `nvim-socket.sh`).

Test plan

  • Edit/Write/MultiEdit across all four backends
  • Bash-tool rm detection
  • Filenames with apostrophes
  • ApplyPatch (Codex)
  • Accept/reject closes the diff
  • :checkhealth code-preview

🤖 Generated with Claude Code

Replaces the string-interpolated "build Lua source in bash" pattern with
JSON args dispatched through a single Lua entry point. User-controlled
data (file paths, file contents) flows as JSON through a temp file and is
decoded by vim.json.decode inside nvim — no user data ever enters a Lua
source string, so the entire escape_lua quoting layer is gone.

New:
- bin/nvim-call.sh — nvim_call(mod, fn, json_args) helper. Only mod/fn
  and the temp-file path (all our own values) get interpolated into Lua
  source. Returns rc=0/1/2 (ok / no socket / dispatch failed).
- lua/code-preview/rpc.lua — dispatch(mod, fn, args_file) reads JSON,
  validates it's an array, calls require(mod)[fn](unpack(args)). Errors
  are surfaced via log.error (vim.notify) so silent dispatch failures
  are visible to the developer.

Removed:
- bin/nvim-send.sh — escape_lua and the dead nvim_send function.

Migrated all 13 callsites:
- core-pre-tool.sh: changes.set, neo_tree.refresh/reveal, hook_context,
  diff.show_diff (Edit/Write/MultiEdit + ApplyPatch paths).
- core-post-tool.sh: diff.close_for_file, changes.clear_by_statuses.
- backends/{copilot,codex}/code-{preview,close}-diff.sh: swapped the
  six inline `vim.json.encode({debug=...})` queries for a single
  code-preview.log.state() RPC.

Supporting:
- log.state() — bundled {debug, log_file, servername, cwd} so backends
  get all logging-setup fields in one round trip.
- neo_tree.refresh_deferred / reveal_deferred — so callers don't build
  vim.defer_fn(...) as a string.
- health.lua: swapped nvim-send.sh for nvim-call.sh in the executable
  check; added a load check for code-preview.rpc.

Bug fixes uncovered during testing (both pre-existing, both around bash
quoting brittleness that this migration was designed to retire):
- detect_rm_paths didn't strip outer quotes, so `rm "/abs/path"`
  resolved to $CWD/"/abs/path". Now strips surrounding ' and " and
  handles ~/ via a quoted case pattern.
- xargs trim on RM_PATHS / WRITE_PATHS does shell-style quote
  processing — a single apostrophe in a filename (e.g. it's-mine.txt)
  silently zeroed the list. Replaced with parameter-expansion trim.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Cannon07 Cannon07 merged commit 56c3be4 into main May 16, 2026
2 checks passed
@Cannon07 Cannon07 deleted the feat/structured-rpc branch May 17, 2026 13:30
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