Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sea-web-terminal-pty.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix web terminal creation in the packaged CLI.
24 changes: 19 additions & 5 deletions apps/kimi-code/scripts/native/assets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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 = {};
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/scripts/native/check-bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
48 changes: 48 additions & 0 deletions apps/kimi-code/scripts/native/native-deps.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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<string, number>} [nativeFileModes]
* — posix modes keyed by nativeFileRelatives path (e.g. 0o755)
*/

/** @type {readonly NativeDepDescriptor[]} */
Expand Down Expand Up @@ -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),
},
]);

/**
Expand All @@ -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,
}));
}
2 changes: 2 additions & 0 deletions apps/kimi-code/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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();
Expand Down
29 changes: 29 additions & 0 deletions apps/kimi-code/src/native/module-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ let installed = false;
// Path shape: native/<darwin|win32>/prebuilds/<arch>/<file>.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;
Expand Down Expand Up @@ -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);
};
}
19 changes: 19 additions & 0 deletions apps/kimi-code/src/native/node-pty-loader.ts
Original file line number Diff line number Diff line change
@@ -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<NodePtyModule> {
const cached = loadNativePackage<NodePtyModule>('node-pty');
if (cached !== null) return cached;
return nodeRequire('node-pty') as NodePtyModule;
}

export function installNodePtyLoader(): void {
(globalThis as { __kimiImportNodePty?: typeof importNodePty }).__kimiImportNodePty =
importNodePty;
}
2 changes: 1 addition & 1 deletion apps/kimi-code/src/native/smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
43 changes: 43 additions & 0 deletions apps/kimi-code/test/scripts/native/native-deps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
});
Expand All @@ -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);
});
});
18 changes: 16 additions & 2 deletions apps/kimi-code/tsdown.native.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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'),
},
Expand Down