From 27bb27fb0c192dbedd6db601bec466f75578832b Mon Sep 17 00:00:00 2001 From: Chris Knight Date: Mon, 10 Aug 2026 21:15:49 -0400 Subject: [PATCH 1/2] Update Vortex extension README, add packaging script, reconcile design doc against Units E-J README.md now describes what actually shipped (tool acquisition, conflict scanning, resolve action, merge history, status tile) instead of only Unit F's scaffold-era status, is explicit about how the WSM binary and wcc_lite downloads work and where they come from, documents manual install/build steps, and calls out known gaps (no UI trigger for initial WSM acquisition, no settings override surface). Adds `npm run package` (vortex-extension/scripts/package.mjs) to stage dist/ + info.json into a distributable zip for manual installation - no new dependencies, no CI/release workflow, no binaries committed. Reconciles docs/vortex-extension-design.md against the shipped Units E-J: notes the CLI-first invocation-model recommendation was superseded by an MCP-only implementation, marks which UX surfaces from section 5 actually shipped, updates section 2.2's setup-flow steps against toolAcquisition.ts/bundleTools.ts, and updates Open Questions 4/5 to Resolved based on verified WSM-side code (AppSettings.cs's WSM_ mechanism, the --version flag, release.yml). Open Questions 2 and 3 remain explicitly Open/owner-gated, and Open Question 1 is left untouched for Unit K's parallel work. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah --- docs/vortex-extension-design.md | 244 ++++++++++++++++++++++----- vortex-extension/.gitignore | 1 + vortex-extension/README.md | 236 +++++++++++++++++++++----- vortex-extension/package.json | 1 + vortex-extension/scripts/package.mjs | 89 ++++++++++ 5 files changed, 487 insertions(+), 84 deletions(-) create mode 100644 vortex-extension/scripts/package.mjs diff --git a/docs/vortex-extension-design.md b/docs/vortex-extension-design.md index ef946a9..d7189ed 100644 --- a/docs/vortex-extension-design.md +++ b/docs/vortex-extension-design.md @@ -15,14 +15,26 @@ outside WSM's own source control (see "External tool dependencies" in the root `CLAUDE.md`); read any KDiff3-specific mentions below as historical context, not current fact. -**Status: design document only, now with settled decisions.** No TypeScript/Node -scaffolding exists in this repository yet, and this refresh does not add any — actual -implementation remains a separate, later batch. What *has* changed since the first -draft: a planning effort (research, an advisor consult, and explicit owner decisions) -has settled several structural questions that were previously open, most importantly -**scope** and **location** (both below). This document is still the starting point for -that later implementation, updated to reflect what's now decided rather than left as -open options. +**Status: implementation has landed; this document is now reconciled against it, not +just a pre-implementation prediction.** `vortex-extension/` now exists in this +repository and has real, shipped code across several units (referred to below as +E through J): a build/entry-point scaffold and MCP stdio client (E), WSM tool +acquisition from GitHub Releases plus the `WSM_` env-var configuration +mechanism (F), post-deploy conflict scanning and notification (G), the "Resolve Script +Conflicts" action and merge panel (H), a merge-history dashlet (I), and a +dependency/status dashlet plus wcc_lite auto-acquisition (J). See +`vortex-extension/README.md` for a user-facing description of what actually shipped, +and this document's own sections below (particularly §2.2, §3, §5, and §6) for how that +compares against what was originally proposed here. A separate, later unit covering +Vortex/Collections coexistence hazards (§6, Open Question 1) may be in progress +elsewhere and is out of scope for this reconciliation pass — Open Question 1 below is +left exactly as it was. + +What *had* changed as of the previous refresh, before any of E–J existed: a planning +effort (research, an advisor consult, and explicit owner decisions) settled several +structural questions that were previously open, most importantly **scope** and +**location** (both below). This document was, and remains, the starting point the +implementation actually followed. **Scope (decided):** a **companion extension** to Vortex's existing, actively-maintained built-in Witcher 3 game extension (`game-witcher3` — see §0 for its current location). @@ -264,6 +276,49 @@ Setup flow: surfaces it (read-only or editable) in Vortex's per-game settings panel so the user can override it if they already have a WSM install they prefer. +**Reconciliation against Units E–J (this section's setup flow, step by step):** + +1. **Shipped**, close to as proposed: `toolAcquisition.ts`'s `ensureWsmToolRegistered` + is a local-only, network-free check for a previously-acquired binary in this + extension's own private storage, re-run on every extension load and every + `gamemode-activated` event (`index.ts`). **Not yet wired to any user-facing trigger, + though** — see the next bullet. +2. **Partially shipped, and diverges from the original proposal in two ways.** (a) is + real: `toolAcquisition.ts`'s `acquireWsmTool` downloads + `WitcherScriptMerger.Headless--win-x64.zip` from this repo's own GitHub + Releases (option (b) above — no bundled-copy option (a) was built, and none is + needed now that a self-contained publish exists). (b) **"verify via checksum before + trusting it" did not ship as a checksum** — `release.yml` publishes no checksum + manifest, so `githubRelease.ts`'s `downloadReleaseAsset` instead verifies the + downloaded byte count against the size GitHub's API reported for that asset: a + transfer-completeness check (catches truncation/corruption), not a cryptographic + integrity check (doesn't catch a maliciously-substituted asset of the same size). + (c) **No UI trigger exists yet for the actual acquisition call.** `acquireWsmTool` is + implemented, exported, and covered by `test/toolAcquisition.integration.test.ts`, but + no unit through J has registered a "Get WitcherScriptMerger" action that calls it — + `resolveAction.ts` only shows an error notification telling the user to acquire WSM + first if no tool is registered yet; it doesn't offer to do so. (d) **As of this + writing, no GitHub Release actually exists on this repo** (no version tag has been + pushed), so this download path is real but has never run against a real release — + see `vortex-extension/README.md`. (e) The minimum-supported-version check this step + originally anticipated once §6 Open Question 4's release-workflow/`--version` work + landed **was not built** — `AcquireWsmToolOptions.version` is caller-supplied with no + minimum-version comparison anywhere; see Open Question 4 below. +3. **Shipped, matching the proposed fallback exactly.** `bundleTools.ts`'s + `detectQuickBms`/`detectWccLite` check this extension's own managed storage first, + then fall back to detecting a prior `IDCs/WitcherScriptMerger` fork install's own + `Tools\` subfolder (the build `game-witcher3` may have already downloaded as + `W3ScriptMerger`) — exactly the "detect and reuse whatever `game-witcher3` already + fetched" mechanism this step proposed. wcc_lite additionally gained a real + auto-download path (`wccLiteAcquisition.ts`, Unit J) not originally scoped for this + section — see §6 Open Question 2, still genuinely open on whether that auto-download + should exist at all as a matter of policy, independent of the fact that it now does + as a matter of code. +4. **Not shipped.** No per-game settings panel exists for this extension, editable or + otherwise — there is currently no way for a user to override the resolved WSM binary + path via this extension's own UI. See `vortex-extension/README.md`'s "Known gaps" + section. + --- ## 3. Invocation model @@ -350,6 +405,33 @@ stdio handshake cost across a burst of related calls (e.g. `scan_conflicts` then exactly what a per-workflow process buys without paying for a daemon's crash/restart and orphaned-process-cleanup complexity. +**Reconciliation against Units E–J: implementation diverged from this section's +recommendation, and did so from the very first unit.** This section recommended +shipping the CLI `merge` verb first, treating MCP as a v2 enhancement. What actually +happened: Unit E (the foundation scaffold) built the hand-rolled MCP `child_process` +client — `mcpClient.ts` — as **its own main deliverable**, per its own PR description, +before any other extension feature existed, and no unit from E through J ever +implemented the CLI-driven `api.runExecutable('merge', ...)` + `MergeInventory.xml`-diff +flow this section described as the v1 default. Every feature that shipped (conflict +scanning, the resolve action, the merge-history dashlet, the status dashlet) is built +on `WsmMcpClient`, not the CLI. + +Two facts this section's own reasoning already supplies explain why, without needing to +speculate about unit-ordering decisions this document has no visibility into: (1) this +section's own `IRunOptions`-has-no-stdio finding means an MCP client was the only way to +get structured output or a pre-merge preview at all, so once *any* feature needed +either, that client had to exist; and (2) Unit H's "Resolve Script Conflicts" action, per +`resolveAction.ts`'s own doc comment, needed exactly that preview +(`merge_conflicts({dryRun: true})`) from its first version, not as a later addition. This +section's own listed CLI-only limitation — no pre-merge preview possible without +duplicating WSM's own scanning logic — is exactly the gap that requirement runs into. + +This is a correction to this document's own recommendation, not a claim it was +unreasonable at the time it was written. The CLI `merge` verb itself is unaffected by +any of this: it still exists in both hosts (see each host's own `CLAUDE.md`) and remains +available to a future headless/scripted caller that doesn't need MCP's richer surface — +it's just not what this extension itself ended up using anywhere. + --- ## 4. Data model mapping @@ -519,13 +601,67 @@ Proposed surface, roughly in order of how load-bearing each piece is: same conflict interactively. The extension should probably offer both as fallback actions rather than trying to reproduce manual conflict resolution itself. +**Reconciliation against Units E–J:** every proposed surface above shipped, on the MCP +("v2") shape throughout — see §3's own reconciliation note for why the CLI ("v1") path +was never built at all, so there is no "v1 first, v2 later" split in what actually +exists; each item below shipped as its v2 description, from its first version. + +- **Notification/badge — shipped (Unit G, `conflictNotifications.ts`).** The real v2 + behavior described above: a post-deploy `scan_conflicts` call, with a dashboard + notification only when the *unresolved* conflict set's signature has changed since + the last check that Vortex session (not an unconditional per-deploy prompt). + Suppressed during mod/dependency-install activity so a Collection install's + deploy-per-mod burst doesn't spawn a scan per mod or notify against a mid-install + state. +- **"Resolve Script Conflicts" action — shipped (Unit H, `resolveAction.ts` + + `mergePanel.ts`), the v2 shape but not full v2 scope.** Dry-run preview via + `merge_conflicts({dryRun: true})`, a Markdown dialog (`IDialogContent`'s `md` field, + not a custom React panel — see `mergePanel.ts`'s own doc comment for why: no JSX + pipeline exists in this project yet) showing merged/skipped/unmatched counts plus + function-level merge decisions, then confirm → real merge → result dialog. **Narrower + than this section proposed**: no per-file selection and no `orderOverrides` — the + shipped action always merges every detected conflict in one pass, a deliberate v1 + scope-cut noted in `resolveAction.ts`'s own doc comment, not an oversight. +- **Merge history view — shipped (Unit I, `mergeHistoryDashlet.ts`), via `list_merges` + specifically** — the "once available... for parity/simplicity" option this section + named, not a direct `MergeInventory.xml` parse. Shows relative path, merged mod name, + and per-source-mod hashes, with a manual Refresh button; fetches on mount and on + refresh only (not on a timer). +- **Dependency/status tile — shipped (Unit J, `statusTile.ts` + `wsmStatusSummary.ts`), + via `get_status`** as proposed, plus real (not just detected) wcc_lite acquisition + beyond what this section scoped — see §2.2's own reconciliation note (step 3) and §6 + Open Question 2 for the licensing caveat that addition raised. +- **Skipped/manual-resolution reporting — shipped (Unit H, `mergePanel.ts`'s "Needs + manual review" section), but resolved differently than either fallback this section + proposed.** Neither "open the sidecar in your own editor" nor "launch WSM's GUI" is + built as a distinct extension-side action — instead, WSM's own headless merge + (`DiffPlexMergeEngine.MergeHeadless`, per `WitcherScriptMerger.Core/Mcp/CLAUDE.md`) + already opens each skipped file's `DiffPlexConflicts/` sidecar in the OS's default + associated editor as a side effect of a real (non-dry-run) merge call, so the + extension's own dialog just reports that this already happened rather than adding a + second launcher on top of it. This only covers WSM's headless-flow file editor + though — `resolveAction.ts`'s own doc comment notes there's deliberately no + "launch WSM's GUI" fallback either, since Unit F only acquires the GUI-less + Headless build. +- **Not proposed by this section, but added: a "Get wcc_lite from Nexus Mods" button** + on the status tile (Unit J) — see §2.2's reconciliation note (step 3) and the + licensing caveat in §6 Open Question 2. +- **Proposed nowhere in this section, and still missing: any "Get WitcherScriptMerger" + trigger**, or any settings/override UI for the resolved WSM path — see §2.2's + reconciliation note (steps 2 and 4) and `vortex-extension/README.md`'s "Known gaps" + section. + --- ## 6. Open questions -Updated against the planning effort's findings — several of the original 8 are now -resolved (marked **Resolved**), one is **Partially resolved**, and the rest remain -genuinely open (marked **Open**) because nothing found so far settles them. +Updated against the planning effort's findings, and now again against what Units E–J +actually shipped (this pass) — several of the original 8 are now resolved (marked +**Resolved**), one is **Partially resolved**, and the rest remain genuinely open +(marked **Open**) because nothing found so far settles them. **Open Question 1 is left +exactly as it reads below, word for word, unchanged by this reconciliation pass** — a +separate unit's own work may bear on it and this document should not get ahead of +that. 1. **Relationship to `game-witcher3`'s existing built-in Script Merger integration (§0). Partially resolved.** The scope decision at the top of this document settles @@ -542,37 +678,56 @@ genuinely open (marked **Open**) because nothing found so far settles them. per-profile backup/restore having no concept of `MergeInventory.xml` at all (§0, new this round). Both need an explicit answer before real implementation, not just acknowledgment that they exist. -2. **Open.** Packaging/distribution strategy for QuickBMS/wcc_lite. The root `CLAUDE.md` - is explicit that both have unresolved licensing and must never enter source - control. Does that same caution block this extension from ever auto-downloading - them on the user's behalf, even from a third-party mirror? Or is "detect and reuse - whatever `game-witcher3` already fetched" (§2.2 step 3) the sanctioned answer, - permanently, regardless of how good WSM's own self-contained-publish story gets? - Nothing found this round resolves this — it's still a licensing/policy call for the - repo owner. +2. **Still Open — not resolved by this reconciliation, deliberately.** Packaging/ + distribution strategy for QuickBMS/wcc_lite. The root `CLAUDE.md` is explicit that + both have unresolved licensing and must never enter source control. Does that same + caution block this extension from ever auto-downloading them on the user's behalf, + even from a third-party mirror? Or is "detect and reuse whatever `game-witcher3` + already fetched" (§2.2 step 3) the sanctioned answer, permanently, regardless of how + good WSM's own self-contained-publish story gets? **Update: Unit J shipped code that + answers a narrower version of this question, without the repo owner's sign-off this + item calls for.** `wccLiteAcquisition.ts`/`nexusDownloader.ts` now auto-download + wcc_lite from its Nexus Mods "Official ModKit" page through Vortex's own + authenticated Nexus-download mechanism, going beyond the "detect and reuse" option + named above — see §2.2's own reconciliation note (step 3) and Unit J's own PR + description, which flags this exact tension explicitly rather than treating it as + settled. **This item remains marked Open regardless**: shipped code is not the same + as an owner decision, and the question this item asks (should this be happening at + all, as a matter of licensing policy) is still unanswered. QuickBMS itself is + unaffected — it is still never auto-downloaded, detection/link-only, per + `bundleTools.ts`. 3. **Open.** Does this become a public, Nexus-Mods-registry-listed Vortex extension, or stay a manually-installed/internal tool? This affects branding, support burden, - and whether Nexus Mods' own extension review process applies. Nothing found this - round resolves this either. -4. **Resolved.** Minimum supported WSM CLI/MCP version. A GitHub Actions release - workflow producing self-contained single-file builds attached to GitHub Releases, - plus a `--version` CLI flag and an MCP server-info version string, are landing - alongside this doc's refresh (a parallel WSM-side unit — not yet in `main` as of - this writing, so treat as imminent rather than already-available). Once live, the - extension has something concrete to version-check a download or an already-resolved - binary against, rather than trusting whatever a release tag happens to contain. -5. **Resolved (design-level; not yet landed in code).** Should WSM itself grow a - config-override mechanism, instead of requiring an external caller to hand-edit - `WitcherScriptMerger.exe.config` XML? Yes — the `WSM_` environment-variable - override described in §4.1 is the answer, and is that first-class, supported - mechanism once it exists: available to this extension, `game-witcher3`, and any - other caller, without anyone independently reimplementing XML surgery against an - internal config format that could change. **As of this writing, that mechanism does - not yet exist in `WitcherScriptMerger.Core/AppSettings.cs`** — it's landing - alongside this doc's refresh in a parallel WSM-side unit (same status as Open - Question 4's release-workflow/`--version` work), not already merged to `main`. Until - it lands, an implementation starting today has to fall back to the hand-edit pattern - and its interim safety rule described in §4.1. + and whether Nexus Mods' own extension review process applies. Nothing found in + Units E–J resolves this either — see `vortex-extension/README.md`'s explicit note + that the extension is not yet published anywhere. +4. **Resolved, with one caveat.** Minimum supported WSM CLI/MCP version. A GitHub + Actions release workflow (`.github/workflows/release.yml`) producing self-contained + single-file builds attached to GitHub Releases, plus a `--version` CLI flag + (confirmed present in both `WitcherScriptMerger/Program.cs` and + `WitcherScriptMerger.Headless/Program.cs`) and an MCP server-info version string + (`WitcherScriptMerger.Core/VersionInfo.cs`), have landed in `main`. **Caveat: the + enabling mechanism is resolved, but the actual minimum-version check this question + was really asking about was never built** — `toolAcquisition.ts`'s + `AcquireWsmToolOptions.version` is caller-supplied with no comparison against any + minimum anywhere in the extension. As of this writing, no version tag has actually + been pushed to this repo either, so no GitHub Release exists yet for any of this to + version-check against in practice — see `vortex-extension/README.md`. +5. **Resolved — now landed in code, not just at the design level.** Should WSM itself + grow a config-override mechanism, instead of requiring an external caller to + hand-edit `WitcherScriptMerger.exe.config` XML? Yes — the `WSM_` + environment-variable override described in §4.1 is the answer, and it is now real: + `WitcherScriptMerger.Core/AppSettings.cs` defines + `EnvironmentVariablePrefix = "WSM_"` and `GetEnvironmentOverride(key)`, checked + before falling through to `ConfigurationManager`. `vortex-extension/src/wsmEnv.ts` + (`buildWsmEnv`/`mergeWithProcessEnv`) is this extension's own client for exactly + this mechanism, and it's the *only* way the extension configures a spawned WSM + process — it never reads or writes `.exe.config`/`.dll.config` XML anywhere. + `test/toolAcquisition.integration.test.ts` proves this end-to-end against a real + spawned process (deliberately setting wrong placeholder values in a scratch XML + config and asserting the env-var override wins). The hand-edit pattern and its + interim safety rule described earlier in §4.1 are now historical context only — + nothing in this extension uses them. 6. **Resolved.** Process lifecycle for MCP mode (§3): spawn per user-initiated workflow, tear down when the relevant panel/dashlet closes. Not a permanent session-long daemon, and not spawn-per-tool-call either — see §3's reasoning @@ -644,3 +799,12 @@ genuinely open (marked **Open**) because nothing found so far settles them. `init(context)`, not `activate(context)` as the first draft had it. - Reporting on Nexus Mods' 2026 SteamOS/Steam Deck commitment for Vortex (PC Gamer, Steam Deck HQ, OpenCritic coverage of the Nexus Mods roadmap announcement). +- **This reconciliation pass (Units E–J against this document)**: the actual shipped + source under `vortex-extension/src/` and `vortex-extension/test/` (read in full, not + sampled), each unit's own merged PR description (`gh pr list`/`gh pr view` against + `TheValiantOne/WitcherScriptMerger`), and direct verification of the WSM-side claims + this document makes about code outside `vortex-extension/` — + `WitcherScriptMerger.Core/AppSettings.cs` (the `WSM_` mechanism, Open + Question 5), `.github/workflows/release.yml` and both hosts' `Program.cs` (the + `--version` flag and release workflow, Open Question 4) — rather than trusting this + document's own prior "landing alongside this refresh" language at face value. diff --git a/vortex-extension/.gitignore b/vortex-extension/.gitignore index 480ab16..90867d1 100644 --- a/vortex-extension/.gitignore +++ b/vortex-extension/.gitignore @@ -1,4 +1,5 @@ node_modules/ dist/ +release/ *.tsbuildinfo coverage/ diff --git a/vortex-extension/README.md b/vortex-extension/README.md index 2c46222..533cbe5 100644 --- a/vortex-extension/README.md +++ b/vortex-extension/README.md @@ -1,11 +1,171 @@ # witcherscriptmerger-vortex A [Vortex](https://www.nexusmods.com/about/vortex/) (Nexus Mods' mod manager) companion -extension for WitcherScriptMerger (WSM). It drives WSM's `mcp` server mode (see the repo -root `CLAUDE.md` and `WitcherScriptMerger.Core/Mcp/CLAUDE.md`) from inside Vortex, as a -companion to Vortex's own built-in `game-witcher3` extension - it does **not** register -the `witcher3` game itself, and every feature it adds is gated on Witcher 3 being the -currently active game. +extension for WitcherScriptMerger (WSM). It is **not** a replacement for Vortex's own +built-in Witcher 3 game extension (`game-witcher3`) or that extension's built-in Script +Merger integration - it's a companion that adds a second, distinctly-branded discovered +tool and its own conflict-scanning/merge/history/status UI, gated on Witcher 3 being the +active game, alongside whatever `game-witcher3` already does. It never calls +`context.registerGame` and never touches `game-witcher3`'s own registrations. See +`docs/vortex-extension-design.md` at the repo root for the full design rationale (section +0 in particular, for exactly what `game-witcher3` already does on its own). + +**Not yet published to Vortex's in-app extension registry.** Installing it today means +building it yourself and dropping it into your own Vortex plugins folder - see "Install" +below. Whether this ever becomes a publicly-listed extension is still an open decision +(design doc, section 6, Open Question 3) - nothing here should be read as implying +otherwise. + +## What it does + +Everything below is gated on Witcher 3 being Vortex's currently active game - none of it +does anything for any other game. + +- **Acquires a WSM build automatically** (`src/toolAcquisition.ts`): downloads the + `WitcherScriptMerger.Headless--win-x64.zip` release asset from this repo's own + GitHub Releases (`TheValiantOne/WitcherScriptMerger`), verifies the downloaded byte + count against what GitHub's API reported (a transfer-completeness check, not a + cryptographic signature), extracts it into this extension's own private storage + (under Vortex's `userData` folder), and registers it as a Vortex discovered tool - + `WitcherScriptMergerEnhanced` ("WitcherScriptMerger (Enhanced)" / "WSM+"), a + deliberately different tool ID from `game-witcher3`'s own `W3ScriptMerger` (which + downloads and launches a different, older WSM fork's GUI). **As of this writing, no + version tag has been pushed to this repo, so no GitHub Release actually exists yet** - + the download logic itself is real and unit-tested against a mocked HTTP client, and the + rest of the pipeline (extract, install-marker, tool registration, `WSM_` + environment-variable configuration reaching a real spawned process) is proven end-to-end + in `test/toolAcquisition.integration.test.ts` using a locally-built binary standing in + for a downloaded one - but the actual GitHub-Releases download step has never been + exercised against a real release. +- **Scans for script conflicts after every deployment** (`src/conflictScan.ts`, + `src/conflictNotifications.ts`): once a WSM build has been acquired, every Vortex + `did-deploy` for Witcher 3 triggers a short-lived WSM MCP process, calls its + `scan_conflicts` tool, and shows (or updates/dismisses) a dashboard notification when + the set of *unresolved* conflicts has actually changed since the last check that Vortex + session - not an unconditional "check for conflicts?" prompt on every deploy. + Suppressed while a mod/dependency install (e.g. installing a Collection) is still in + progress, so a burst of deploy-per-mod cycles doesn't spawn a WSM process per mod or + show a stale mid-install notification. +- **A "Resolve Script Conflicts" action** (`src/resolveAction.ts`, + `src/mergePanel.ts`): a button on the Mods page toolbar. Clicking it spawns a WSM MCP + process for a dry-run preview (`merge_conflicts({dryRun: true})`), shows a dialog with + merged/skipped/unmatched counts plus any function-level merge decisions (cases where a + whole-file merge failed but merging function-by-function succeeded), and - only on + confirmation - spawns a second process to run the real merge and shows its result. v1 + scope, deliberately: merges every detected conflict in one pass; there's no per-file + selection or custom merge-order override yet. +- **A merge-history dashboard tile** (`src/mergeHistoryDashlet.ts`): lists every merge + WSM has already recorded (via its MCP `list_merges` tool) - relative path, which merged + mod folder holds the result, and each source mod's recorded hash - with a manual + Refresh button. +- **A dependency/status dashboard tile** (`src/statusTile.ts`, + `src/wsmStatusSummary.ts`): shows whether WSM's text-merge engine and bundle-content + tooling (QuickBMS/wcc_lite) are ready, the resolved mods directory (and whether it + exists), the configured merged-mod name, and a live conflict count - so a setup problem + shows up here instead of as a confusing failure mid-deploy. Also offers a "Get wcc_lite + from Nexus Mods" button - see the next section for exactly what that does. + +### Known gaps in what's shipped so far + +- **No in-Vortex button triggers the *initial* WSM download yet.** `acquireWsmTool` (the + full download/verify/extract/register pipeline) is implemented and exported, but no + unit built so far has wired it to a UI action - it's only ever exercised by this + project's own tests. Until a later unit adds that trigger, a fresh install has two + options: wait for that action to land, or place an already-built + `WitcherScriptMerger.Headless.exe` yourself under + `\witcherscriptmerger-vortex\tool\` (Vortex's own `userData` folder is + typically `%APPDATA%\Vortex`) - the extension re-registers whatever it finds there as + a discovered tool on every load and every game-mode switch, with no network access + needed for that re-registration step. **Its `.dll.config` file needs to sit right next + to it too** - WSM reads settings via `ConfigurationManager` against that file, and its + `AppSettings` constructor calls `Environment.Exit(1)` with no further diagnostic if it + can't find one, so an exe copied there alone fails silently on launch. +- **No settings UI lets you point the extension at an existing WSM install** you already + have elsewhere - the design doc originally proposed a per-game settings-panel override + for this; it hasn't been built. Today, the only way this extension resolves a WSM path + is the acquisition/re-registration flow above (its own private storage directory) - + there is no override surface yet. + +## Being transparent about what gets downloaded, from where, and by whom + +Two different automatic downloads exist, and neither is this extension (or WSM) +bundling/redistributing anything itself: + +- **The WSM build itself** (`src/githubRelease.ts`/`src/toolAcquisition.ts`) is a plain + HTTPS GET against `api.github.com`, fetching a build produced by *this same repository's + own* `.github/workflows/release.yml` - i.e. WSM downloading itself, essentially, the + same way any tool auto-updater would. +- **wcc_lite** (`src/wccLiteAcquisition.ts`, `src/nexusDownloader.ts`) - needed only for + `.bundle`-content (DLC/expansion) conflicts, never for flat-file `.ws`/`.xml` conflicts + - is fetched differently: through Vortex's *own* Nexus Mods integration + (`api.ext.nexusDownload`), using the user's own already-authenticated Nexus session, + from the "Official ModKit" mod page (Nexus mod id 3173 on the `witcher3` domain, + published by CD Projekt RED) - the same official tool WSM's own GUI + (`DependencyForm.cs`) already points users at manually. It's downloaded with + **`allowInstall: false`**, specifically so Vortex never deploys or load-orders it as a + mod - it lands in this extension's own private storage + (`\witcherscriptmerger-vortex\bundle-tools\wcc_lite\`), not the game's + Mods folder. This extension does not host, mirror, or repackage wcc_lite anywhere; it + only automates the same manual "go get it from Nexus" step a user would otherwise do by + hand, through Vortex's own download machinery. + - **This 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.md` already treats QuickBMS/wcc_lite packaging/licensing as + an open decision requiring the repo owner's sign-off, and auto-fetching at runtime + into a Vortex-managed location (rather than committing to source control, which this + still never does) is a related but distinct question that is *also* still open. See + `docs/vortex-extension-design.md`, section 6, Open Question 2. +- **QuickBMS** is never downloaded automatically by this extension at all (no canonical + Nexus-hosted release was found for it, and its redistribution terms are murkier than + wcc_lite's) - the status tile only detects an existing local install or links to + QuickBMS's own homepage, mirroring WSM's own GUI for this exact dependency. + +## Install (manual - not yet published anywhere) + +``` +cd vortex-extension +npm install +npm run package +``` + +`npm run package` runs the typecheck + webpack build (`npm run build`) and then stages +the result into a distributable zip: `dist/index.js`(`.map`) plus the root `info.json` +manifest, copied flat (no nested subfolder) into +`release/witcherscriptmerger-vortex-.zip` (and an equivalent unzipped +`release/witcherscriptmerger-vortex/` folder, if you'd rather copy files directly). +`release/` is gitignored - nothing under it is ever committed. + +To install: extract that zip's contents (or copy the staged folder's contents) so +`index.js` and `info.json` land directly inside + +``` +%APPDATA%\Vortex\plugins\witcherscriptmerger-vortex\ +``` + +The folder name under `plugins\` is arbitrary - `info.json` declares no explicit `id` +field (`@nexusmods/vortex-api`'s own `IExtension` typing marks it optional), so nothing +in Vortex's own manifest format requires a specific folder name. `witcherscriptmerger-vortex` +above just matches this project's own `package.json` name and its npm/git identity, for +consistency with everything else this project already calls itself. Restart Vortex (or +use its "Extensions" page reload, if available) afterward to pick it up. + +If you'd rather do this by hand without the packaging script: `npm run build` alone +produces `dist/index.js` (+ `dist/index.js.map`), and you'd need to copy both of those +plus the root `info.json` into the same plugins subfolder yourself. + +## Requirements + +- **A WSM build capable of `mcp` mode** - either the CLI/MCP-only + `WitcherScriptMerger.Headless.exe` this extension's own tool-acquisition pipeline + downloads (once wired to a UI trigger - see "Known gaps" above; in the meantime, see + that section's manual-placement workaround), or the full WinForms + `WitcherScriptMerger.exe`, which also supports `mcp` mode. Either way, this is a + Windows-only requirement today, matching Vortex itself being Windows-only. +- **QuickBMS and wcc_lite are only needed for `.bundle`-content (DLC/expansion) + conflicts** - ordinary flat-file `.ws`/`.xml` conflicts merge with neither installed, + via WSM's in-process DiffPlex-based merge engine. See "Being transparent..." above for + exactly how (and whether) this extension can get wcc_lite for you; QuickBMS is always + a manual, user-sourced install (see that section). ## This is a separate toolchain @@ -14,46 +174,34 @@ independent of the rest of this repository's .NET solution (`WitcherScriptMerger.sln`). `dotnet build`/`dotnet format` at the repo root never look inside this folder, and nothing here is reachable from them. +## Dev workflow + ``` -cd vortex-extension -npm install -npm run build # typecheck + webpack bundle -> dist/index.js +npm run typecheck # tsc --noEmit +npm run build # typecheck + webpack bundle -> dist/index.js +npm run package # build + stage/zip for manual install - see "Install" above npm run lint -npm test # fast, Node-only unit tests +npm test # fast, Node-only unit tests +npm run test:integration # slower, real-process integration tests (needs the .NET SDK) ``` -`npm test` only runs the fast, Node-only unit tests (`src/**/*.test.ts`) - no .NET SDK -needed. The real, spawned-process integration tests are a separate script, `npm run -test:integration`, since they need a local .NET SDK and a built/published -`WitcherScriptMerger.Headless` - kept out of the default `npm test` so a Node-only -environment (e.g. a contributor machine or CI runner without the .NET SDK on `PATH`) -isn't forced through a multi-minute .NET build just to iterate on this extension's own -TypeScript. Two different `WitcherScriptMerger.Headless` invocations are involved: -`test/mcpClient.integration.test.ts` runs a plain `dotnet build` itself if the exe isn't -already present (framework-dependent, fast); `test/toolAcquisition.integration.test.ts` -instead runs `dotnet publish -c Release -p:PublishProfile=win-x64` (self-contained, -single-file, matching `.github/workflows/release.yml`'s own publish step exactly) if -that specific publish output isn't already present - slower on a cold run (produces a -~78 MB standalone exe) since it stands in for a downloaded-and-extracted release asset, -which the plain `dotnet build` output doesn't represent. - -## Status - -The foundation scaffold (info.json manifest, build tooling, the `init(context)` entry -point, and the shared MCP stdio client in `src/mcpClient.ts`) is in place, plus one real -feature: **tool acquisition**. `src/toolAcquisition.ts` downloads a WSM release build -from GitHub Releases, verifies/extracts it, and registers it as a discovered Vortex tool -(`src/discoveredTool.ts`, tool ID `WitcherScriptMergerEnhanced` - distinct from Vortex's -own built-in `game-witcher3` extension's `W3ScriptMerger`). `src/wsmEnv.ts` builds the -`WSM_` environment-variable overrides (see -`WitcherScriptMerger.Core/AppSettings.cs`) used to configure a spawned WSM process - -never by editing its `.exe.config`/`.dll.config` XML. **The actual GitHub-Releases -download path is unverified against a real release** - no version tag has been pushed to -this repo yet, so no release exists; see `src/githubRelease.ts`'s own doc comment and -this feature's own PR description for exactly what was verified instead (a mocked-HTTP -unit test for the download logic, plus a full acquisition/registration/env-var-config -integration test using a locally-built binary standing in for a downloaded one). - -Conflict scanning, the merge panel, and dashlets are separate, later units not yet built -on top of this scaffold. See `docs/vortex-extension-design.md` for the fuller design -context this scaffold and the tool-acquisition unit follow. +Two-tier test convention: + +- `src/**/*.test.ts` (run by `npm test`) - fast, mocked-dependency unit tests. No .NET + SDK needed, so a Node-only environment (a contributor machine or CI runner without + `dotnet` on `PATH`) can iterate on this extension's own TypeScript without ever + touching the .NET side. +- `test/**/*.integration.test.ts` (run by `npm run test:integration`, `--no-file-parallelism`) + - real, no-mocks tests that spawn an actual `WitcherScriptMerger.Headless` process. + Two different invocations are involved: `test/mcpClient.integration.test.ts` runs a + plain `dotnet build` itself if the exe isn't already present (framework-dependent, + fast); `test/toolAcquisition.integration.test.ts` instead runs + `dotnet publish -c Release -p:PublishProfile=win-x64` (self-contained, single-file, + matching `.github/workflows/release.yml`'s own publish step exactly) if that specific + publish output isn't already present - slower on a cold run (produces a large, + standalone exe), since it stands in for a downloaded-and-extracted release asset that + the plain `dotnet build` output doesn't represent. + +## License + +GPLv2, matching the root `LICENSE` file - this folder isn't separately licensed. diff --git a/vortex-extension/package.json b/vortex-extension/package.json index 703f116..5d1ebac 100644 --- a/vortex-extension/package.json +++ b/vortex-extension/package.json @@ -8,6 +8,7 @@ "typecheck": "tsc --noEmit", "build": "npm run typecheck && webpack --config webpack.config.cjs", "watch": "webpack --config webpack.config.cjs --mode development --watch", + "package": "npm run build && node scripts/package.mjs", "lint": "eslint src test", "test": "vitest run src", "test:integration": "vitest run test --no-file-parallelism" diff --git a/vortex-extension/scripts/package.mjs b/vortex-extension/scripts/package.mjs new file mode 100644 index 0000000..3b22ea2 --- /dev/null +++ b/vortex-extension/scripts/package.mjs @@ -0,0 +1,89 @@ +#!/usr/bin/env node +// Stages this extension's build output into a distributable zip for manual +// installation into %APPDATA%\Vortex\plugins\\ - see README.md's "Install" +// section for the full manual-install flow this replaces the copy-by-hand steps for. +// +// Scope note (deliberately minimal): this commits no binaries and asserts no +// distribution model. It only stages `dist/` (webpack's own output - just +// `index.js`/`index.js.map`, see webpack.config.cjs) plus `info.json` (the Vortex +// extension manifest) into a local, gitignored `release/` folder and zips that folder. +// It does not upload, publish, or register anywhere - see +// docs/vortex-extension-design.md, section 6, Open Question 3 (public Nexus-registry +// listing), which remains open and is not resolved by this script existing. +// +// Requires `npm run build` to have already produced `dist/index.js` - this script +// fails fast with a clear message rather than silently packaging a stale/missing dist. +// +// Zipping is platform-conditional rather than an added npm dependency: PowerShell's +// `Compress-Archive` on Windows (matching this repo's own `release.yml`, which is also +// pwsh-driven for its Windows-built assets), or the `zip` CLI on macOS/Linux (present +// on both by default) for anyone building this extension outside Windows even though +// Vortex itself only runs on Windows today (docs/vortex-extension-design.md, Open +// Question 8) - the built extension is only useful there, but nothing about producing +// the zip itself requires it. + +import { execFileSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(__dirname, '..'); +const DIST_DIR = path.join(ROOT, 'dist'); +const INFO_JSON = path.join(ROOT, 'info.json'); +const RELEASE_DIR = path.join(ROOT, 'release'); + +const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')); +// The staged folder's own name - not read from info.json (which declares no explicit +// "id" field; @nexusmods/vortex-api's own IExtension typing marks `id` optional), so +// there's no single canonical extension id to derive this from. package.json's own +// `name` is used instead, matching this repo's git history/npm package identity. +const stageName = pkg.name; +const stageDir = path.join(RELEASE_DIR, stageName); + +if (!fs.existsSync(path.join(DIST_DIR, 'index.js'))) { + console.error("dist/index.js not found - run 'npm run build' first (or use 'npm run package', which does this for you)."); + process.exit(1); +} + +fs.rmSync(RELEASE_DIR, { recursive: true, force: true }); +fs.mkdirSync(stageDir, { recursive: true }); + +for (const file of fs.readdirSync(DIST_DIR)) { + fs.copyFileSync(path.join(DIST_DIR, file), path.join(stageDir, file)); +} +fs.copyFileSync(INFO_JSON, path.join(stageDir, 'info.json')); + +const zipPath = path.join(RELEASE_DIR, `${stageName}-${pkg.version}.zip`); + +if (process.platform === 'win32') { + execFileSync( + 'powershell', + [ + '-NoProfile', + '-NonInteractive', + '-Command', + `Compress-Archive -Path '${stageDir}\\*' -DestinationPath '${zipPath}' -Force`, + ], + { stdio: 'inherit' }, + ); +} else { + try { + execFileSync('zip', ['-r', path.basename(zipPath), stageName], { cwd: RELEASE_DIR, stdio: 'inherit' }); + } catch (err) { + console.error( + `\nCould not run 'zip' (${err instanceof Error ? err.message : String(err)}). This script's posix zip path ` + + "hasn't been exercised beyond a missing-binary check - install a 'zip' CLI, or skip the zip step and copy " + + `the staged folder directly instead:\n ${stageDir}`, + ); + process.exit(1); + } +} + +console.log(`\nPackaged: ${zipPath}`); +console.log(`Staged (unzipped) folder: ${stageDir}`); +console.log( + `\nManual install: extract the zip (or copy the staged folder above) so its contents land directly in\n` + + ` %APPDATA%\\Vortex\\plugins\\${stageName}\\\n` + + `i.e. that folder should directly contain index.js and info.json, not a nested subfolder.`, +); From ddd30d818da25aec8740fe03dd8269b5a917cade Mon Sep 17 00:00:00 2001 From: Chris Knight Date: Mon, 10 Aug 2026 21:24:50 -0400 Subject: [PATCH 2/2] Fix package.mjs: flat zip on all platforms, safer PowerShell quoting Two independent code-review passes found the posix zip branch nested staged files under a / subfolder while the Windows branch produced a flat archive - contradicting this same script's own "not a nested subfolder" install instructions. Fixed by cd-ing into the staged directory and zipping its contents on both branches, mirroring release.yml's own `cd && zip -r ../out.zip .` pattern (which runs on plain Ubuntu via bash, not PowerShell - corrected a comment that had claimed a false pwsh parity with that step). Also: escape embedded single quotes before interpolating paths into the PowerShell command (a repo path containing an apostrophe would otherwise break the quoted string), wrap the Windows branch in the same try/catch the posix branch already had for a clear error instead of a raw stack trace, and check info.json exists up front alongside the existing dist/index.js check. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah --- vortex-extension/scripts/package.mjs | 91 +++++++++++++++++++--------- 1 file changed, 61 insertions(+), 30 deletions(-) diff --git a/vortex-extension/scripts/package.mjs b/vortex-extension/scripts/package.mjs index 3b22ea2..941ad97 100644 --- a/vortex-extension/scripts/package.mjs +++ b/vortex-extension/scripts/package.mjs @@ -15,12 +15,17 @@ // fails fast with a clear message rather than silently packaging a stale/missing dist. // // Zipping is platform-conditional rather than an added npm dependency: PowerShell's -// `Compress-Archive` on Windows (matching this repo's own `release.yml`, which is also -// pwsh-driven for its Windows-built assets), or the `zip` CLI on macOS/Linux (present -// on both by default) for anyone building this extension outside Windows even though -// Vortex itself only runs on Windows today (docs/vortex-extension-design.md, Open -// Question 8) - the built extension is only useful there, but nothing about producing -// the zip itself requires it. +// `Compress-Archive` on Windows, or the `zip` CLI on macOS/Linux (present on both by +// default) for anyone building this extension outside Windows even though Vortex +// itself only runs on Windows today (docs/vortex-extension-design.md, Open Question 8) +// - the built extension is only useful there, but nothing about producing the zip +// itself requires it. Both branches `cd` into the staged folder and zip its *contents* +// (not the folder itself), matching this repo's own `.github/workflows/release.yml` +// (its `package-release` job's own `zip -r` step, `( cd publish/... && zip -r +// ../../dist/out.zip . )`) - so the resulting archive extracts flat, with `index.js`/ +// `info.json` at the zip root, not nested one level down under a `/` folder. +// (That release.yml step runs on plain Ubuntu via bash's `zip`, not PowerShell - it's +// the *archive layout*, not the tool, this mirrors.) import { execFileSync } from 'node:child_process'; import * as fs from 'node:fs'; @@ -45,45 +50,71 @@ if (!fs.existsSync(path.join(DIST_DIR, 'index.js'))) { console.error("dist/index.js not found - run 'npm run build' first (or use 'npm run package', which does this for you)."); process.exit(1); } +if (!fs.existsSync(INFO_JSON)) { + console.error(`info.json not found at '${INFO_JSON}' - this is this extension's own Vortex manifest and should always be present.`); + process.exit(1); +} fs.rmSync(RELEASE_DIR, { recursive: true, force: true }); fs.mkdirSync(stageDir, { recursive: true }); -for (const file of fs.readdirSync(DIST_DIR)) { - fs.copyFileSync(path.join(DIST_DIR, file), path.join(stageDir, file)); -} +// cpSync (not a manual readdir+copyFileSync loop) so this doesn't break if +// webpack.config.cjs's output ever grows a subdirectory (e.g. code-splitting) - +// today's output is flat (just index.js/index.js.map), but recursive copy costs +// nothing and removes that assumption. +fs.cpSync(DIST_DIR, stageDir, { recursive: true }); fs.copyFileSync(INFO_JSON, path.join(stageDir, 'info.json')); const zipPath = path.join(RELEASE_DIR, `${stageName}-${pkg.version}.zip`); -if (process.platform === 'win32') { - execFileSync( - 'powershell', - [ - '-NoProfile', - '-NonInteractive', - '-Command', - `Compress-Archive -Path '${stageDir}\\*' -DestinationPath '${zipPath}' -Force`, - ], - { stdio: 'inherit' }, - ); -} else { - try { - execFileSync('zip', ['-r', path.basename(zipPath), stageName], { cwd: RELEASE_DIR, stdio: 'inherit' }); - } catch (err) { - console.error( - `\nCould not run 'zip' (${err instanceof Error ? err.message : String(err)}). This script's posix zip path ` + - "hasn't been exercised beyond a missing-binary check - install a 'zip' CLI, or skip the zip step and copy " + - `the staged folder directly instead:\n ${stageDir}`, +// Escapes a path for embedding inside a PowerShell single-quoted string: doubling an +// embedded `'` is PowerShell's own escape for that context (not a backslash escape, +// which single-quoted PowerShell strings don't interpret at all) - without this, a +// repo checked out under a path containing an apostrophe (e.g. a Windows user profile +// like `C:\Users\O'Brien\...`) would prematurely terminate the quoted string and fail +// with a PowerShell parse error rather than produce a zip. +function escapePowerShellSingleQuoted(value) { + return value.replace(/'/g, "''"); +} + +try { + if (process.platform === 'win32') { + // -DestinationPath refers to a location one level up from stageDir (RELEASE_DIR), + // so this doesn't need stageDir to exist as a Compress-Archive *source* root + // itself - '\*' selects stageDir's contents, producing a flat archive (index.js/ + // info.json at the zip root), matching the posix branch below. + execFileSync( + 'powershell', + [ + '-NoProfile', + '-NonInteractive', + '-Command', + `Compress-Archive -Path '${escapePowerShellSingleQuoted(stageDir)}\\*' -DestinationPath '${escapePowerShellSingleQuoted(zipPath)}' -Force`, + ], + { stdio: 'inherit' }, ); - process.exit(1); + } else { + // cwd: stageDir (not RELEASE_DIR) + zipping '.' is what makes this flat, mirroring + // release.yml's own `cd && zip -r ../../dist/out.zip .` pattern - + // zipping `stageName` from RELEASE_DIR instead (an earlier version of this script + // did exactly that) nests every entry under a `/` prefix, contradicting + // this script's own "lands directly in ..., not a nested subfolder" install + // instructions below. + execFileSync('zip', ['-r', zipPath, '.'], { cwd: stageDir, stdio: 'inherit' }); } +} catch (err) { + console.error( + `\nFailed to create the zip (${err instanceof Error ? err.message : String(err)}). This script's zip step ` + + `needs '${process.platform === 'win32' ? 'powershell' : 'zip'}' on PATH. As a fallback, you can skip ` + + `zipping entirely and copy the staged folder's contents directly instead:\n ${stageDir}`, + ); + process.exit(1); } console.log(`\nPackaged: ${zipPath}`); console.log(`Staged (unzipped) folder: ${stageDir}`); console.log( - `\nManual install: extract the zip (or copy the staged folder above) so its contents land directly in\n` + + `\nManual install: extract the zip (or copy the staged folder's contents) so they land directly in\n` + ` %APPDATA%\\Vortex\\plugins\\${stageName}\\\n` + `i.e. that folder should directly contain index.js and info.json, not a nested subfolder.`, );