diff --git a/.changeset/fix-kitty-backspace.md b/.changeset/fix-kitty-backspace.md
new file mode 100644
index 000000000..3e14cb3eb
--- /dev/null
+++ b/.changeset/fix-kitty-backspace.md
@@ -0,0 +1,5 @@
+---
+"@moonshot-ai/kimi-code": patch
+---
+
+Fix Backspace handling in Kitty terminals.
diff --git a/.github/actions/macos-notarize/action.yml b/.github/actions/macos-notarize/action.yml
index 9a919d810..1a8dc04f7 100644
--- a/.github/actions/macos-notarize/action.yml
+++ b/.github/actions/macos-notarize/action.yml
@@ -1,8 +1,8 @@
name: macOS notarize
description: |
- Submit a signed binary to Apple notary service via notarytool, wait for
- result, and run Gatekeeper online check (spctl). Fails the job with the
- notarytool log on rejection.
+ Submit a signed binary and sibling native helpers to Apple notary service via
+ notarytool, wait for result, and run Gatekeeper online check (spctl). Fails
+ the job with the notarytool log on rejection.
inputs:
binary-path:
@@ -47,10 +47,18 @@ runs:
# 2. Pack with ditto (--norsrc avoids ._AppleDouble files)
binary_name=$(basename "$BINARY_PATH")
+ binary_dir=$(dirname "$BINARY_PATH")
+ package_root="${RUNNER_TEMP}/${binary_name}-notarize-root"
zip_path="${RUNNER_TEMP}/${binary_name}.notarize.zip"
- ditto -c -k --norsrc --keepParent "$BINARY_PATH" "$zip_path"
+ rm -rf "$package_root"
+ mkdir -p "$package_root"
+ cp "$BINARY_PATH" "$package_root/$binary_name"
+ if [[ -d "$binary_dir/native" ]]; then
+ cp -R "$binary_dir/native" "$package_root/native"
+ fi
+ ditto -c -k --norsrc "$package_root" "$zip_path"
- echo "==> Submitting $binary_name for notarization..."
+ echo "==> Submitting $binary_name and sibling native helpers for notarization..."
# 3. Submit and capture log
log_path="${RUNNER_TEMP}/notarize-${binary_name}.log"
@@ -74,14 +82,14 @@ runs:
--key-id "$APPLE_NOTARIZATION_KEY_ID" \
--issuer "$APPLE_NOTARIZATION_ISSUER_ID" || true
fi
- rm -f "$key_path" "$zip_path"
+ rm -rf "$key_path" "$zip_path" "$package_root"
exit 1
fi
echo "==> Notarization accepted"
# 5. Cleanup
- rm -f "$key_path" "$zip_path"
+ rm -rf "$key_path" "$zip_path" "$package_root"
- name: Verify with Gatekeeper online check
shell: bash
@@ -91,5 +99,14 @@ runs:
set -euo pipefail
echo "==> codesign -dv $BINARY_PATH"
codesign -dv --verbose=2 "$BINARY_PATH"
+ binary_dir=$(dirname "$BINARY_PATH")
+ if [[ -d "$binary_dir/native" ]]; then
+ while IFS= read -r -d '' helper; do
+ echo "==> codesign --verify $helper"
+ codesign --verify --strict --verbose=2 "$helper"
+ echo "==> codesign -dv $helper"
+ codesign -dv --verbose=2 "$helper"
+ done < <(find "$binary_dir/native" -type f -name '*.node' -print0)
+ fi
echo "==> spctl -a -vvv -t install $BINARY_PATH"
spctl -a -vvv -t install "$BINARY_PATH"
diff --git a/apps/kimi-code/package.json b/apps/kimi-code/package.json
index 2c7289645..4a484b5fd 100644
--- a/apps/kimi-code/package.json
+++ b/apps/kimi-code/package.json
@@ -67,7 +67,7 @@
"postinstall": "node scripts/postinstall.mjs"
},
"dependencies": {
- "@earendil-works/pi-tui": "^0.74.0",
+ "@earendil-works/pi-tui": "^0.79.3",
"@mariozechner/clipboard": "^0.3.2",
"chalk": "^5.4.1",
"cli-highlight": "^2.1.11",
diff --git a/apps/kimi-code/scripts/native/03-inject.mjs b/apps/kimi-code/scripts/native/03-inject.mjs
index 24cd0ad32..9a5441047 100644
--- a/apps/kimi-code/scripts/native/03-inject.mjs
+++ b/apps/kimi-code/scripts/native/03-inject.mjs
@@ -1,6 +1,7 @@
import { copyFile, mkdir, stat } from 'node:fs/promises';
-import { resolve } from 'node:path';
+import { dirname, resolve } from 'node:path';
+import { resolveExecutableNativeFiles } from './assets.mjs';
import { fail, run, tryRun } from './exec.mjs';
import {
appRoot,
@@ -52,12 +53,25 @@ async function injectSeaBlob(target) {
await run(postjectPath(), args);
}
+async function copyExecutableNativeFiles(target) {
+ const files = resolveExecutableNativeFiles({ appRoot, target });
+ for (const file of files) {
+ const destination = resolve(nativeBinDir(target), file.relativePath);
+ await mkdir(dirname(destination), { recursive: true });
+ await copyFile(file.sourcePath, destination);
+ }
+ if (files.length > 0) {
+ console.log(`Copied ${files.length} native helper file(s) for ${target}`);
+ }
+}
+
export async function runInjectStep() {
const target = targetTriple();
await ensureBlobExists();
await copyNodeExecutable(target);
await removeSignatureIfNeeded(target);
await injectSeaBlob(target);
+ await copyExecutableNativeFiles(target);
}
if (import.meta.url === `file://${process.argv[1]}`) {
diff --git a/apps/kimi-code/scripts/native/04-sign.mjs b/apps/kimi-code/scripts/native/04-sign.mjs
index 2930b5e63..f3ee41c5d 100644
--- a/apps/kimi-code/scripts/native/04-sign.mjs
+++ b/apps/kimi-code/scripts/native/04-sign.mjs
@@ -1,10 +1,11 @@
import { createHash } from 'node:crypto';
import { createReadStream } from 'node:fs';
-import { writeFile } from 'node:fs/promises';
+import { stat, writeFile } from 'node:fs/promises';
import { basename, resolve } from 'node:path';
import { run } from './exec.mjs';
-import { nativeBinPath, targetTriple } from './paths.mjs';
+import { resolveExecutableFileRelatives } from './native-deps.mjs';
+import { nativeBinDir, nativeBinPath, targetTriple } from './paths.mjs';
const ENTITLEMENTS_PATH = resolve(import.meta.dirname, 'entitlements.plist');
@@ -28,6 +29,18 @@ export function buildCodesignArgs({ identity, executable, entitlementsPath, keyc
return args;
}
+export function buildCodesignNativeHelperArgs({ identity, file, keychainPath }) {
+ if (identity === '-') {
+ return ['--sign', '-', file];
+ }
+ const args = ['--sign', identity, '--options', 'runtime', '--timestamp'];
+ if (keychainPath) {
+ args.push('--keychain', keychainPath);
+ }
+ args.push('--force', file);
+ return args;
+}
+
async function sha256(path) {
return await new Promise((resolveHash, reject) => {
const hash = createHash('sha256');
@@ -48,6 +61,19 @@ export async function runSignStep({ identity = '-', keychainPath = null } = {})
const executable = nativeBinPath(target);
if (process.platform === 'darwin') {
+ for (const relativePath of resolveExecutableFileRelatives(target)) {
+ const file = resolve(nativeBinDir(target), relativePath);
+ await stat(file);
+ await run(
+ 'codesign',
+ buildCodesignNativeHelperArgs({
+ identity,
+ file,
+ keychainPath,
+ }),
+ );
+ }
+
const args = buildCodesignArgs({
identity,
executable,
diff --git a/apps/kimi-code/scripts/native/05-verify.mjs b/apps/kimi-code/scripts/native/05-verify.mjs
index 5c47da6bc..77df74812 100644
--- a/apps/kimi-code/scripts/native/05-verify.mjs
+++ b/apps/kimi-code/scripts/native/05-verify.mjs
@@ -1,5 +1,8 @@
+import { resolve } from 'node:path';
+
import { run } from './exec.mjs';
-import { nativeBinPath, targetTriple } from './paths.mjs';
+import { resolveExecutableFileRelatives } from './native-deps.mjs';
+import { nativeBinDir, nativeBinPath, targetTriple } from './paths.mjs';
export async function runVerifyStep({ requireGatekeeper = false } = {}) {
if (process.platform !== 'darwin') {
@@ -13,6 +16,14 @@ export async function runVerifyStep({ requireGatekeeper = false } = {}) {
console.log(`==> codesign -dv ${executable}`);
await run('codesign', ['-dv', '--verbose=2', executable]);
+ for (const relativePath of resolveExecutableFileRelatives(target)) {
+ const file = resolve(nativeBinDir(target), relativePath);
+ console.log(`==> codesign --verify ${file}`);
+ await run('codesign', ['--verify', '--strict', '--verbose=2', file]);
+ console.log(`==> codesign -dv ${file}`);
+ await run('codesign', ['-dv', '--verbose=2', file]);
+ }
+
if (requireGatekeeper) {
// spctl in 'install' mode simulates the Gatekeeper online check — only a
// fully notarized binary passes. Ad-hoc signed binaries fail, so this is
diff --git a/apps/kimi-code/scripts/native/assets.mjs b/apps/kimi-code/scripts/native/assets.mjs
index 7b0560b69..7454a0939 100644
--- a/apps/kimi-code/scripts/native/assets.mjs
+++ b/apps/kimi-code/scripts/native/assets.mjs
@@ -6,7 +6,7 @@ import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path
import { pathToFileURL } from 'node:url';
import { NATIVE_ASSET_MANIFEST_VERSION, buildManifestKey } from './manifest.mjs';
-import { resolveTargetDeps, SUPPORTED_TARGETS } from './native-deps.mjs';
+import { nativeDeps, resolveTargetDeps, SUPPORTED_TARGETS } from './native-deps.mjs';
export { NATIVE_ASSET_MANIFEST_VERSION };
@@ -17,9 +17,7 @@ export const NATIVE_TARGETS = Object.freeze(
SUPPORTED_TARGETS.map((t) => {
const deps = resolveTargetDeps(t);
const clipboardTarget = deps.find((d) => d.id === 'clipboard-target')?.resolvedName;
- const koffiNativeFile = deps.find((d) => d.id === 'koffi')?.nativeFileRelatives?.[0];
- const koffiTriplet = koffiNativeFile?.match(/koffi\/([^/]+)\/koffi\.node$/)?.[1] ?? null;
- return [t, { clipboardPackage: clipboardTarget, koffiTriplet }];
+ return [t, { clipboardPackage: clipboardTarget }];
}),
),
);
@@ -274,3 +272,39 @@ export async function collectNativeAssets({ appRoot, target }) {
assets,
};
}
+
+export function resolveExecutableNativeFiles({ appRoot, target }) {
+ const requireFromApp = createRequire(pathToFileURL(resolve(appRoot, 'package.json')));
+ const files = [];
+
+ for (const dep of nativeDeps) {
+ const executableFileRelatives = dep.executableFileRelatives?.(target) ?? [];
+ if (executableFileRelatives.length === 0) continue;
+
+ const packageName = dep.name(target);
+ const parentName = dep.parent
+ ? nativeDeps.find((p) => p.id === dep.parent)?.name(target) ?? null
+ : null;
+ const packageRoot = resolvePackageRootGeneric(
+ requireFromApp,
+ packageName,
+ parentName,
+ appRoot,
+ target,
+ );
+
+ for (const relativePath of executableFileRelatives) {
+ const sourcePath = resolve(packageRoot, relativePath);
+ if (!existsSync(sourcePath)) {
+ fail(`Native package ${packageName} does not contain ${relativePath} at ${packageRoot}`);
+ }
+ files.push({
+ packageName,
+ relativePath: toPosixPath(relativePath),
+ sourcePath,
+ });
+ }
+ }
+
+ return files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
+}
diff --git a/apps/kimi-code/scripts/native/check-bundle.mjs b/apps/kimi-code/scripts/native/check-bundle.mjs
index a0479f209..2c69ac9e1 100644
--- a/apps/kimi-code/scripts/native/check-bundle.mjs
+++ b/apps/kimi-code/scripts/native/check-bundle.mjs
@@ -21,12 +21,10 @@ const optionalRuntimeRequires = new Set([
'utf-8-validate',
]);
const optionalRelativeRuntimeRequires = new Set(['./crypto/build/Release/sshcrypto.node']);
-const handledNativeRuntimeRequires = new Set(['koffi']);
function isAllowedSpecifier(specifier) {
if (builtins.has(specifier) || specifier.startsWith('node:')) return true;
if (optionalRuntimeRequires.has(specifier)) return true;
- if (handledNativeRuntimeRequires.has(specifier)) return true;
return false;
}
diff --git a/apps/kimi-code/scripts/native/entitlements.plist b/apps/kimi-code/scripts/native/entitlements.plist
index d7e0e9d6b..c6a4a68fe 100644
--- a/apps/kimi-code/scripts/native/entitlements.plist
+++ b/apps/kimi-code/scripts/native/entitlements.plist
@@ -11,8 +11,8 @@
com.apple.security.cs.allow-unsigned-executable-memory
-
com.apple.security.cs.disable-library-validation
diff --git a/apps/kimi-code/scripts/native/native-deps.mjs b/apps/kimi-code/scripts/native/native-deps.mjs
index f195a3cb1..dbe627f4c 100644
--- a/apps/kimi-code/scripts/native/native-deps.mjs
+++ b/apps/kimi-code/scripts/native/native-deps.mjs
@@ -27,13 +27,11 @@ const clipboardSubpackageByTarget = Object.freeze({
'win32-x64': '@mariozechner/clipboard-win32-x64-msvc',
});
-const koffiTripletByTarget = Object.freeze({
- 'darwin-arm64': 'darwin_arm64',
- 'darwin-x64': 'darwin_x64',
- 'linux-arm64': 'linux_arm64',
- 'linux-x64': 'linux_x64',
- 'win32-arm64': 'win32_arm64',
- 'win32-x64': 'win32_x64',
+const piTuiExecutableFilesByTarget = Object.freeze({
+ 'darwin-arm64': ['native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node'],
+ 'darwin-x64': ['native/darwin/prebuilds/darwin-x64/darwin-modifiers.node'],
+ 'win32-arm64': ['native/win32/prebuilds/win32-arm64/win32-console-mode.node'],
+ 'win32-x64': ['native/win32/prebuilds/win32-x64/win32-console-mode.node'],
});
export function isSupportedTarget(target) {
@@ -52,6 +50,9 @@ export function isSupportedTarget(target) {
* @property {(target: string) => string[]} [nativeFileRelatives]
* — explicit list of .node files relative to package root
* (used by 'js-and-native-file'; native-files mode auto-scans *.node)
+ * @property {(target: string) => string[]} [executableFileRelatives]
+ * — files copied next to the native executable, preserving the
+ * package-relative path
*/
/** @type {readonly NativeDepDescriptor[]} */
@@ -71,20 +72,21 @@ export const nativeDeps = Object.freeze([
{
id: 'pi-tui',
name: () => '@earendil-works/pi-tui',
- // pi-tui is bundled into main.cjs at build time — we don't collect it as
- // a native dep, only register it so koffi can declare it as parent.
+ // pi-tui is bundled into main.cjs. Its native helpers are loaded from
+ // native/... beside the executable instead of from the native asset cache.
collect: 'virtual',
parent: null,
- },
- {
- id: 'koffi',
- name: () => 'koffi',
- collect: 'js-and-native-file',
- parent: 'pi-tui',
- nativeFileRelatives: (target) => [`build/koffi/${koffiTripletByTarget[target]}/koffi.node`],
+ executableFileRelatives: (target) => piTuiExecutableFilesByTarget[target] ?? [],
},
]);
+export function resolveExecutableFileRelatives(target) {
+ if (!isSupportedTarget(target)) {
+ throw new Error(`Unsupported native executable target: ${target}`);
+ }
+ return nativeDeps.flatMap((d) => d.executableFileRelatives?.(target) ?? []);
+}
+
/**
* Resolve which deps need collecting for a given build target, with concrete names.
*/
diff --git a/apps/kimi-code/scripts/native/package.mjs b/apps/kimi-code/scripts/native/package.mjs
index e146af0ab..6cc10f946 100644
--- a/apps/kimi-code/scripts/native/package.mjs
+++ b/apps/kimi-code/scripts/native/package.mjs
@@ -1,12 +1,18 @@
import { createHash } from 'node:crypto';
import { createReadStream, createWriteStream } from 'node:fs';
-import { mkdir, stat, writeFile } from 'node:fs/promises';
-import { basename, resolve } from 'node:path';
+import { mkdir, readdir, stat, writeFile } from 'node:fs/promises';
+import { basename, relative, resolve } from 'node:path';
import { pipeline } from 'node:stream/promises';
import { ZipFile } from 'yazl';
-import { executableName, nativeArtifactsDir, nativeBinPath, targetTriple } from './paths.mjs';
+import {
+ executableName,
+ nativeArtifactsDir,
+ nativeBinDir,
+ nativeBinPath,
+ targetTriple,
+} from './paths.mjs';
const target = targetTriple();
const execName = executableName();
@@ -33,6 +39,38 @@ async function sha256(path) {
});
}
+function toPosixPath(path) {
+ return path.split('\\').join('/');
+}
+
+async function listFiles(root) {
+ const files = [];
+
+ async function walk(dir) {
+ let entries;
+ try {
+ entries = await readdir(dir, { withFileTypes: true });
+ } catch (error) {
+ if (error?.code === 'ENOENT') return;
+ throw error;
+ }
+
+ for (const entry of entries) {
+ const path = resolve(dir, entry.name);
+ if (entry.isDirectory()) {
+ await walk(path);
+ continue;
+ }
+ if (entry.isFile()) {
+ files.push(path);
+ }
+ }
+ }
+
+ await walk(root);
+ return files.sort((a, b) => a.localeCompare(b));
+}
+
try {
await stat(sourceBinary);
} catch {
@@ -43,6 +81,9 @@ await mkdir(artifactsDir, { recursive: true });
const zip = new ZipFile();
zip.addFile(sourceBinary, execName, { mode: 0o100755 });
+for (const file of await listFiles(resolve(nativeBinDir(target), 'native'))) {
+ zip.addFile(file, toPosixPath(relative(nativeBinDir(target), file)));
+}
zip.end();
await pipeline(zip.outputStream, createWriteStream(artifactPath));
diff --git a/apps/kimi-code/src/main.ts b/apps/kimi-code/src/main.ts
index e94472590..c638c98c5 100644
--- a/apps/kimi-code/src/main.ts
+++ b/apps/kimi-code/src/main.ts
@@ -35,7 +35,6 @@ import { runUpdatePreflight } from './cli/update/preflight';
import { createKimiCodeHostIdentity, getVersion } from './cli/version';
import { CLI_SHUTDOWN_TIMEOUT_MS, CLI_UI_MODE, PROCESS_NAME } from './constant/app';
import { cleanupStaleNativeCacheForCurrent } from './native/native-assets';
-import { installNativeModuleHook } from './native/module-hook';
import { runNativeAssetSmokeIfRequested } from './native/smoke';
export async function handleMainCommand(opts: CLIOptions, version: string): Promise {
@@ -122,7 +121,6 @@ export function main(): void {
// before any client is constructed. No-op when no proxy variable is set; an
// invalid proxy URL is reported and ignored rather than aborting startup.
installGlobalProxyDispatcher();
- installNativeModuleHook();
if (runNativeAssetSmokeIfRequested()) return;
// Start the background cleanup of stale native cache. Fire-and-forget; must not block startup or throw.
diff --git a/apps/kimi-code/src/native/module-hook.ts b/apps/kimi-code/src/native/module-hook.ts
deleted file mode 100644
index bbef5d4cb..000000000
--- a/apps/kimi-code/src/native/module-hook.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { createRequire } from 'node:module';
-
-import { loadNativePackage } from './native-require';
-
-type ModuleLoad = (request: string, parent: unknown, isMain: boolean) => unknown;
-
-interface ModuleWithLoad {
- _load?: ModuleLoad;
-}
-
-const nodeRequire = createRequire(import.meta.url);
-let installed = false;
-let loadingNativePackage = false;
-
-export function installNativeModuleHook(): void {
- if (installed) return;
- installed = true;
-
- const moduleBuiltin = nodeRequire('node:module') as ModuleWithLoad;
- const originalLoad = moduleBuiltin._load;
- if (originalLoad === undefined) return;
-
- moduleBuiltin._load = function loadWithNativeAssets(
- this: unknown,
- request: string,
- parent: unknown,
- isMain: boolean,
- ): unknown {
- if (request === 'koffi' && !loadingNativePackage) {
- loadingNativePackage = true;
- try {
- const pkg = loadNativePackage('koffi');
- if (pkg !== null) return pkg;
- } finally {
- loadingNativePackage = false;
- }
- }
- return originalLoad.call(this, request, parent, isMain);
- };
-}
diff --git a/apps/kimi-code/src/native/smoke.ts b/apps/kimi-code/src/native/smoke.ts
index 56d39253f..74fba04eb 100644
--- a/apps/kimi-code/src/native/smoke.ts
+++ b/apps/kimi-code/src/native/smoke.ts
@@ -1,6 +1,10 @@
+import { existsSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+
+import { resolveExecutableFileRelatives } from '../../scripts/native/native-deps.mjs';
import { getEmbeddedNativeAssetManifest, getNativePackageRoot } from './native-assets';
-const smokePackages = ['@mariozechner/clipboard', 'koffi'];
+const smokePackages = ['@mariozechner/clipboard'];
export function runNativeAssetSmokeIfRequested(): boolean {
if (process.env['KIMI_CODE_NATIVE_ASSET_SMOKE'] !== '1') return false;
@@ -16,6 +20,15 @@ export function runNativeAssetSmokeIfRequested(): boolean {
throw new Error(`Native package is not available: ${packageName}`);
}
}
+ const executableNativeFiles = resolveExecutableFileRelatives(
+ manifest.target,
+ ) as readonly string[];
+ for (const file of executableNativeFiles) {
+ const path = join(dirname(process.execPath), file);
+ if (!existsSync(path)) {
+ throw new Error(`Native executable helper is not available: ${file}`);
+ }
+ }
process.stdout.write(`Native asset smoke passed: ${manifest.target}\n`);
process.exit(0);
} catch (error) {
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 c96b642e6..7622f44c4 100644
--- a/apps/kimi-code/test/scripts/native/native-deps.test.ts
+++ b/apps/kimi-code/test/scripts/native/native-deps.test.ts
@@ -2,10 +2,13 @@ import { describe, expect, it } from 'vitest';
import {
nativeDeps,
+ resolveExecutableFileRelatives,
resolveTargetDeps,
isSupportedTarget,
SUPPORTED_TARGETS,
} from '../../../scripts/native/native-deps.mjs';
+import { resolveExecutableNativeFiles } from '../../../scripts/native/assets.mjs';
+import { appRoot } from '../../../scripts/native/paths.mjs';
describe('SUPPORTED_TARGETS', () => {
it('contains the six published targets', () => {
@@ -41,7 +44,7 @@ describe('resolveTargetDeps', () => {
const names = deps.map((d) => d.resolvedName);
expect(names).toContain('@mariozechner/clipboard');
expect(names).toContain('@mariozechner/clipboard-darwin-arm64');
- expect(names).toContain('koffi');
+ expect(names).not.toContain('@earendil-works/pi-tui');
});
it('picks the right clipboard subpackage per target', () => {
@@ -56,15 +59,6 @@ describe('resolveTargetDeps', () => {
).toContain('@mariozechner/clipboard-win32-arm64-msvc');
});
- it('encodes koffi native file path with target triplet', () => {
- const linuxKoffi = resolveTargetDeps('linux-arm64').find((d) => d.resolvedName === 'koffi');
- expect(linuxKoffi?.nativeFileRelatives).toEqual(['build/koffi/linux_arm64/koffi.node']);
- const macKoffi = resolveTargetDeps('darwin-x64').find((d) => d.resolvedName === 'koffi');
- expect(macKoffi?.nativeFileRelatives).toEqual(['build/koffi/darwin_x64/koffi.node']);
- const winArmKoffi = resolveTargetDeps('win32-arm64').find((d) => d.resolvedName === 'koffi');
- expect(winArmKoffi?.nativeFileRelatives).toEqual(['build/koffi/win32_arm64/koffi.node']);
- });
-
it('throws on unsupported target', () => {
expect(() => resolveTargetDeps('linux-x64-musl')).toThrow(/unsupported/i);
});
@@ -82,9 +76,28 @@ describe('nativeDeps registry shape', () => {
expect(target?.parent).toBe('clipboard-host');
});
- it('has koffi (collect=js-and-native-file, parent=pi-tui)', () => {
- const koffi = nativeDeps.find((d) => d.id === 'koffi');
- expect(koffi?.collect).toBe('js-and-native-file');
- expect(koffi?.parent).toBe('pi-tui');
+ it('keeps pi-tui as an executable helper dependency', () => {
+ const piTui = nativeDeps.find((d) => d.id === 'pi-tui');
+ expect(piTui?.collect).toBe('virtual');
+ expect(piTui?.executableFileRelatives?.('darwin-arm64')).toEqual([
+ 'native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node',
+ ]);
+ expect(piTui?.executableFileRelatives?.('win32-x64')).toEqual([
+ 'native/win32/prebuilds/win32-x64/win32-console-mode.node',
+ ]);
+ expect(piTui?.executableFileRelatives?.('linux-x64')).toEqual([]);
+ expect(resolveExecutableFileRelatives('darwin-x64')).toEqual([
+ 'native/darwin/prebuilds/darwin-x64/darwin-modifiers.node',
+ ]);
+ });
+
+ it('resolves pi-tui helper files copied next to the executable', () => {
+ expect(resolveExecutableNativeFiles({ appRoot, target: 'darwin-arm64' })).toEqual([
+ expect.objectContaining({
+ packageName: '@earendil-works/pi-tui',
+ relativePath: 'native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node',
+ }),
+ ]);
+ expect(resolveExecutableNativeFiles({ appRoot, target: 'linux-x64' })).toEqual([]);
});
});
diff --git a/apps/kimi-code/test/scripts/native/release-artifacts.test.ts b/apps/kimi-code/test/scripts/native/release-artifacts.test.ts
index 791fd14e0..920c81b31 100644
--- a/apps/kimi-code/test/scripts/native/release-artifacts.test.ts
+++ b/apps/kimi-code/test/scripts/native/release-artifacts.test.ts
@@ -18,6 +18,12 @@ const artifactsDir = resolve(appRoot, 'dist-native/artifacts');
const target = 'test-zip-artifact';
const executableName = process.platform === 'win32' ? 'kimi.exe' : 'kimi';
const fakeBinary = resolve(appRoot, 'dist-native/bin', target, executableName);
+const fakeNativeFile = resolve(
+ appRoot,
+ 'dist-native/bin',
+ target,
+ 'native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node',
+);
function sha256(bytes: Buffer | string): string {
return createHash('sha256').update(bytes).digest('hex');
@@ -98,8 +104,11 @@ describe('native release artifacts', () => {
it('packages the native binary as a zip archive and checksums the archive', async () => {
const binaryContent = 'native binary payload\n';
+ const nativeContent = 'native helper payload\n';
mkdirSync(resolve(appRoot, 'dist-native/bin', target), { recursive: true });
writeFileSync(fakeBinary, binaryContent, { mode: 0o755 });
+ mkdirSync(resolve(fakeNativeFile, '..'), { recursive: true });
+ writeFileSync(fakeNativeFile, nativeContent);
await execFileAsync(process.execPath, [packageScript], {
cwd: appRoot,
@@ -110,8 +119,17 @@ describe('native release artifacts', () => {
const checksumPath = `${archivePath}.sha256`;
expect(existsSync(archivePath)).toBe(true);
expect(existsSync(checksumPath)).toBe(true);
- expect(zipEntryNames(archivePath)).toEqual([executableName]);
+ expect(zipEntryNames(archivePath)).toEqual([
+ executableName,
+ 'native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node',
+ ]);
expect(readZipEntry(archivePath, executableName).toString('utf-8')).toBe(binaryContent);
+ expect(
+ readZipEntry(
+ archivePath,
+ 'native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node',
+ ).toString('utf-8'),
+ ).toBe(nativeContent);
expect(readFileSync(checksumPath, 'utf-8')).toBe(
`${sha256(readFileSync(archivePath))} kimi-code-${target}.zip\n`,
);
diff --git a/apps/kimi-code/test/scripts/native/sign-args.test.ts b/apps/kimi-code/test/scripts/native/sign-args.test.ts
index 803f1820a..a7036b0a7 100644
--- a/apps/kimi-code/test/scripts/native/sign-args.test.ts
+++ b/apps/kimi-code/test/scripts/native/sign-args.test.ts
@@ -1,6 +1,9 @@
import { describe, expect, it } from 'vitest';
-import { buildCodesignArgs } from '../../../scripts/native/04-sign.mjs';
+import {
+ buildCodesignArgs,
+ buildCodesignNativeHelperArgs,
+} from '../../../scripts/native/04-sign.mjs';
describe('buildCodesignArgs', () => {
it('returns ad-hoc args for identity "-"', () => {
@@ -46,3 +49,35 @@ describe('buildCodesignArgs', () => {
expect(args).not.toContain('--keychain');
});
});
+
+describe('buildCodesignNativeHelperArgs', () => {
+ it('returns ad-hoc args for identity "-"', () => {
+ expect(
+ buildCodesignNativeHelperArgs({
+ identity: '-',
+ file: '/path/native/helper.node',
+ keychainPath: null,
+ }),
+ ).toEqual(['--sign', '-', '/path/native/helper.node']);
+ });
+
+ it('returns hardened-runtime args without app entitlements for Developer ID identity', () => {
+ expect(
+ buildCodesignNativeHelperArgs({
+ identity: 'Developer ID Application: Moonshot AI (ABCD1234)',
+ file: '/path/native/helper.node',
+ keychainPath: '/tmp/sign.keychain-db',
+ }),
+ ).toEqual([
+ '--sign',
+ 'Developer ID Application: Moonshot AI (ABCD1234)',
+ '--options',
+ 'runtime',
+ '--timestamp',
+ '--keychain',
+ '/tmp/sign.keychain-db',
+ '--force',
+ '/path/native/helper.node',
+ ]);
+ });
+});
diff --git a/flake.nix b/flake.nix
index 45ae4dcff..ce3c5f1a0 100644
--- a/flake.nix
+++ b/flake.nix
@@ -190,6 +190,10 @@
"apps/kimi-code/dist-native/bin/${nativeTarget}/kimi" \
"$out/bin/kimi"
+ if [ -d "apps/kimi-code/dist-native/bin/${nativeTarget}/native" ]; then
+ cp -R "apps/kimi-code/dist-native/bin/${nativeTarget}/native" "$out/bin/"
+ fi
+
runHook postInstall
'';
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 13fa0c2a0..c202c3f7b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -67,8 +67,8 @@ importers:
apps/kimi-code:
dependencies:
'@earendil-works/pi-tui':
- specifier: ^0.74.0
- version: 0.74.0
+ specifier: ^0.79.3
+ version: 0.79.3
'@mariozechner/clipboard':
specifier: ^0.3.2
version: 0.3.2
@@ -774,9 +774,9 @@ packages:
search-insights:
optional: true
- '@earendil-works/pi-tui@0.74.0':
- resolution: {integrity: sha512-1aIfXZp7D/z+1VlZX8BZcs6pgO8rjmil7kwyhctNDsWvce3Yfl8GVgu4eq+I0Mjhr8Cj+ipBiv9CLIzdoyCOIQ==}
- engines: {node: '>=20.0.0'}
+ '@earendil-works/pi-tui@0.79.3':
+ resolution: {integrity: sha512-cpmkEM1aEuGUx6YZM36VlzpulwLzqD5T2cUEkGHndDTNGEbnn5sj/9SYm+QBfKjvZsWoHfZuFBnu4+hh96/FbA==}
+ engines: {node: '>=22.19.0'}
'@emnapi/core@1.10.0':
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
@@ -2379,9 +2379,6 @@ packages:
'@types/mdurl@2.0.0':
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
- '@types/mime-types@2.1.4':
- resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==}
-
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
@@ -3497,6 +3494,10 @@ packages:
resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==}
engines: {node: '>=18'}
+ get-east-asian-width@1.6.0:
+ resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==}
+ engines: {node: '>=18'}
+
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
@@ -3902,9 +3903,6 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
- koffi@2.16.0:
- resolution: {integrity: sha512-h/2NJueOKWd0YYycEOWDspomizgNfuOKf/V7ZE2fytvuRtHoY9Tb+y4x6GJ6pFqaVndWn9dLK+sCI14eWtu5rA==}
-
layout-base@1.0.2:
resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==}
@@ -6055,15 +6053,10 @@ snapshots:
transitivePeerDependencies:
- '@algolia/client-search'
- '@earendil-works/pi-tui@0.74.0':
+ '@earendil-works/pi-tui@0.79.3':
dependencies:
- '@types/mime-types': 2.1.4
- chalk: 5.6.2
- get-east-asian-width: 1.5.0
+ get-east-asian-width: 1.6.0
marked: 15.0.12
- mime-types: 3.0.2
- optionalDependencies:
- koffi: 2.16.0
'@emnapi/core@1.10.0':
dependencies:
@@ -7231,8 +7224,6 @@ snapshots:
'@types/mdurl@2.0.0': {}
- '@types/mime-types@2.1.4': {}
-
'@types/ms@2.1.0': {}
'@types/node@12.20.55': {}
@@ -8504,6 +8495,8 @@ snapshots:
get-east-asian-width@1.5.0: {}
+ get-east-asian-width@1.6.0: {}
+
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -8912,9 +8905,6 @@ snapshots:
kind-of@6.0.3: {}
- koffi@2.16.0:
- optional: true
-
layout-base@1.0.2: {}
layout-base@2.0.1: {}