diff --git a/.changeset/sea-web-terminal-pty.md b/.changeset/sea-web-terminal-pty.md new file mode 100644 index 00000000000..eb027d2292a --- /dev/null +++ b/.changeset/sea-web-terminal-pty.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix web terminal creation in the packaged CLI. diff --git a/apps/kimi-code/scripts/native/assets.mjs b/apps/kimi-code/scripts/native/assets.mjs index 41fb600e35c..6ee343abee7 100644 --- a/apps/kimi-code/scripts/native/assets.mjs +++ b/apps/kimi-code/scripts/native/assets.mjs @@ -2,7 +2,7 @@ import { createHash } from 'node:crypto'; import { existsSync, realpathSync } from 'node:fs'; import { readdir, readFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; -import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path'; +import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { @@ -182,10 +182,19 @@ async function collectPackageFiles({ for (const nativeFileRelative of nativeFileRelatives) { const nativeFile = resolve(packageRoot, nativeFileRelative); - if (!existsSync(nativeFile)) { - fail(`Native package ${packageName} does not contain ${nativeFileRelative} at ${packageRoot}`); + if (existsSync(nativeFile)) { + selected.add(nativeFile); + continue; + } + const compiledFallback = resolve(packageRoot, 'build/Release', basename(nativeFileRelative)); + if (existsSync(compiledFallback)) { + selected.add(compiledFallback); + continue; + } + if (nativeFileRelative.endsWith('/spawn-helper')) { + continue; } - selected.add(nativeFile); + fail(`Native package ${packageName} does not contain ${nativeFileRelative} at ${packageRoot}`); } if (includeNativeFiles) { @@ -204,7 +213,7 @@ async function collectPackageFiles({ return sorted; } -async function packageManifestEntries({ packageName, packageRoot, files, target }) { +async function packageManifestEntries({ packageName, packageRoot, files, target, fileModes = {} }) { const root = `node_modules/${packageName}`; const entries = []; const assets = {}; @@ -214,10 +223,14 @@ async function packageManifestEntries({ packageName, packageRoot, files, target const packageRelativePath = toPosixPath(relative(packageRoot, file)); const relativePath = `${root}/${packageRelativePath}`; const assetKey = `native/${target}/${relativePath}`; + const mode = + fileModes[packageRelativePath] ?? + (packageRelativePath.endsWith('/spawn-helper') ? 0o755 : undefined); entries.push({ assetKey, relativePath, sha256: sha256(sourceBytes), + mode, }); assets[assetKey] = file; } @@ -268,6 +281,7 @@ export async function collectNativeAssets({ appRoot, target }) { packageRoot, files, target, + fileModes: dep.nativeFileModes ?? {}, }); manifestPackages.push(result.packageManifest); Object.assign(assets, result.assets); diff --git a/apps/kimi-code/scripts/native/check-bundle.mjs b/apps/kimi-code/scripts/native/check-bundle.mjs index 8b3519db73a..169940ee016 100644 --- a/apps/kimi-code/scripts/native/check-bundle.mjs +++ b/apps/kimi-code/scripts/native/check-bundle.mjs @@ -16,6 +16,7 @@ const optionalRuntimeRequires = new Set([ 'canvas', 'chokidar', 'cpu-features', + 'node-pty', 'fast-json-stringify/lib/serializer', 'fast-json-stringify/lib/validator', 'utf-8-validate', diff --git a/apps/kimi-code/scripts/native/native-deps.mjs b/apps/kimi-code/scripts/native/native-deps.mjs index 8e26d9229dd..7bcf7e2d17e 100644 --- a/apps/kimi-code/scripts/native/native-deps.mjs +++ b/apps/kimi-code/scripts/native/native-deps.mjs @@ -39,6 +39,43 @@ const piTuiNativeFileByTarget = Object.freeze({ 'win32-x64': ['native/win32/prebuilds/win32-x64/win32-console-mode.node'], }); +const nodePtyWinSidecars = Object.freeze([ + 'pty.node', + 'conpty.node', + 'conpty_console_list.node', + 'winpty.dll', + 'winpty-agent.exe', + 'conpty/conpty.dll', + 'conpty/OpenConsole.exe', +]); + +const nodePtyUnixFiles = Object.freeze(['pty.node', 'spawn-helper']); + +const nodePtyNativeFileByTarget = Object.freeze({ + 'darwin-arm64': nodePtyUnixFiles.map((name) => `prebuilds/darwin-arm64/${name}`), + 'darwin-x64': nodePtyUnixFiles.map((name) => `prebuilds/darwin-x64/${name}`), + 'linux-arm64': nodePtyUnixFiles.map((name) => `prebuilds/linux-arm64/${name}`), + 'linux-x64': nodePtyUnixFiles.map((name) => `prebuilds/linux-x64/${name}`), + 'win32-arm64': [ + ...nodePtyWinSidecars.map((name) => `prebuilds/win32-arm64/${name}`), + 'lib/worker/conoutSocketWorker.js', + ], + 'win32-x64': [ + ...nodePtyWinSidecars.map((name) => `prebuilds/win32-x64/${name}`), + 'lib/worker/conoutSocketWorker.js', + ], +}); + +function nodePtyFileModes(target) { + const modes = {}; + for (const relative of nodePtyNativeFileByTarget[target] ?? []) { + if (relative.endsWith('/spawn-helper') || relative.endsWith('.exe')) { + modes[relative] = 0o755; + } + } + return modes; +} + export function isSupportedTarget(target) { return SUPPORTED_TARGETS.includes(target); } @@ -57,6 +94,8 @@ export function isSupportedTarget(target) { * (used by 'js-and-native-file' and 'native-file-only'; * native-files mode auto-scans *.node). 'native-file-only' collects * package.json + these .node files but skips the package entry JS. + * @property {(target: string) => Record} [nativeFileModes] + * — posix modes keyed by nativeFileRelatives path (e.g. 0o755) */ /** @type {readonly NativeDepDescriptor[]} */ @@ -84,6 +123,14 @@ export const nativeDeps = Object.freeze([ parent: null, nativeFileRelatives: (target) => piTuiNativeFileByTarget[target] ?? [], }, + { + id: 'node-pty', + name: () => 'node-pty', + collect: 'js-and-native-file', + parent: null, + nativeFileRelatives: (target) => nodePtyNativeFileByTarget[target] ?? [], + nativeFileModes: (target) => nodePtyFileModes(target), + }, ]); /** @@ -99,6 +146,7 @@ export function resolveTargetDeps(target) { ...d, resolvedName: d.name(target), nativeFileRelatives: d.nativeFileRelatives?.(target) ?? [], + nativeFileModes: d.nativeFileModes?.(target) ?? {}, parentName: d.parent ? nativeDeps.find((p) => p.id === d.parent)?.name(target) ?? null : null, })); } diff --git a/apps/kimi-code/src/main.ts b/apps/kimi-code/src/main.ts index 37ec0a88272..db66aa843e8 100644 --- a/apps/kimi-code/src/main.ts +++ b/apps/kimi-code/src/main.ts @@ -43,6 +43,7 @@ import { cleanupStaleNativeCacheForCurrent } from './native/native-assets'; import { installMinidbTextBuildWorker } from './native/minidb-worker'; import { installKapSearchWorker } from './native/search-worker'; import { installNativeModuleHook } from './native/module-hook'; +import { installNodePtyLoader } from './native/node-pty-loader'; import { runNativeAssetSmokeIfRequested } from './native/smoke'; /** @@ -170,6 +171,7 @@ function bootstrap(): void { // invalid proxy URL is reported and ignored rather than aborting startup. installGlobalProxyDispatcher(); installNativeModuleHook(); + installNodePtyLoader(); // Best-effort SEA worker installation. Diagnostics are trace-only and avoid // exposing the user's cache path; failure keeps MiniDb's bounded inline mode. const workerInstall = installMinidbTextBuildWorker(); diff --git a/apps/kimi-code/src/native/module-hook.ts b/apps/kimi-code/src/native/module-hook.ts index bc8a1a67b5b..8369feee637 100644 --- a/apps/kimi-code/src/native/module-hook.ts +++ b/apps/kimi-code/src/native/module-hook.ts @@ -22,6 +22,8 @@ let installed = false; // Path shape: native//prebuilds//.node — note the // two path segments after "prebuilds", so ".+" (not "[^/]+") is required. const PI_TUI_NATIVE_PATTERN = /native[\\/](?:win32|darwin)[\\/]prebuilds[\\/].+\.node$/; +const NODE_PTY_PREBUILD_PATTERN = + /(?:^|[\\/])prebuilds[\\/](?:darwin|linux|win32)-[^\\/]+[\\/](?:pty|conpty|conpty_console_list)\.node$/; export function installNativeModuleHook(): void { if (installed) return; @@ -51,6 +53,33 @@ export function installNativeModuleHook(): void { } } } + + if (typeof request === 'string' && (request === 'node-pty' || request.startsWith('node-pty/'))) { + const pkgRoot = getNativePackageRoot('node-pty'); + if (pkgRoot !== null) { + const redirected = + request === 'node-pty' + ? join(pkgRoot, 'lib', 'index.js') + : join(pkgRoot, request.slice('node-pty/'.length)); + return originalLoad.call(this, redirected, parent, isMain); + } + } + + if ( + typeof request === 'string' && + NODE_PTY_PREBUILD_PATTERN.test(request) && + !existsSync(request) + ) { + const pkgRoot = getNativePackageRoot('node-pty'); + if (pkgRoot !== null) { + const match = request.match(NODE_PTY_PREBUILD_PATTERN); + if (match !== null) { + const redirected = join(pkgRoot, match[0].replace(/^[\\/]/, '')); + return originalLoad.call(this, redirected, parent, isMain); + } + } + } + return originalLoad.call(this, request, parent, isMain); }; } diff --git a/apps/kimi-code/src/native/node-pty-loader.ts b/apps/kimi-code/src/native/node-pty-loader.ts new file mode 100644 index 00000000000..650388bc132 --- /dev/null +++ b/apps/kimi-code/src/native/node-pty-loader.ts @@ -0,0 +1,19 @@ +import { createRequire } from 'node:module'; +import type * as NodePty from 'node-pty'; + +import { loadNativePackage } from './native-require'; + +type NodePtyModule = typeof NodePty; + +const nodeRequire = createRequire(import.meta.url); + +export async function importNodePty(): Promise { + const cached = loadNativePackage('node-pty'); + if (cached !== null) return cached; + return nodeRequire('node-pty') as NodePtyModule; +} + +export function installNodePtyLoader(): void { + (globalThis as { __kimiImportNodePty?: typeof importNodePty }).__kimiImportNodePty = + importNodePty; +} diff --git a/apps/kimi-code/src/native/smoke.ts b/apps/kimi-code/src/native/smoke.ts index a7f80957ce1..58c80ebb723 100644 --- a/apps/kimi-code/src/native/smoke.ts +++ b/apps/kimi-code/src/native/smoke.ts @@ -13,7 +13,7 @@ import { getNativePackageRoot, } from './native-assets'; -const smokePackages = ['@mariozechner/clipboard', '@moonshot-ai/pi-tui']; +const smokePackages = ['@mariozechner/clipboard', '@moonshot-ai/pi-tui', 'node-pty']; function smokePiTuiNativeLoad(): void { const platform = process.platform; diff --git a/apps/kimi-code/test/scripts/native/native-deps.test.ts b/apps/kimi-code/test/scripts/native/native-deps.test.ts index 980f1c7714f..4d0640f6ee6 100644 --- a/apps/kimi-code/test/scripts/native/native-deps.test.ts +++ b/apps/kimi-code/test/scripts/native/native-deps.test.ts @@ -42,6 +42,7 @@ describe('resolveTargetDeps', () => { expect(names).toContain('@mariozechner/clipboard'); expect(names).toContain('@mariozechner/clipboard-darwin-arm64'); expect(names).toContain('@moonshot-ai/pi-tui'); + expect(names).toContain('node-pty'); }); it('picks the right clipboard subpackage per target', () => { @@ -75,6 +76,42 @@ describe('resolveTargetDeps', () => { ]); }); + it('encodes node-pty native files and executable modes per target', () => { + const linuxPty = resolveTargetDeps('linux-x64').find((d) => d.resolvedName === 'node-pty'); + expect(linuxPty?.nativeFileRelatives).toEqual([ + 'prebuilds/linux-x64/pty.node', + 'prebuilds/linux-x64/spawn-helper', + ]); + expect(linuxPty?.nativeFileModes).toEqual({ + 'prebuilds/linux-x64/spawn-helper': 0o755, + }); + const linuxArmPty = resolveTargetDeps('linux-arm64').find((d) => d.resolvedName === 'node-pty'); + expect(linuxArmPty?.nativeFileRelatives).toEqual([ + 'prebuilds/linux-arm64/pty.node', + 'prebuilds/linux-arm64/spawn-helper', + ]); + expect(linuxArmPty?.nativeFileModes).toEqual({ + 'prebuilds/linux-arm64/spawn-helper': 0o755, + }); + + const macPty = resolveTargetDeps('darwin-arm64').find((d) => d.resolvedName === 'node-pty'); + expect(macPty?.nativeFileRelatives).toEqual([ + 'prebuilds/darwin-arm64/pty.node', + 'prebuilds/darwin-arm64/spawn-helper', + ]); + expect(macPty?.nativeFileModes).toEqual({ + 'prebuilds/darwin-arm64/spawn-helper': 0o755, + }); + + const winPty = resolveTargetDeps('win32-x64').find((d) => d.resolvedName === 'node-pty'); + expect(winPty?.nativeFileRelatives).toContain('prebuilds/win32-x64/conpty.node'); + expect(winPty?.nativeFileRelatives).toContain('lib/worker/conoutSocketWorker.js'); + expect(winPty?.nativeFileModes).toEqual({ + 'prebuilds/win32-x64/winpty-agent.exe': 0o755, + 'prebuilds/win32-x64/conpty/OpenConsole.exe': 0o755, + }); + }); + it('throws on unsupported target', () => { expect(() => resolveTargetDeps('linux-x64-musl')).toThrow(/unsupported/i); }); @@ -97,4 +134,10 @@ describe('nativeDeps registry shape', () => { expect(piTui?.collect).toBe('native-file-only'); expect(piTui?.parent).toBe(null); }); + + it('has node-pty (collect=js-and-native-file, no parent)', () => { + const pty = nativeDeps.find((d) => d.id === 'node-pty'); + expect(pty?.collect).toBe('js-and-native-file'); + expect(pty?.parent).toBe(null); + }); }); diff --git a/apps/kimi-code/tsdown.native.config.ts b/apps/kimi-code/tsdown.native.config.ts index c1008cb6165..bcc2bd2b8c3 100644 --- a/apps/kimi-code/tsdown.native.config.ts +++ b/apps/kimi-code/tsdown.native.config.ts @@ -16,7 +16,21 @@ const builtins = new Set([ ...builtinModules, ...builtinModules.map((name) => `node:${name}`), ]); -const optionalNativeDependencies = new Set(['cpu-features']); +const optionalNativeDependencies = new Set(['cpu-features', 'node-pty']); + +function nodePtyImportPlugin() { + return { + name: 'kimi-node-pty-import', + transform(code: string) { + const next = code.replaceAll( + /await\s+import\(\s*['"]node-pty['"]\s*\)/g, + 'await globalThis.__kimiImportNodePty()', + ); + if (next === code) return null; + return next; + }, + }; +} function shouldAlwaysBundle(id: string): boolean { if (builtins.has(id) || id.startsWith('node:')) return false; @@ -42,7 +56,7 @@ export default defineConfig({ platform: 'node', target: 'node24', banner: { js: '#!/usr/bin/env node' }, - plugins: [rawTextPlugin()], + plugins: [rawTextPlugin(), nodePtyImportPlugin()], alias: { '@': resolve(appRoot, 'src'), },