fix(cli): fork the whole import closure in adapter override (#418) - #437
Open
Agnik47 wants to merge 1 commit into
Open
fix(cli): fork the whole import closure in adapter override (#418)#437Agnik47 wants to merge 1 commit into
Agnik47 wants to merge 1 commit into
Conversation
…#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.
Contributor
🟠 Maintainer review suggested — low confidenceThe automated review could not reach a fully supported conclusion. Limitations
This review is advisory and does not block merging. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #418.
The bug
createAdapterOverridecopied exactly one file:Nothing followed the command's imports, so the fork still said
import { compactRepeatedText } from './shared.js'whileshared.jsstayedbehind in
~/.webcmd/plugins/<site>/.This is not a corner case. In this repo:
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
mainwith the linkedin plugin:The override throws on load, resolution falls back to the plugin copy, and
adapter statusstill reports the override as tracked. The user edits a filethat 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), andkeep a fork-time
.base/copy of each file.The same repro on this branch:
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 theynoted, 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 optionalfield — existing provenance stores keep loading unchanged). That record is
what stops the copies from creating new problems:
adapter statusreports them asdependencyand names the overridethat imports each one, rather than
user adapter— a label that invitesdeleting a file the fork needs:
requiredBycolumn.doctorno 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 updatereports upstream drift in them, naming the file(
also changed upstream: shared.js). A fork whose real logic lives in./shared.jswould 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 resetremoves their base copies along with the override's.One subtle case
A plugin command can import another command's file — linkedin's
salesnav-threadimports./salesnav-inbox.js, which is itself theregistered command
linkedin/salesnav-inbox. Forking the first thereforeplaces a second command's file in
clis/. Overriding that second command nowadopts 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 alreadymade 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, basecopies, recorded shas,
dependenciesomitted 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-namedfile in a different site still reported.
src/cli.test.ts(2) —dependencyclassification in table and JSON.src/plugin.test.ts(3) — dependency drift reported (changed and deletedupstream), unchanged dependency not reported.
vitest run --project unit --project plugin: 46 pre-existing failures onmain(WindowsEPERMonfs.symlinkSync, plus hosted/site-memoryfailures), the identical 46 on this branch — no regressions.
tsc --noEmitclean.
Out of scope
webcmd adapter path <site>/<command>returnsAdapter source is unavailable for ...after an override. That reproduces onmainfor a single-file adapter with no imports at all, so it is aseparate defect from this one and is left alone here. Happy to open a
follow-up issue for it.