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..941ad97 --- /dev/null +++ b/vortex-extension/scripts/package.mjs @@ -0,0 +1,120 @@ +#!/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, 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'; +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); +} +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 }); + +// 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`); + +// 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' }, + ); + } 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'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.`, +);