Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license

This commit is contained in:
yilsem committed 2026-09-05 13:26:46 +03:00
commit 03634b1ca3
11340 files changed
+3442369

No files matched your search

@@ -0,0 +1,6 @@
export function logForDebugging(
_message: string,
_options: {
level?: string
} = {}
): void {}
@@ -0,0 +1,131 @@
import { lastGrapheme } from './intl.js'
let earlyInputBuffer = ''
let isCapturing = false
let readableHandler: (() => void) | null = null
export function startCapturingEarlyInput(): void {
if (!process.stdin.isTTY || isCapturing || process.argv.includes('-p') || process.argv.includes('--print')) {
return
}
isCapturing = true
earlyInputBuffer = ''
try {
process.stdin.setEncoding('utf8')
process.stdin.setRawMode(true)
process.stdin.ref()
readableHandler = () => {
let chunk = process.stdin.read()
while (chunk !== null) {
if (typeof chunk === 'string') {
processChunk(chunk)
}
chunk = process.stdin.read()
}
}
process.stdin.on('readable', readableHandler)
} catch {
isCapturing = false
}
}
function processChunk(str: string): void {
let i = 0
while (i < str.length) {
const char = str[i]!
const code = char.charCodeAt(0)
if (code === 3) {
stopCapturingEarlyInput()
process.exit(130)
return
}
if (code === 4) {
stopCapturingEarlyInput()
return
}
if (code === 127 || code === 8) {
if (earlyInputBuffer.length > 0) {
const last = lastGrapheme(earlyInputBuffer)
earlyInputBuffer = earlyInputBuffer.slice(0, -(last.length || 1))
}
i++
continue
}
if (code === 27) {
i++
while (i < str.length && !(str.charCodeAt(i) >= 64 && str.charCodeAt(i) <= 126)) {
i++
}
if (i < str.length) {
i++
}
continue
}
if (code < 32 && code !== 9 && code !== 10 && code !== 13) {
i++
continue
}
if (code === 13) {
earlyInputBuffer += '\n'
i++
continue
}
earlyInputBuffer += char
i++
}
}
export function stopCapturingEarlyInput(): void {
if (!isCapturing) {
return
}
isCapturing = false
if (readableHandler) {
process.stdin.removeListener('readable', readableHandler)
readableHandler = null
}
}
export function consumeEarlyInput(): string {
stopCapturingEarlyInput()
const input = earlyInputBuffer.trim()
earlyInputBuffer = ''
return input
}
export function hasEarlyInput(): boolean {
return earlyInputBuffer.trim().length > 0
}
export function seedEarlyInput(text: string): void {
earlyInputBuffer = text
}
export function isCapturingEarlyInput(): boolean {
return isCapturing
}
@@ -0,0 +1,66 @@
type TerminalName = string | null
function detectTerminal(): TerminalName {
if (process.env.CURSOR_TRACE_ID) {
return 'cursor'
}
if (process.env.TERM === 'xterm-ghostty') {
return 'ghostty'
}
if (process.env.TERM?.includes('kitty')) {
return 'kitty'
}
if (process.env.TERM_PROGRAM) {
return process.env.TERM_PROGRAM
}
if (process.env.TMUX) {
return 'tmux'
}
if (process.env.STY) {
return 'screen'
}
if (process.env.KITTY_WINDOW_ID) {
return 'kitty'
}
if (process.env.WT_SESSION) {
return 'windows-terminal'
}
return process.env.TERM ?? null
}
export const env = {
terminal: detectTerminal()
}
// Terminals known to correctly implement OSC 52 clipboard writes
// (ESC ] 52 ; c ; <b64> BEL/ST — osc() in ink/termio/osc.ts emits BEL
// for most terminals and ST for kitty). When detected, setClipboard() skips the
// native-tool safety net entirely — running wl-copy/xclip/pbcopy in
// parallel with OSC 52 races the terminal's own clipboard write and can
// corrupt it (e.g. wl-copy on Wayland holds the selection in a background
// daemon; stacking two writes within ~30ms triggers a SIGTERM race).
// Intentionally conservative: terminals with known flaky or disabled-by-
// default OSC 52 (iTerm2 disables OSC 52 by default; Alacritty detection
// is unreliable) are not on this list. Users on those terminals keep the
// existing behaviour (native safety net fires alongside OSC 52).
//
// Lives here in utils/env.ts (rather than ink/terminal.ts) so that
// ink/termio/osc.ts can import it without creating a circular dependency:
// ink/terminal.ts already imports `link` from ink/termio/osc.ts.
const OSC52_CAPABLE_TERMINALS = ['ghostty', 'kitty', 'WezTerm', 'windows-terminal', 'vscode']
/** True if this terminal is known to correctly handle OSC 52 clipboard
* writes, so setClipboard() can skip the native-tool safety net.
* Accepts an optional terminal name for testability; defaults to the
* module-level `env.terminal` detected at startup. */
export function supportsOsc52Clipboard(terminal: string | null = env.terminal): boolean {
return OSC52_CAPABLE_TERMINALS.includes(terminal ?? '')
}
@@ -0,0 +1,13 @@
export function isEnvTruthy(envVar: string | boolean | undefined): boolean {
if (!envVar) {
return false
}
if (typeof envVar === 'boolean') {
return envVar
}
const v = envVar.toLowerCase().trim()
return ['1', 'true', 'yes', 'on'].includes(v)
}
@@ -0,0 +1,159 @@
import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { execFileNoThrow } from './execFileNoThrow.js'
// These tests shell out to /bin/sh, use chmodSync(0o755), and rely on
// POSIX sleep/job control. They will not work on Windows.
const onWindows = process.platform === 'win32'
// We simulate `wl-copy`'s daemonization behavior with a tiny shell script:
// 1. Fork a short-lived background sleeper that inherits stdio (so the
// parent process's pipes can never close).
// 2. Record the sleeper PID to a file so afterEach can clean it up.
// 3. Exit immediately with status 0.
//
// Without resolveOnExit, the await on `'close'` hangs until SIGTERM at
// timeout — exactly the production wl-copy bug. With resolveOnExit, the
// promise settles on `'exit'` regardless of the inherited pipes.
let scriptDir: string
let daemonScript: string
let sleeperPids: number[]
/** Read the PID file the daemon script writes, and track it for afterEach cleanup. */
function trackSleeperPid(pidFile: string): void {
try {
const pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10)
if (pid > 0) {
sleeperPids.push(pid)
}
} catch {
// PID file not written or unreadable — sleeper may have already exited.
}
}
beforeEach(() => {
sleeperPids = []
scriptDir = join(tmpdir(), `hermes-execfile-test-${process.pid}-${Date.now()}`)
mkdirSync(scriptDir, { recursive: true })
daemonScript = join(scriptDir, 'fake-daemonizer.sh')
// Posix sh: the `sleep 3 &` child inherits stdin/stdout/stderr from the
// shell, which inherited them from `spawn(stdio: 'pipe')`. The shell
// exits but its child (the sleeper) keeps the pipes open. Mirrors how
// wl-copy double-forks then exits while the daemon holds the selection.
// The sleeper writes its PID to $1 so we can clean it up reliably.
writeFileSync(daemonScript, '#!/bin/sh\nsleep 3 &\necho $! > "$1"\nexit 0\n')
chmodSync(daemonScript, 0o755)
})
afterEach(() => {
// Kill orphaned sleepers so they don't accumulate across watch runs.
for (const pid of sleeperPids) {
try {
process.kill(pid, 'SIGKILL')
} catch {
// Already exited — fine.
}
}
rmSync(scriptDir, { recursive: true, force: true })
})
describe.skipIf(onWindows)('execFileNoThrow with daemon-style children', () => {
// Formerly a documented forever-hang: without resolveOnExit, the 'close'
// event doesn't fire when the immediate child has exited but a forked
// daemon still holds stdio open, and even the SIGTERM at timeout used to
// leave the promise unsettled (#93134). The unconditional settle(124) in
// the timeout handler now guarantees the await resolves with 124.
// The daemon script's sleeper lives 30s so it genuinely outlives the
// timeout (and vitest's own 5s test timeout — before the fix this test
// fails by timing out, not by asserting).
it('settles with code=124 on timeout when a daemon inherits stdio and resolveOnExit is off', async () => {
const pidFile = join(scriptDir, 'sleeper-skip.pid')
const longDaemonScript = join(scriptDir, 'fake-daemonizer-long.sh')
writeFileSync(longDaemonScript, '#!/bin/sh\nsleep 30 &\necho $! > "$1"\nexit 0\n')
chmodSync(longDaemonScript, 0o755)
const start = Date.now()
const result = await execFileNoThrow(longDaemonScript, [pidFile], { timeout: 300 })
trackSleeperPid(pidFile)
expect(result.code).toBe(124)
expect(Date.now() - start).toBeLessThan(2000)
})
it("settles immediately on 'exit' when resolveOnExit is true, regardless of daemon stdio", async () => {
const pidFile = join(scriptDir, 'sleeper-exit.pid')
const start = Date.now()
const result = await execFileNoThrow(daemonScript, [pidFile], {
timeout: 2000,
resolveOnExit: true
})
trackSleeperPid(pidFile)
const elapsed = Date.now() - start
// The shell exits in a few ms. resolveOnExit lets us return on exit
// (code 0) instead of waiting for the orphaned sleeper to release
// stdio. Should be well under 200ms even on slow CI.
expect(result.code).toBe(0)
expect(elapsed).toBeLessThan(500)
})
it("still surfaces the right code when resolveOnExit'd child exits non-zero", async () => {
const pidFile = join(scriptDir, 'sleeper-fail.pid')
const failScript = join(scriptDir, 'fail.sh')
writeFileSync(failScript, `#!/bin/sh\nsleep 3 &\necho $! > "${pidFile}"\nexit 7\n`)
chmodSync(failScript, 0o755)
const result = await execFileNoThrow(failScript, [], {
timeout: 2000,
resolveOnExit: true
})
trackSleeperPid(pidFile)
expect(result.code).toBe(7)
})
it('settles on timeout=124 when the child itself never exits, even with resolveOnExit', async () => {
const slowScript = join(scriptDir, 'slow.sh')
writeFileSync(slowScript, '#!/bin/sh\nsleep 30\n')
chmodSync(slowScript, 0o755)
const result = await execFileNoThrow(slowScript, [], {
timeout: 200,
resolveOnExit: true
})
// Child process never exits on its own → timer fires → SIGTERM →
// child exits → 'exit' fires with non-null signal. The settle()
// call from the timer registers code=124 first. Either way: 124.
expect(result.code).toBe(124)
})
it('does not double-resolve when both timer and exit fire', async () => {
const pidFile = join(scriptDir, 'sleeper-race.pid')
// Race: child happens to exit right around the timeout. The settled
// guard ensures only the first resolution wins.
const result = await execFileNoThrow(daemonScript, [pidFile], {
timeout: 50, // very tight
resolveOnExit: true
})
trackSleeperPid(pidFile)
// Either code=0 (exit beat timer) or code=124 (timer beat exit).
// Both are valid outcomes; the contract is that the promise settles
// exactly once and doesn't throw.
expect([0, 124]).toContain(result.code)
})
})
@@ -0,0 +1,115 @@
import { type ChildProcess, spawn, type StdioOptions } from 'child_process'
type ExecFileOptions = {
input?: string
timeout?: number
useCwd?: boolean
env?: NodeJS.ProcessEnv
/** Resolve as soon as the child *exits*, instead of waiting for its
* stdio streams to close. Use this for tools that fork a daemon and
* let the daemon inherit the parent's stdio (e.g. `wl-copy`): the
* child exits immediately, but `'close'` never fires because the
* daemon holds the pipes open.
*
* When true, stdout and stderr are set to 'ignore' to prevent the
* daemon from inheriting those pipe FDs — the caller must not
* depend on collecting stdout/stderr content. Both will always be
* empty strings in this mode. */
resolveOnExit?: boolean
}
export function execFileNoThrow(
file: string,
args: string[],
options: ExecFileOptions = {}
): Promise<{
stdout: string
stderr: string
code: number
error?: string
}> {
return new Promise(resolve => {
// When resolveOnExit is true, ignore stdout/stderr so the daemon
// doesn't inherit those pipe FDs — prevents handle leaks that can
// keep the parent process alive. No output data is collected in
// this mode; both stdout and stderr will be empty strings.
const stdioConfig: StdioOptions = options.resolveOnExit ? ['pipe', 'ignore', 'ignore'] : 'pipe'
const child: ChildProcess = spawn(file, args, {
cwd: options.useCwd ? process.cwd() : undefined,
env: options.env,
stdio: stdioConfig
})
let stdout = ''
let stderr = ''
let timedOut = false
let settled = false
const settle = (code: number, error?: string) => {
if (settled) {
return
}
settled = true
if (timer) {
clearTimeout(timer)
}
// Destroy any remaining streams to release FDs promptly.
// After settle(), nobody reads from these anymore.
child.stdout?.destroy()
child.stderr?.destroy()
resolve({ stdout, stderr, code, ...(error ? { error } : {}) })
}
const timer = options.timeout
? setTimeout(() => {
timedOut = true
child.kill('SIGTERM')
// Settle unconditionally: SIGTERM-ing the child does not
// guarantee 'close'/'exit' will fire. In the default
// (non-resolveOnExit) path a daemonized grandchild that
// inherited the stdio pipes keeps them open forever, so
// 'close' never fires and the promise leaked (#93134).
// The settled-guard makes this a no-op when the child's own
// 'exit'/'close' won the race.
settle(124)
}, options.timeout)
: null
child.stdout?.on('data', chunk => {
stdout += String(chunk)
})
child.stderr?.on('data', chunk => {
stderr += String(chunk)
})
child.on('error', error => {
settle(1, String(error))
})
if (options.resolveOnExit) {
// 'exit' fires when the child process itself exits — even if the
// daemon it forked still holds the inherited stdio pipes open.
// When a signal kills the child, code is null — map that to 1
// so callers don't mistake a signal-terminated run for success.
child.on('exit', (code, signal) => {
const exitCode = timedOut ? 124 : (code ?? (signal ? 1 : 0))
settle(exitCode)
})
} else {
child.on('close', (code, signal) => {
const exitCode = timedOut ? 124 : (code ?? (signal ? 1 : 0))
settle(exitCode)
})
}
if (options.input) {
child.stdin?.write(options.input)
}
child.stdin?.end()
})
}
@@ -0,0 +1,3 @@
export function isMouseClicksDisabled(): boolean {
return /^(1|true|yes|on)$/.test((process.env.HERMES_TUI_DISABLE_MOUSE_CLICKS ?? '').trim().toLowerCase())
}
@@ -0,0 +1,87 @@
let graphemeSegmenter: Intl.Segmenter | null = null
let wordSegmenter: Intl.Segmenter | null = null
export function getGraphemeSegmenter(): Intl.Segmenter {
if (!graphemeSegmenter) {
graphemeSegmenter = new Intl.Segmenter(undefined, {
granularity: 'grapheme'
})
}
return graphemeSegmenter
}
export function firstGrapheme(text: string): string {
if (!text) {
return ''
}
const segments = getGraphemeSegmenter().segment(text)
const first = segments[Symbol.iterator]().next().value
return first?.segment ?? ''
}
export function lastGrapheme(text: string): string {
if (!text) {
return ''
}
let last = ''
for (const { segment } of getGraphemeSegmenter().segment(text)) {
last = segment
}
return last
}
export function getWordSegmenter(): Intl.Segmenter {
if (!wordSegmenter) {
wordSegmenter = new Intl.Segmenter(undefined, { granularity: 'word' })
}
return wordSegmenter
}
const rtfCache = new Map<string, Intl.RelativeTimeFormat>()
export function getRelativeTimeFormat(
style: 'long' | 'short' | 'narrow',
numeric: 'always' | 'auto'
): Intl.RelativeTimeFormat {
const key = `${style}:${numeric}`
let rtf = rtfCache.get(key)
if (!rtf) {
rtf = new Intl.RelativeTimeFormat('en', { style, numeric })
rtfCache.set(key, rtf)
}
return rtf
}
let cachedTimeZone: string | null = null
export function getTimeZone(): string {
if (!cachedTimeZone) {
cachedTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
}
return cachedTimeZone
}
let cachedSystemLocaleLanguage: string | undefined | null = null
export function getSystemLocaleLanguage(): string | undefined {
if (cachedSystemLocaleLanguage === null) {
try {
const locale = Intl.DateTimeFormat().resolvedOptions().locale
cachedSystemLocaleLanguage = new Intl.Locale(locale).language
} catch {
cachedSystemLocaleLanguage = undefined
}
}
return cachedSystemLocaleLanguage
}
@@ -0,0 +1,7 @@
export function logError(error: unknown): void {
if (!process.env.HERMES_INK_DEBUG_ERRORS) {
return
}
console.error(error)
}
@@ -0,0 +1,57 @@
let _npmSemver: typeof import('semver') | undefined
function getNpmSemver(): typeof import('semver') {
if (!_npmSemver) {
_npmSemver = require('semver') as typeof import('semver')
}
return _npmSemver
}
export function gt(a: string, b: string): boolean {
if (typeof Bun !== 'undefined') {
return Bun.semver.order(a, b) === 1
}
return getNpmSemver().gt(a, b, { loose: true })
}
export function gte(a: string, b: string): boolean {
if (typeof Bun !== 'undefined') {
return Bun.semver.order(a, b) >= 0
}
return getNpmSemver().gte(a, b, { loose: true })
}
export function lt(a: string, b: string): boolean {
if (typeof Bun !== 'undefined') {
return Bun.semver.order(a, b) === -1
}
return getNpmSemver().lt(a, b, { loose: true })
}
export function lte(a: string, b: string): boolean {
if (typeof Bun !== 'undefined') {
return Bun.semver.order(a, b) <= 0
}
return getNpmSemver().lte(a, b, { loose: true })
}
export function satisfies(version: string, range: string): boolean {
if (typeof Bun !== 'undefined') {
return Bun.semver.satisfies(version, range)
}
return getNpmSemver().satisfies(version, range, { loose: true })
}
export function order(a: string, b: string): -1 | 0 | 1 {
if (typeof Bun !== 'undefined') {
return Bun.semver.order(a, b)
}
return getNpmSemver().compare(a, b, { loose: true }) as -1 | 0 | 1
}
@@ -0,0 +1,106 @@
import { type AnsiCode, ansiCodesToString, reduceAnsiCodes, tokenize, undoAnsiCodes } from '@alcalzone/ansi-tokenize'
import { lruEvict } from '../ink/lru.js'
import { stringWidth } from '../ink/stringWidth.js'
function isEndCode(code: AnsiCode): boolean {
return code.code === code.endCode
}
function filterStartCodes(codes: AnsiCode[]): AnsiCode[] {
return codes.filter(c => !isEndCode(c))
}
// LRU cache: same (string, start, end) → same output. Output.get() re-emits
// identical writes every frame for stable transcript content; this avoids
// re-tokenizing them. CPU profile (Apr 2026) showed sliceAnsi at 18% total
// time during scroll. Bounded at 4096 entries — entries are short clipped
// lines so memory cost is small.
const sliceCache = new Map<string, string>()
const SLICE_CACHE_LIMIT = 4096
export default function sliceAnsi(str: string, start: number, end?: number): string {
if (!str) {
return ''
}
// Hot-path: only cache when end is defined (the Output.get() use-case).
if (end !== undefined) {
const key = `${start}|${end}|${str}`
const cached = sliceCache.get(key)
if (cached !== undefined) {
sliceCache.delete(key)
sliceCache.set(key, cached)
return cached
}
const result = computeSlice(str, start, end)
if (sliceCache.size >= SLICE_CACHE_LIMIT) {
sliceCache.delete(sliceCache.keys().next().value!)
}
sliceCache.set(key, result)
return result
}
return computeSlice(str, start, end)
}
export function sliceCacheSize(): number {
return sliceCache.size
}
export function evictSliceCache(keepRatio = 0): void {
lruEvict(sliceCache, keepRatio)
}
function computeSlice(str: string, start: number, end?: number): string {
const tokens = tokenize(str)
let activeCodes: AnsiCode[] = []
let position = 0
let result = ''
let include = false
for (const token of tokens) {
const width = token.type === 'ansi' ? 0 : token.fullWidth ? 2 : stringWidth(token.value)
if (end !== undefined && position >= end) {
if (token.type === 'ansi' || width > 0 || !include) {
break
}
}
if (token.type === 'ansi') {
activeCodes.push(token)
if (include) {
result += token.code
}
} else {
if (!include && position >= start) {
if (start > 0 && width === 0) {
continue
}
include = true
activeCodes = filterStartCodes(reduceAnsiCodes(activeCodes))
result = ansiCodesToString(activeCodes)
}
if (include) {
result += token.value
}
position += width
}
}
const activeStartCodes = filterStartCodes(reduceAnsiCodes(activeCodes))
result += ansiCodesToString(undoAnsiCodes(activeStartCodes))
return result
}