diff --git a/packages/core/package.json b/packages/core/package.json index 853e4c12..1682d955 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -6,8 +6,7 @@ "types": "types/index.d.ts", "files": [ "dist", - "types", - "scripts" + "types" ], "exports": { ".": { @@ -45,7 +44,6 @@ "build:client:watch": " vite build --config ./vite.client.config.ts --watch", "clear": "rimraf ./dist && rimraf ./types", "build": "pnpm clear && tsc && pnpm build:server && pnpm build:client", - "postinstall": "node ./scripts/verify-terminal-runtime.js", "pub": "pnpm publish", "pub:beta": "pnpm publish --tag beta" }, diff --git a/packages/core/scripts/verify-terminal-runtime.js b/packages/core/scripts/verify-terminal-runtime.js deleted file mode 100644 index 5afe8327..00000000 --- a/packages/core/scripts/verify-terminal-runtime.js +++ /dev/null @@ -1,333 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); - -const LOG_PREFIX = '[code-inspector-plugin]'; -const PROBE_TIMEOUT_MS = 2000; - -function logMessage(logger, level, message) { - const target = - (logger && typeof logger[level] === 'function' && logger[level]) || - (logger && typeof logger.log === 'function' && logger.log) || - console.log; - target.call(logger, `${LOG_PREFIX} ${message}`); -} - -function resolveNodePtyPackageRoot(resolveFromDir) { - try { - const packageJsonPath = require.resolve('node-pty/package.json', { - paths: [resolveFromDir || __dirname], - }); - return path.dirname(packageJsonPath); - } catch { - return null; - } -} - -function getSpawnHelperCandidates(nodePtyRoot, platform, arch) { - if (!nodePtyRoot || platform === 'win32') { - return []; - } - - return [ - path.join(nodePtyRoot, 'build', 'Release', 'spawn-helper'), - path.join(nodePtyRoot, 'build', 'Debug', 'spawn-helper'), - path.join(nodePtyRoot, 'prebuilds', `${platform}-${arch}`, 'spawn-helper'), - ]; -} - -function isExecutableFile(filePath, platform) { - try { - const stat = fs.statSync(filePath); - if (!stat.isFile()) return false; - if (platform === 'win32') return true; - fs.accessSync(filePath, fs.constants.X_OK); - return true; - } catch { - return false; - } -} - -function ensureHelperExecutable(filePath, platform) { - if (!filePath || !fs.existsSync(filePath)) { - return { exists: false, executable: false, fixed: false }; - } - - if (platform === 'win32') { - return { exists: true, executable: true, fixed: false }; - } - - if (isExecutableFile(filePath, platform)) { - return { exists: true, executable: true, fixed: false }; - } - - try { - const stat = fs.statSync(filePath); - fs.chmodSync(filePath, stat.mode | 0o111); - return { - exists: true, - executable: isExecutableFile(filePath, platform), - fixed: true, - }; - } catch (error) { - return { - exists: true, - executable: false, - fixed: false, - error: error && error.message ? error.message : String(error), - }; - } -} - -function normalizeEnv(env) { - const normalized = {}; - const source = env || process.env; - - for (const [key, value] of Object.entries(source)) { - if (typeof value === 'string') { - normalized[key] = value; - } - } - - return normalized; -} - -function resolveProbeCwd(cwd) { - try { - if (cwd && fs.statSync(cwd).isDirectory()) { - return cwd; - } - } catch { - // ignore invalid cwd - } - return process.cwd(); -} - -function getProbeCommand(platform) { - if (platform === 'win32') { - return { - command: 'cmd.exe', - args: ['/d', '/s', '/c', 'exit 0'], - }; - } - - return { - command: '/bin/sh', - args: ['-lc', 'exit 0'], - }; -} - -async function probeNodePtySpawn(nodePty, options) { - const platform = options.platform || process.platform; - const probe = getProbeCommand(platform); - - if (!nodePty || typeof nodePty.spawn !== 'function') { - return { - ok: false, - reason: 'node-pty module does not expose a spawn function.', - }; - } - - try { - const child = nodePty.spawn(probe.command, probe.args, { - name: 'xterm-256color', - cols: 80, - rows: 24, - cwd: resolveProbeCwd(options.cwd), - env: normalizeEnv(options.env), - }); - - return await new Promise((resolve) => { - let settled = false; - const finish = (result) => { - if (settled) return; - settled = true; - clearTimeout(timeout); - resolve(result); - }; - - const timeout = setTimeout(() => { - try { - if (child && typeof child.kill === 'function') { - child.kill(); - } - } catch { - // ignore cleanup errors - } - - finish({ - ok: false, - reason: `PTY probe timed out after ${PROBE_TIMEOUT_MS}ms.`, - }); - }, PROBE_TIMEOUT_MS); - - if (!child || typeof child.onExit !== 'function') { - finish({ - ok: false, - reason: 'node-pty spawn result does not expose onExit.', - }); - return; - } - - child.onExit(() => { - finish({ ok: true }); - }); - }); - } catch (error) { - return { - ok: false, - reason: error && error.message ? error.message : String(error), - }; - } -} - -async function runTerminalRuntimeCheck(options) { - const settings = options || {}; - const logger = settings.logger || console; - const platform = settings.platform || process.platform; - const arch = settings.arch || process.arch; - const resolveFromDir = settings.resolveFromDir || __dirname; - - // node-pty can block synchronously while connecting ConPTY pipes on Windows, - // which prevents the probe timeout from starting and stalls package installs. - // See https://github.com/microsoft/node-pty/issues/763. - if (platform === 'win32') { - return { - ok: true, - skipped: true, - fixedPaths: [], - helperPaths: [], - reason: 'Terminal runtime verification is deferred on Windows.', - }; - } - - const nodePtyRoot = Object.prototype.hasOwnProperty.call( - settings, - 'nodePtyRoot', - ) - ? settings.nodePtyRoot - : resolveNodePtyPackageRoot(resolveFromDir); - - if (!nodePtyRoot) { - logMessage( - logger, - 'log', - 'Skipping terminal runtime verification because node-pty is not installed.', - ); - return { - ok: true, - skipped: true, - fixedPaths: [], - helperPaths: [], - reason: 'node-pty is not installed.', - }; - } - - const helperPaths = getSpawnHelperCandidates(nodePtyRoot, platform, arch); - const fixedPaths = []; - - for (const helperPath of helperPaths) { - const result = ensureHelperExecutable(helperPath, platform); - if (!result.exists) { - continue; - } - if (result.fixed && result.executable) { - fixedPaths.push(helperPath); - logMessage( - logger, - 'log', - `Restored execute permission on node-pty helper: ${helperPath}`, - ); - continue; - } - if (!result.executable) { - logMessage( - logger, - 'warn', - `node-pty helper is not executable: ${helperPath}${result.error ? ` (${result.error})` : ''}`, - ); - } - } - - let nodePty = settings.nodePty; - if (!nodePty) { - try { - const modulePath = require.resolve('node-pty', { - paths: [resolveFromDir], - }); - nodePty = require(modulePath); - } catch (error) { - logMessage( - logger, - 'warn', - `Skipping terminal PTY probe because node-pty could not be loaded: ${error && error.message ? error.message : String(error)}`, - ); - return { - ok: true, - skipped: true, - fixedPaths, - helperPaths, - reason: 'node-pty could not be loaded.', - }; - } - } - - const probe = await probeNodePtySpawn(nodePty, { - cwd: settings.cwd || resolveFromDir, - env: settings.env, - platform, - }); - - if (probe.ok) { - logMessage( - logger, - 'log', - fixedPaths.length > 0 - ? `Terminal runtime verification passed after repairing ${fixedPaths.length} helper file(s).` - : 'Terminal runtime verification passed.', - ); - } else { - logMessage( - logger, - 'warn', - `Terminal runtime verification failed: ${probe.reason}. Terminal mode will be disabled at runtime.`, - ); - } - - return { - ok: probe.ok, - skipped: false, - fixedPaths, - helperPaths, - reason: probe.reason, - }; -} - -async function main() { - try { - await runTerminalRuntimeCheck(); - } catch (error) { - logMessage( - console, - 'warn', - `Terminal runtime verification crashed: ${error && error.message ? error.message : String(error)}`, - ); - } -} - -if (require.main === module) { - void main(); -} - -module.exports = { - PROBE_TIMEOUT_MS, - ensureHelperExecutable, - getProbeCommand, - getSpawnHelperCandidates, - isExecutableFile, - normalizeEnv, - probeNodePtySpawn, - resolveNodePtyPackageRoot, - runTerminalRuntimeCheck, -}; diff --git a/test/core/scripts/verify-terminal-runtime.test.ts b/test/core/scripts/verify-terminal-runtime.test.ts deleted file mode 100644 index 78f2e6ae..00000000 --- a/test/core/scripts/verify-terminal-runtime.test.ts +++ /dev/null @@ -1,218 +0,0 @@ -import fs from 'fs'; -import os from 'os'; -import path from 'path'; -import { createRequire } from 'module'; -import { afterEach, describe, expect, it, vi } from 'vitest'; - -const requireFromCore = createRequire( - path.resolve(process.cwd(), 'packages/core/package.json'), -); -const verifyTerminalRuntime = requireFromCore( - './scripts/verify-terminal-runtime.js', -) as { - ensureHelperExecutable: ( - filePath: string, - platform: string, - ) => { exists: boolean; executable: boolean; fixed: boolean }; - getSpawnHelperCandidates: ( - nodePtyRoot: string, - platform: string, - arch: string, - ) => string[]; - probeNodePtySpawn: ( - nodePty: { spawn: Function }, - options: Record, - ) => Promise<{ ok: boolean; reason?: string }>; - runTerminalRuntimeCheck: (options?: Record) => Promise<{ - ok: boolean; - skipped: boolean; - fixedPaths: string[]; - helperPaths: string[]; - reason?: string; - }>; -}; - -describe('verify terminal runtime script', () => { - const tempDirs: string[] = []; - - afterEach(() => { - for (const tempDir of tempDirs.splice(0)) { - try { - fs.rmSync(tempDir, { recursive: true, force: true }); - } catch { - // ignore cleanup errors - } - } - }); - - it('should repair execute permission for spawn-helper on unix', () => { - const tempDir = fs.mkdtempSync( - path.join(os.tmpdir(), 'verify-terminal-helper-'), - ); - tempDirs.push(tempDir); - const helperPath = path.join(tempDir, 'spawn-helper'); - - fs.writeFileSync(helperPath, '#!/bin/sh\nexit 0\n'); - fs.chmodSync(helperPath, 0o644); - - const result = verifyTerminalRuntime.ensureHelperExecutable( - helperPath, - 'darwin', - ); - - expect(result.exists).toBe(true); - expect(result.fixed).toBe(true); - expect(result.executable).toBe(true); - expect(fs.statSync(helperPath).mode & 0o111).not.toBe(0); - }); - - it('should repair helper permissions and pass the PTY probe', async () => { - const tempDir = fs.mkdtempSync( - path.join(os.tmpdir(), 'verify-terminal-run-'), - ); - tempDirs.push(tempDir); - const helperDir = path.join(tempDir, 'prebuilds', 'darwin-arm64'); - const helperPath = path.join(helperDir, 'spawn-helper'); - fs.mkdirSync(helperDir, { recursive: true }); - fs.writeFileSync(helperPath, '#!/bin/sh\nexit 0\n'); - fs.chmodSync(helperPath, 0o644); - - const logger = { - log: vi.fn(), - warn: vi.fn(), - }; - const fakeNodePty = { - spawn: vi.fn(() => ({ - onExit: (callback: Function) => { - callback({ exitCode: 0 }); - }, - })), - }; - - const result = await verifyTerminalRuntime.runTerminalRuntimeCheck({ - logger, - nodePtyRoot: tempDir, - nodePty: fakeNodePty, - platform: 'darwin', - arch: 'arm64', - cwd: tempDir, - }); - - expect(result.ok).toBe(true); - expect(result.skipped).toBe(false); - expect(result.fixedPaths).toEqual([helperPath]); - expect(fakeNodePty.spawn).toHaveBeenCalledTimes(1); - expect(logger.warn).not.toHaveBeenCalled(); - }); - - it('should warn and return a failed probe result without throwing', async () => { - const tempDir = fs.mkdtempSync( - path.join(os.tmpdir(), 'verify-terminal-fail-'), - ); - tempDirs.push(tempDir); - const helperDir = path.join(tempDir, 'prebuilds', 'darwin-arm64'); - const helperPath = path.join(helperDir, 'spawn-helper'); - fs.mkdirSync(helperDir, { recursive: true }); - fs.writeFileSync(helperPath, '#!/bin/sh\nexit 0\n'); - fs.chmodSync(helperPath, 0o755); - - const logger = { - log: vi.fn(), - warn: vi.fn(), - }; - - const result = await verifyTerminalRuntime.runTerminalRuntimeCheck({ - logger, - nodePtyRoot: tempDir, - nodePty: { - spawn: () => { - throw new Error('posix_spawnp failed'); - }, - }, - platform: 'darwin', - arch: 'arm64', - cwd: tempDir, - }); - - expect(result.ok).toBe(false); - expect(result.skipped).toBe(false); - expect(result.helperPaths).toEqual( - verifyTerminalRuntime.getSpawnHelperCandidates(tempDir, 'darwin', 'arm64'), - ); - expect(logger.warn).toHaveBeenCalledTimes(1); - }); - - it('should skip the PTY probe on Windows during installation', async () => { - const logger = { - log: vi.fn(), - warn: vi.fn(), - }; - const spawn = vi.fn(() => { - throw new Error('Windows PTY probe must not run during installation'); - }); - - const result = await verifyTerminalRuntime.runTerminalRuntimeCheck({ - logger, - nodePtyRoot: 'C:\\node_modules\\node-pty', - nodePty: { spawn }, - platform: 'win32', - arch: 'x64', - cwd: process.cwd(), - }); - - expect(result.ok).toBe(true); - expect(result.skipped).toBe(true); - expect(result.reason).toBe( - 'Terminal runtime verification is deferred on Windows.', - ); - expect(spawn).not.toHaveBeenCalled(); - expect(logger.warn).not.toHaveBeenCalled(); - }); - - it('should skip verification when node-pty is not installed', async () => { - const logger = { - log: vi.fn(), - warn: vi.fn(), - }; - - const result = await verifyTerminalRuntime.runTerminalRuntimeCheck({ - logger, - nodePtyRoot: null, - }); - - expect(result.ok).toBe(true); - expect(result.skipped).toBe(true); - expect(result.reason).toBe('node-pty is not installed.'); - expect(logger.warn).not.toHaveBeenCalled(); - }); - - it('should use platform-specific helper candidate paths', () => { - const candidates = verifyTerminalRuntime.getSpawnHelperCandidates( - '/tmp/node-pty', - 'darwin', - 'arm64', - ); - - expect(candidates).toContain( - '/tmp/node-pty/prebuilds/darwin-arm64/spawn-helper', - ); - }); - - it('should resolve a successful PTY probe with a fake node-pty module', async () => { - const result = await verifyTerminalRuntime.probeNodePtySpawn( - { - spawn: () => ({ - onExit: (callback: Function) => { - setTimeout(() => callback({ exitCode: 0 }), 0); - }, - }), - }, - { - platform: 'darwin', - cwd: process.cwd(), - }, - ); - - expect(result).toEqual({ ok: true }); - }); -});