From 9e9ab53344094e6d0c84b356f4b89aba9dd817a7 Mon Sep 17 00:00:00 2001 From: wangtsiao Date: Tue, 20 Oct 2026 02:21:27 +0000 Subject: [PATCH 1/2] fix(tui): import the permissions flag through the package specifier The release bundle deletes tui/lib after materializing the file: packages under node_modules, so the relative value import added in 85e5533d made every shipped terminal and desktop bundle die at startup with ERR_MODULE_NOT_FOUND for slash-commands.js. Export the module via @earendil-works/pi-coding-agent/slash-commands, which resolves in the repo checkout and in the bundle, and add a guard test that rejects runtime value imports escaping into the deleted lib directory. Co-Authored-By: Claude Code --- apps/tui/lib/coding-agent/package.json | 1 + apps/tui/src/native-agent-connection.ts | 2 +- apps/tui/src/runtime-imports.test.ts | 39 +++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 apps/tui/src/runtime-imports.test.ts diff --git a/apps/tui/lib/coding-agent/package.json b/apps/tui/lib/coding-agent/package.json index a5d763d8..5e45b58c 100644 --- a/apps/tui/lib/coding-agent/package.json +++ b/apps/tui/lib/coding-agent/package.json @@ -18,6 +18,7 @@ "./auth-storage": "./src/core/auth-storage.ts", "./model-registry": "./src/core/model-registry.ts", "./settings-manager": "./src/core/settings-manager.ts", + "./slash-commands": "./src/core/slash-commands.ts", "./timings": "./src/core/timings.ts", "./messages": "./src/core/messages.ts", "./theme": "./src/modes/interactive/theme/theme.ts" diff --git a/apps/tui/src/native-agent-connection.ts b/apps/tui/src/native-agent-connection.ts index b27cf8be..5630613c 100644 --- a/apps/tui/src/native-agent-connection.ts +++ b/apps/tui/src/native-agent-connection.ts @@ -36,7 +36,7 @@ import type { } from "@earendil-works/pi-coding-agent/agent-connection"; import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; import type { ContextTreeNode } from "../lib/coding-agent/src/core/context-tree.js"; -import { PERMISSIONS_COMMAND_ENABLED } from "../lib/coding-agent/src/core/slash-commands.js"; +import { PERMISSIONS_COMMAND_ENABLED } from "@earendil-works/pi-coding-agent/slash-commands"; import type { BashResult } from "../lib/coding-agent/src/core/bash-executor.js"; import { measureAsync } from "@earendil-works/pi-coding-agent/timings"; import { StdioJsonRpc, type JsonRpcId } from "./stdio-jsonrpc.js"; diff --git a/apps/tui/src/runtime-imports.test.ts b/apps/tui/src/runtime-imports.test.ts new file mode 100644 index 00000000..bec3f827 --- /dev/null +++ b/apps/tui/src/runtime-imports.test.ts @@ -0,0 +1,39 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { test } from "bun:test"; +import assert from "node:assert/strict"; + +/** + * The release bundle deletes apps/tui's `lib/` directory after materializing + * the file: packages under node_modules (scripts/runtime/tui.ts). A runtime + * value import that escapes into `../lib/...` therefore only fails in the + * shipped product, never in the repo checkout. Keep value imports on package + * specifiers (`@earendil-works/...`); `import type` is erased at transpile and + * stays fine. + */ +const repoTui = join(import.meta.dir, ".."); + +function collectSourceFiles(directory: string): string[] { + const files: string[] = []; + for (const item of readdirSync(directory, { withFileTypes: true })) { + const path = join(directory, item.name); + if (item.isDirectory()) { + if (item.name !== "node_modules") files.push(...collectSourceFiles(path)); + } else if (item.name.endsWith(".ts") && !/\.(test|spec|d)\.ts$/.test(item.name)) { + files.push(path); + } + } + return files; +} + +test("runtime sources never value-import from the deleted lib directory", () => { + const offenders: string[] = []; + for (const path of collectSourceFiles(join(repoTui, "src"))) { + const text = readFileSync(path, "utf8"); + for (const match of text.matchAll(/import\s+(type\s+)?([^"]*?)from\s+"(\.\.[/])+lib[/]/g)) { + const isTypeOnly = Boolean(match[1]) || /^type[\s{]/.test(match[2]); + if (!isTypeOnly) offenders.push(`${path.replace(repoTui + "/", "")}: ${match[0]}`); + } + } + assert.deepEqual(offenders, []); +}); From 9aedd8fb6f5627fe45b24e9d44f159d55e0e08c6 Mon Sep 17 00:00:00 2001 From: wangtsiao Date: Tue, 20 Oct 2026 02:34:28 +0000 Subject: [PATCH 2/2] test(tui): run the runtime-imports guard under the CI node runner The guard used Bun-only imports, which cannot even load under the repo's `npm test` (node --import tsx --test) job; follow the node:test + fileURLToPath convention used by the rest of the suite. Co-Authored-By: Claude Code --- apps/tui/src/runtime-imports.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/tui/src/runtime-imports.test.ts b/apps/tui/src/runtime-imports.test.ts index bec3f827..e8f660d0 100644 --- a/apps/tui/src/runtime-imports.test.ts +++ b/apps/tui/src/runtime-imports.test.ts @@ -1,7 +1,8 @@ -import { readdirSync, readFileSync } from "node:fs"; -import { join } from "node:path"; -import { test } from "bun:test"; import assert from "node:assert/strict"; +import test from "node:test"; +import { readdirSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; /** * The release bundle deletes apps/tui's `lib/` directory after materializing @@ -11,7 +12,7 @@ import assert from "node:assert/strict"; * specifiers (`@earendil-works/...`); `import type` is erased at transpile and * stays fine. */ -const repoTui = join(import.meta.dir, ".."); +const repoTui = join(dirname(fileURLToPath(import.meta.url)), ".."); function collectSourceFiles(directory: string): string[] { const files: string[] = [];