Add Vortex dependency/status dashlet and wcc_lite auto-acquisition (Unit J) - #23
Merged
TheValiantOne merged 5 commits intoAug 11, 2026
Merged
Conversation
…nit J) Adds a status dashlet showing WSM's dependency/status snapshot (text-merge and bundle dependency validity, mods directory, conflict count, detected QuickBMS/wcc_lite paths), and bundle-tooling detection/acquisition: QuickBMS is detected only (never downloaded, per its murkier licensing); wcc_lite is auto-fetched from its Nexus Mods "Official ModKit" page via Vortex's own Nexus-download mechanism, reusing archiveExtractor.ts's existing extract plumbing. Populates wsmEnv.ts's previously-unpopulated quickBmsPath/quickBmsPluginPath/wccLitePath fields. See the PR description for the licensing/EULA caveat this introduces and what was/wasn't verified against a real Vortex host. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
Move registerWsmStatusDashlet(context) out of context.once and into
main()'s own body: @nexusmods/vortex-api's own lib/api.d.ts documents
register functions as needing to be called immediately inside init, not
deferred into once, so the dashlet registration likely never took effect
against a real Vortex host. Also corrects a stale index.ts doc comment
(predating this unit) that told future units to register from inside
once, which would have propagated the same bug into later units.
Also fixes: findFileByNameBounded now propagates non-ENOENT readdir
errors instead of silently treating them as "nothing here" (previously
risked a false-negative detection triggering a destructive wipe +
re-download of a working install) and now prefers an x64-path match when
an archive ships both architectures; getWsmStatusSummary wraps its
pre-connect steps in their own try/catch so a filesystem error produces
the documented {kind:'error'} result instead of an unhandled rejection;
the status tile guards every async state setter against firing after
unmount; a duplicate fileExists/isEnoent implementation was removed in
favor of bundleTools.ts's now-exported copy; nexusDownloader.ts's timeout
error now names the last-observed download state; wccLiteAcquisition.ts's
"not found" error now names the exact mod/file id tried; and two stale
doc comments were corrected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
Both units add their own registration to index.ts's main(): Unit G's did-deploy conflict-scan handler (registered inside context.once, an event listener) and Unit J's status dashlet (registered directly in main(), per Vortex's own documented contract that register calls must not be deferred through context.once - Unit J's own header-comment correction on this point, citing the exact api.d.ts language, is kept as the canonical explanation here). vortexApiStub.ts's shared selectors object needed both Unit J's downloadPathForGame and Unit G's profileById as separate entries. index.test.ts needed the union of both branches' vi.hoisted mocks, vi.mock blocks, and test suites (Unit G's "did-deploy conflict scanning" describe block plus Unit J's dashlet-registration test). Verified after resolution: typecheck, build, lint, 128 unit tests, and 138 total tests including integration all pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
Unit I's merge-history dashlet merged to main since this branch's last
resolution. Both it and Unit J's status dashlet register directly in
main() (per the same register-outside-context.once contract Unit J's
own header-comment correction documents). Combined the imports, header
comment (now describing the merge-history dashlet as the third
registration, the status dashlet as the fourth), and both
context.register* calls. index.test.ts and vortexApiStub.ts merged
cleanly without manual intervention - Unit I's context.registerDashlet
stub and Unit J's vi.mock('./statusTile') approach don't overlap.
Verified after resolution: typecheck, build, lint, 138 unit tests, and
149 total tests including integration all pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
main now contains both Unit I's merge-history dashlet and Unit H's resolve action, each independently colliding with this branch's status-dashlet registration in index.ts/index.test.ts. Combined all three registrations (dashlet, resolve action, status tile) in main()'s body and renumbered the header doc-comment's registration narrative accordingly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
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.
Unit J — dependency/status tile + bundle-tooling acquisition
Summary
context.registerDashlet) showing WSM's dependency/status snapshot: text-merge/bundle dependency validity, mods directory (+ exists), conflict count, merged mod name, and detected QuickBMS/wcc_lite paths. Spawns a short-livedWsmMcpClientper its documented lifecycle policy, closed in afinally.src/bundleTools.ts) for QuickBMS and wcc_lite: checks this extension's own managed storage (getBundleToolsDir, reserved by Unit F) and, as a fallback, a priorIDCs/WitcherScriptMergerfork install's ownTools\folder (the build Vortex's built-ingame-witcher3extension already downloads asW3ScriptMerger).src/wccLiteAcquisition.ts+src/nexusDownloader.ts) via Vortex's own Nexus-download mechanism (api.ext.nexusDownload/nexusGetModFiles), reusingarchiveExtractor.ts's existing extract plumbing. QuickBMS is detection/link-only, never auto-downloaded.wsmEnv.ts's previously-unpopulatedquickBmsPath/quickBmsPluginPath/wccLitePathfields viadetectBundleTools(api), wired into the status tile's own WSM spawn.Licensing/EULA caveat — please read before merging
wcc_lite is an official CD Projekt Red tool ("The Witcher 3 modding tools" / "Official ModKit", Nexus mod id 3173 on the witcher3 domain), distributed via Nexus Mods, not a WSM-authored artifact. This PR auto-downloads it into a separate, Vortex-managed location (
<userData>/witcherscriptmerger-vortex/bundle-tools/wcc_lite/) — it never touches this repo's own source control, and the rootCLAUDE.md's existing "not in source control" policy for QuickBMS/wcc_lite is unchanged by this PR.That said: this extension redistributing/auto-fetching wcc_lite at runtime has not been independently confirmed against Nexus Mods' / CD Projekt Red's own redistribution terms beyond "it's an official tool hosted on an official Nexus mod page." The root
CLAUDE.mdalready treats QuickBMS/wcc_lite licensing as an explicitly open decision requiring the repo owner's sign-off before changing the "not bundled in source control" policy — auto-fetching at runtime into a Vortex-managed location is a different question from bundling in source control, but it still deserves the repo owner's explicit go/no-go before this ships broadly. Flagging this plainly rather than burying it, per this unit's own instructions.QuickBMS is intentionally not auto-downloaded (no canonical Nexus-hosted release found, redistribution terms murkier) — the status tile only detects an existing install or links to QuickBMS's own homepage (
http://aluigi.altervista.org/quickbms.htm), mirroringWitcherScriptMerger/Forms/DependencyForm.cs's own behavior for this exact dependency.Mod id / API-surface caveats
WitcherScriptMerger/Forms/DependencyForm.cs's own wcc_lite link (a Nexus news post announcing a ModKit update, itself pointing at this same mod page) — not verified against a live, authenticated Nexus session (this environment has no Nexus API key, and nexusmods.com returns HTTP 403 for unauthenticated scraping). The exact file id is deliberately not hardcoded — it's resolved live viaapi.ext.nexusGetModFiles, picking the file Nexus marksis_primary, so it doesn't go stale as the ModKit is updated.githubRelease.ts'snodeHttpsClientboundary. wcc_lite is fetched through Vortex's own Nexus-download mechanism (api.ext.nexusDownload), not a plain HTTPS GET — Nexus doesn't serve an unauthenticated direct-download URL for a rawHttpClientto hit even in principle. The mocked-download pipeline is instead covered by injecting a fakeNexusDownloader/ArchiveExtractor(src/wccLiteAcquisition.test.ts), the same DI-seam patterntoolAcquisition.test.tsalready uses for the WSM-tool pipeline.wcc_lite.exeisn't found anywhere inside it after extraction (bounded search, depth 6), acquisition throws a clear, specific error rather than silently failing or wrongly succeeding.api.ext.nexusDownload/nexusGetModFiles/selectors.downloadPathForGame/util.opnare all real, verified-against-typings API surfaces (node_modules/@nexusmods/vortex-api/lib/api.d.ts), but none of this PR's real Vortex-backed implementations (nexusDownloader.ts'screateVortexNexusDownloader, the dashlet's actual render/click flow) have been exercised against a real Vortex host — same "real but unexercised" statusarchiveExtractor.ts's owncreateVortexArchiveExtractoralready carries from Unit F.Testing
npm run typecheck && npm run build && npm run lint && npm test— all clean (84 unit tests across 13 files).npm run test:integration— all clean (92 tests across 16 files, including the pre-existing real-spawned-WSM-process tests and this unit's newtest/bundleTools.integration.test.ts, which proves real, no-mock local detection: a fakewcc_lite.exe/quickbms.exe+witcher3.bmsplaced under a real scratchgetBundleToolsDir(api)-shaped path is found without any network/extraction attempt, andacquireWccLiteshort-circuits before touching an injected downloader/extractor that would fail the test if invoked).get_statussubsequently reportingbundleDependenciesValid: trueafterward is unverified beyond the mocked/local tests above. Flagging this plainly per the task's own instructions.code-reviewskill), both rounds' findings fixed:Promisechaining bug (the "Get wcc_lite" button's busy state cleared before its own post-download refresh actually finished), a stale-error-not-cleared-on-refresh bug, a missing concurrency guard between the two tile buttons,<a target="_blank">vs.vortex-api's ownutil.opnfor external links inside Vortex's Electron renderer, missing wipe-before-extract/in-flight-coalescing onacquireWccLite(now mirrorstoolAcquisition.ts's own pattern), and an implicit (peer-dependency-only)react/react-dom/@types/reactresolution now made explicit inpackage.json.registerWsmStatusDashlet(context)was being called insidecontext.once(...), but@nexusmods/vortex-api's ownlib/api.d.tsdoc comment onIExtensionContextis explicit that register functions "must be called immediately inside the init function" and thatonceis documented as being for extension setup "except for the register calls" — so the dashlet registration would very likely never have taken effect against a real Vortex host. Fixed:registerWsmStatusDashlet(context)now runs directly inmain's body, synchronously, beforecontext.once(...). This also corrected a stale doc comment inindex.ts(predating this unit) that told future units to register insidecontext.once— left as-is, it would have propagated the same bug into units G/H/I's own registrations. Also fixed this round:findFileByNameBoundednow propagates non-ENOENTreaddirerrors instead of silently treating them as "nothing here" (a transient lock/permission error could otherwise causeacquireWccLiteto wipe a working install before an unnecessary re-download) and now prefers anx64-path match when an archive ships both architectures at the same depth;getWsmStatusSummarynow wraps its pre-connect steps (fileExists/detectBundleTools) in their own try/catch so a filesystem error there produces the documented{kind:'error'}shape instead of an unhandled rejection; the status tile now guards every async state-setter against firing after unmount (the tile's own visibility is tied to a live game-mode switch, so a user can navigate away mid-download); a stale duplicatefileExists/isEnoentimplementation was removed in favor of importingbundleTools.ts's (now-exported) copy;nexusDownloader.ts's timeout error now names the last-observedDownloadState(so a user-paused download reads as "paused," not a generic timeout);wccLiteAcquisition.ts's "no wcc_lite.exe found" error now names the exact mod/file id that was tried; and two stale doc comments (storage.ts'sgetBundleToolsDir,wsmEnv.ts's bundle-tool-path fields overclaiming universal population) were corrected.Known limitations left as-is (flagging rather than silently shipping)
getWsmStatusSummaryreports{kind:'not-acquired'}whenever no WSM build has been downloaded yet, and no unit so far (including this one) has built a "Get WitcherScriptMerger" UI trigger fortoolAcquisition.ts's ownacquireWsmTool(it's exported specifically for "a later unit's own UI trigger... to call on explicit user request," per that file's own doc comment, predating this unit). So for a user with no WSM build yet, this unit's dashlet — and the wcc_lite/QuickBMS detection it surfaces — is only reachable once some unit adds that trigger. Not this unit's scope to add (out of scope per the task instructions), but worth the coordinator's awareness.WsmStatusDashletContentitself (onlyregisterWsmStatusDashlet'sregisterDashletargument shape is tested) — this repo has no React testing-library dependency for any unit yet; adding one felt like scope creep for this unit specifically. The component's data-fetching logic (wsmStatusSummary.ts) has full coverage; the render/hook logic does not.statusTile.tswraps its content in a plain<div>rather thanvortex-api's own exportedDashletchrome component (confirmed real and unconditionally exported inlib/api.d.ts) — left as a documented, deliberate choice (see that file's own doc comment) rather than risk gettingIDashletProps' actual children-rendering behavior wrong without a real Vortex host to verify against.AI-assisted development
This PR was substantially produced by an AI coding agent (Claude Code), per
CONTRIBUTING.md's "AI-assisted development" section.