Skip to content

fix(cli): fork the whole import closure in adapter override (#418) - #437

Open
Agnik47 wants to merge 1 commit into
agentrhq:mainfrom
Agnik47:fix/418-adapter-override-sibling-imports
Open

fix(cli): fork the whole import closure in adapter override (#418)#437
Agnik47 wants to merge 1 commit into
agentrhq:mainfrom
Agnik47:fix/418-adapter-override-sibling-imports

Conversation

@Agnik47

@Agnik47 Agnik47 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes #418.

The bug

createAdapterOverride copied exactly one file:

const content = fs.readFileSync(pluginFile);
fs.writeFileSync(overridePath, content);   // ~/.webcmd/clis/<site>/<cmd>.js
fs.writeFileSync(basePath, content);       // clis/.base/<site>/<cmd>.js

Nothing followed the command's imports, so the fork still said
import { compactRepeatedText } from './shared.js' while shared.js stayed
behind in ~/.webcmd/plugins/<site>/.

This is not a corner case. In this repo:

count
plugin command files 871
files importing a relative sibling 510 (59%)
plugins affected 87

So the headline "fork an installed command and edit it" workflow was broken
for the majority of installed adapters.

The failure is quiet. Reproduced on main with the linkedin plugin:

$ webcmd adapter override linkedin/timeline
✅ Override created for linkedin/timeline

$ ls ~/.webcmd/clis/linkedin
timeline.js                       # shared.js was never copied

$ webcmd validate linkedin/timeline
⚠ Failed to load adapter ...\clis\linkedin\timeline.js: Cannot find module
  ...\clis\linkedin\shared.js imported from ...\clis\linkedin\timeline.js
webcmd validate: PASS             # passes — via the plugin copy, not the fork
Checked 1 command(s)
Errors: 0  Warnings: 0

The override throws on load, resolution falls back to the plugin copy, and
adapter status still reports the override as tracked. The user edits a file
that never runs, and every signal says everything is fine.

The fix

Resolve the command's transitive relative-import graph and copy it along with
the command, preserving nested paths (./_shared/protocol-capture.js), and
keep a fork-time .base/ copy of each file.

The same repro on this branch:

$ webcmd adapter override linkedin/timeline
✅ Override created for linkedin/timeline
     yours: ...\clis\linkedin\timeline.js
     base:  ...\clis\.base\linkedin\timeline.js
     also copied (imported by your command): shared.js

$ webcmd validate linkedin/timeline
webcmd validate: PASS             # no load failure — the fork itself loads

The closure is resolved before anything is written, so an adapter whose
imports cannot be copied from inside the plugin directory fails with nothing
left on disk rather than leaving the half-copied override this fix exists to
prevent.

Why copy rather than rewrite the imports

The issue offers two directions: copy the dependency closure, or rewrite the
override's imports to point back at the plugin directory. This takes the
first. The reporter's own workaround — rewriting to
../../plugins/linkedin/shared.js — is tied to the current layout, as they
noted, and an override that imports out of the plugin directory silently
changes behaviour when that plugin is updated and breaks outright when it is
uninstalled. A fork should be self-contained.

Keeping the rest of the override surface honest

Copied files are recorded on the override record (dependencies, an optional
field — existing provenance stores keep loading unchanged). That record is
what stops the copies from creating new problems:

  • adapter status reports them as dependency and names the override
    that imports each one, rather than user adapter — a label that invites
    deleting a file the fork needs:
    linkedin
      copied for override: linkedin/shared (imported by linkedin/timeline)
      override: linkedin/timeline (plugin linkedin)
    
    JSON rows gain a requiredBy column.
  • doctor no longer reports them as untracked shadows. Without this,
    every fork of a command with a sibling import would produce a warning
    telling the user to reset the fork they had just created.
  • plugin update reports upstream drift in them, naming the file
    (also changed upstream: shared.js). A fork whose real logic lives in
    ./shared.js would otherwise run its fork-time copy forever without a word
    — the same silent staleness the existing sha256 check exists to prevent,
    one import away.
  • adapter reset removes their base copies along with the override's.

One subtle case

A plugin command can import another command's file — linkedin's
salesnav-thread imports ./salesnav-inbox.js, which is itself the
registered command linkedin/salesnav-inbox. Forking the first therefore
places a second command's file in clis/. Overriding that second command now
adopts the existing copy instead of refusing with "an override already
exists" (a file webcmd itself placed there is not a reason to send the user to
adapter reset), and an existing copy is never overwritten, so edits already
made to it survive.

Tests

25 new tests, all failing before this change:

  • src/adapter-import-closure.test.ts (10) — sibling, transitive, nested,
    cycles, re-exports/side-effect/dynamic/require/double-quoted specifiers,
    bare specifiers ignored, missing target skipped, escaping specifier refused
    directly and transitively.
  • src/adapter-override.test.ts (11) — closure copied and loadable, base
    copies, recorded shas, dependencies omitted for single-file adapters,
    reset cleanup, user edits preserved, dependency adoption, and nothing
    written when the closure cannot be resolved.
  • src/adapter-shadow.test.ts (2) — copied files suppressed; a same-named
    file in a different site still reported.
  • src/cli.test.ts (2) — dependency classification in table and JSON.
  • src/plugin.test.ts (3) — dependency drift reported (changed and deleted
    upstream), unchanged dependency not reported.

vitest run --project unit --project plugin: 46 pre-existing failures on
main (Windows EPERM on fs.symlinkSync, plus hosted/site-memory
failures), the identical 46 on this branch — no regressions. tsc --noEmit
clean.

Out of scope

webcmd adapter path <site>/<command> returns
Adapter source is unavailable for ... after an override. That reproduces on
main for a single-file adapter with no imports at all, so it is a
separate defect from this one and is left alone here. Happy to open a
follow-up issue for it.

…#418)

`adapter override <site>/<command>` copied one file. 510 of the 871 adapter
files in this repo import a sibling helper, so for most commands the fork
threw `Cannot find module .../clis/<site>/shared.js` on load, command
resolution fell back to the plugin copy, and `adapter status` still reported
the override as tracked — the user edited a file that never ran.

Resolve the command's transitive relative-import graph and copy it with the
command, preserving nested paths, with a fork-time `.base/` copy of each file.
The closure is resolved before anything is written, so an adapter whose
imports cannot be copied from inside the plugin directory fails with nothing
left on disk.

Copied files are recorded on the override, which keeps the rest of the
override surface honest about them:

- `adapter status` reports them as `dependency`, naming the override that
  imports each one, instead of `user adapter` — a label that invites deleting
  a file the fork needs.
- `doctor` no longer reports them as untracked shadows, which would have
  turned every fork of a command with a sibling import into a warning telling
  the user to reset the fork they just made.
- `plugin update` reports upstream drift in them, so a fork whose real logic
  lives in `./shared.js` cannot go stale unreported.
- `adapter reset` removes their base copies along with the override's.

A plugin command can import another command's file (linkedin's
`salesnav-thread` imports `./salesnav-inbox.js`), so forking one command can
place a second command's file in `clis/`. Overriding that second command now
adopts the existing copy rather than refusing with "an override already
exists", and never overwrites a copy the user has edited.
@github-actions

Copy link
Copy Markdown
Contributor

🟠 Maintainer review suggested — low confidence

The automated review could not reach a fully supported conclusion.

Limitations

  • Some review context was unavailable or reduced.

This review is advisory and does not block merging.

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.

[Bug]: adapter override breaks adapters with sibling imports

1 participant