From f7963bb1d478bd356bdc8ace6252b10b376f18fb Mon Sep 17 00:00:00 2001 From: Angelbyte Date: Wed, 30 Sep 2026 10:29:25 -0600 Subject: [PATCH] fix(vim-editor-adapter): fail closed when host-provided pi-tui metadata is unresolvable Closes #1592 --- lib/vim-editor-adapter.ts | 15 ++++++++++++--- tests/vim-editor-adapter.test.ts | 13 ++++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/vim-editor-adapter.ts b/lib/vim-editor-adapter.ts index 6d99f0ccd..9d654b654 100644 --- a/lib/vim-editor-adapter.ts +++ b/lib/vim-editor-adapter.ts @@ -41,9 +41,18 @@ interface PrivateEditor { } const SUPPORTED_VERSIONS = new Set(["0.99.1"]); -const importedTuiMetadata: unknown = createRequire(import.meta.url)("@earendil-works/pi-tui/package.json"); -const IMPORTED_TUI_VERSION = typeof importedTuiMetadata === "object" && importedTuiMetadata !== null && - "version" in importedTuiMetadata ? importedTuiMetadata.version : undefined; +// pi-tui is an optional peer: an installed extension may resolve it only +// through the host's import alias, which createRequire does not see. Missing +// metadata leaves the default Editor path unauthorized instead of failing load. +export function readTuiVersion(load: (id: string) => unknown = createRequire(import.meta.url)): unknown { + try { + const metadata = load("@earendil-works/pi-tui/package.json"); + return typeof metadata === "object" && metadata !== null && "version" in metadata ? metadata.version : undefined; + } catch { + return undefined; + } +} +const IMPORTED_TUI_VERSION = readTuiVersion(); // The caller supplies the Editor constructor from the same Pi/TUI package pair // as the version metadata. An arbitrary object with matching fields is not an editor. diff --git a/tests/vim-editor-adapter.test.ts b/tests/vim-editor-adapter.test.ts index ec188b46e..90be7230c 100644 --- a/tests/vim-editor-adapter.test.ts +++ b/tests/vim-editor-adapter.test.ts @@ -5,7 +5,7 @@ import { createRequire } from "node:module"; import { fileURLToPath, pathToFileURL } from "node:url"; import { dirname, resolve } from "node:path"; import { CURSOR_MARKER, Editor, visibleWidth } from "@earendil-works/pi-tui"; -import { createVimEditorAdapter } from "../lib/vim-editor-adapter.ts"; +import { createVimEditorAdapter, readTuiVersion } from "../lib/vim-editor-adapter.ts"; import { resolveVimRuntime, VIM_AGENT_INDEX_PATTERN, VIM_CLI_ENTRY_PATTERN } from "../extensions/gentle-shell.ts"; import { VimOperatorEngine } from "../lib/vim-operator-engine.ts"; import { VimVisualEngine } from "../lib/vim-visual-engine.ts"; @@ -177,6 +177,17 @@ test("runtime identity resolves only the matching installed coding-agent/TUI pai assert.deepEqual(resolveVimRuntime("/nonexistent/cli.js", runtimeAgent.CustomEditor), { version: "0.99.1", editorClass: Editor }); }); +test("TUI metadata lookup fails closed when pi-tui is only host-provided", () => { + // Optional peer: installed extensions may not have pi-tui on disk, and the + // host alias does not cover createRequire. Loading must not throw. + const missing = () => { throw Object.assign(new Error("Cannot find module"), { code: "MODULE_NOT_FOUND" }); }; + assert.equal(readTuiVersion(missing), undefined); + assert.equal(readTuiVersion(() => ({ version: "0.99.1" })), "0.99.1"); + assert.equal(readTuiVersion(() => null), undefined); + // Without trusted metadata, the default Editor path is not authorized. + assert.throws(() => createVimEditorAdapter(editor(), "unknown"), /Unsupported Pi editor/); +}); + function assertInstalledPiPairBehavior(version: "0.99.1", EditorClass: typeof Editor, CustomClass: { prototype: unknown } | undefined): void { assert.equal(CustomClass ? Object.getPrototypeOf(CustomClass.prototype) : EditorClass.prototype, EditorClass.prototype); const e = new EditorClass({ terminal: { rows: 6, columns: 22 }, requestRender() {} } as never, { borderColor: (s: string) => s } as never);