fix(windows): prevent terminal startup failures

## Bug 详情
- issues.md #46:Windows MSYS2/UCRT64 下执行 csc,欢迎信息输出后在 stdin raw mode/终端输入初始化阶段崩溃,出现 native std::bad_weak_ptr 和后续 Bun mutex corrupt。
- issues.md #47:cmd.exe 可启动,但 PowerShell 运行 csc 会走 npm 生成的 csc.ps1;截图显示 node.exe 报错 "Input must be provided either through stdin or as a prompt argument when using --print",说明裸 csc 被误判成 print/non-interactive 模式。

## 根因
- MSYS2/Cygwin/mintty 等 Windows 终端对 Node/Bun stdin raw mode 和终端扩展控制序列兼容性不稳定,启动早期强行 setRawMode/查询终端能力会触发崩溃风险。
- npm 的 PowerShell shim 解析优先级高于 csc.cmd,且可能让 stdout TTY 状态不可用;原 main.tsx 只要 stdout 非 TTY 就自动进入 non-interactive/--print 路径,最终在没有 prompt/stdin 时触发 #47 的报错。

## 修复方案
- 为 Ink stdin raw mode 增加 Windows 风险终端检测,在 MSYS2/Cygwin/mintty 或显式禁用开关下跳过 raw mode,使用 cooked input fallback。
- early input 捕获在启动早期复用同类风险判断,避免 REPL 渲染前就触发 raw mode 崩溃。
- postinstall 在 Windows 上清理属于本包的 csc.ps1,让 PowerShell 回落到与 cmd.exe 一致的 csc.cmd 启动路径。
- 新增 non-interactive 判定工具,保留显式 -p/--print、--init-only、--sdk-url 的 headless 行为,但要求 stdin/stdout 都不是交互 TTY 才自动进入 non-interactive,避免 PowerShell shim 下裸 csc 被误判。

## 变更要点
- 新增 packages/@ant/ink/src/core/raw-mode-support.ts 及单元测试,覆盖普通 Windows shell、MSYS2/Cygwin/mintty 和禁用开关
- 调整 Ink App raw mode 生命周期,只在真正启用 raw mode 后写入/清理 bracketed paste、focus reporting、extended key 等终端控制序列
- 调整 component unmount/exit 清理逻辑,按 rawModeEnabledCount/rawModeActive 关闭实际启用的输入监听和 raw mode
- 调整 src/utils/earlyInput.ts,避免不安全 Windows 终端在启动阶段启用 stdin raw mode
- 调整 scripts/postinstall.cjs,立即和延迟清理 npm 生成的 csc.ps1 shim
- 调整 src/main.tsx 使用 shouldUseNonInteractiveSession,并新增对应测试覆盖 PowerShell shim 场景

## 自测
- bun test packages/@ant/ink/src/core/__tests__/raw-mode-support.test.ts src/utils/__tests__/earlyInput.test.ts src/utils/__tests__/nonInteractiveSession.test.ts
- node --check scripts/postinstall.cjs
- bun run src/entrypoints/cli.tsx --version
- bun run lint
- git diff --check

## 已知限制
- bun run typecheck 仍失败,错误来自仓库既有缺失声明/缺失模块和无关类型问题,不来自本次改动。
This commit is contained in:
IronRookieCoder 2026-05-14 16:55:07 +08:00
parent 95a4430fa2
commit 223b694965
8 changed files with 418 additions and 53 deletions

View File

@ -40,6 +40,7 @@ import {
type ParsedMouse, type ParsedMouse,
parseMultipleKeypresses, parseMultipleKeypresses,
} from '../core/parse-keypress.js'; } from '../core/parse-keypress.js';
import { supportsStdinRawMode } from '../core/raw-mode-support.js';
import reconciler from '../core/reconciler.js'; import reconciler from '../core/reconciler.js';
import { finishSelection, hasSelection, type SelectionState, startSelection } from '../core/selection.js'; import { finishSelection, hasSelection, type SelectionState, startSelection } from '../core/selection.js';
import { isXtermJs, setXtversionName, supportsExtendedKeys } from '../core/terminal.js'; import { isXtermJs, setXtversionName, supportsExtendedKeys } from '../core/terminal.js';
@ -157,6 +158,7 @@ export default class App extends PureComponent<Props, State> {
// Count how many components enabled raw mode to avoid disabling // Count how many components enabled raw mode to avoid disabling
// raw mode until all components don't need it anymore // raw mode until all components don't need it anymore
rawModeEnabledCount = 0; rawModeEnabledCount = 0;
rawModeActive = false;
internal_eventEmitter = new EventEmitter(); internal_eventEmitter = new EventEmitter();
keyParseState = INITIAL_STATE; keyParseState = INITIAL_STATE;
@ -195,7 +197,11 @@ export default class App extends PureComponent<Props, State> {
// Determines if TTY is supported on the provided stdin // Determines if TTY is supported on the provided stdin
isRawModeSupported(): boolean { isRawModeSupported(): boolean {
return this.props.stdin.isTTY; return supportsStdinRawMode(this.props.stdin);
}
isCookedModeFallbackSupported(): boolean {
return this.props.stdin.isTTY && !this.isRawModeSupported();
} }
override render() { override render() {
@ -258,7 +264,7 @@ export default class App extends PureComponent<Props, State> {
this.pendingHyperlinkTimer = null; this.pendingHyperlinkTimer = null;
} }
// ignore calling setRawMode on an handle stdin it cannot be called // ignore calling setRawMode on an handle stdin it cannot be called
if (this.isRawModeSupported()) { if (this.rawModeEnabledCount > 0) {
this.handleSetRawMode(false); this.handleSetRawMode(false);
} else { } else {
// Even when raw mode was never enabled (e.g. non-TTY stdin on // Even when raw mode was never enabled (e.g. non-TTY stdin on
@ -279,7 +285,8 @@ export default class App extends PureComponent<Props, State> {
handleSetRawMode = (isEnabled: boolean): void => { handleSetRawMode = (isEnabled: boolean): void => {
const { stdin } = this.props; const { stdin } = this.props;
if (!this.isRawModeSupported()) { const rawModeSupported = this.isRawModeSupported();
if (!rawModeSupported && !this.isCookedModeFallbackSupported()) {
if (stdin === process.stdin) { if (stdin === process.stdin) {
throw new Error( throw new Error(
'Raw mode is not supported on the current process.stdin, which Ink uses as input stream by default.\nRead about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported', 'Raw mode is not supported on the current process.stdin, which Ink uses as input stream by default.\nRead about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported',
@ -314,39 +321,50 @@ export default class App extends PureComponent<Props, State> {
} }
stdin.ref(); stdin.ref();
stdin.setRawMode(true); if (rawModeSupported) {
stdin.addListener('readable', this.handleReadable); stdin.setRawMode(true);
// Enable bracketed paste mode this.rawModeActive = true;
this.props.stdout.write(EBP); } else {
// Enable terminal focus reporting (DECSET 1004) this.rawModeActive = false;
this.props.stdout.write(EFE); defaultCallbacks.logForDebugging(
// Enable extended key reporting so ctrl+shift+<letter> is '[stdin] Raw mode disabled for this Windows terminal; using cooked input fallback',
// distinguishable from ctrl+<letter>. We write both the kitty stack { level: 'warn' },
// push (CSI >1u) and xterm modifyOtherKeys level 2 (CSI >4;2m) — );
// terminals honor whichever they implement (tmux only accepts the
// latter).
if (supportsExtendedKeys()) {
this.props.stdout.write(ENABLE_KITTY_KEYBOARD);
this.props.stdout.write(ENABLE_MODIFY_OTHER_KEYS);
} }
// Probe terminal identity. XTVERSION survives SSH (query/reply goes stdin.addListener('readable', this.handleReadable);
// through the pty), unlike TERM_PROGRAM. Used for wheel-scroll base if (rawModeSupported) {
// detection when env vars are absent. Fire-and-forget: the DA1 // Enable bracketed paste mode
// sentinel bounds the round-trip, and if the terminal ignores the this.props.stdout.write(EBP);
// query, flush() still resolves and name stays undefined. // Enable terminal focus reporting (DECSET 1004)
// Deferred to next tick so it fires AFTER the current synchronous this.props.stdout.write(EFE);
// init sequence completes — avoids interleaving with alt-screen/mouse // Enable extended key reporting so ctrl+shift+<letter> is
// tracking enable writes that may happen in the same render cycle. // distinguishable from ctrl+<letter>. We write both the kitty stack
setImmediate(() => { // push (CSI >1u) and xterm modifyOtherKeys level 2 (CSI >4;2m) —
void Promise.all([this.querier.send(xtversion()), this.querier.flush()]).then(([r]) => { // terminals honor whichever they implement (tmux only accepts the
if (r) { // latter).
setXtversionName(r.name); if (supportsExtendedKeys()) {
defaultCallbacks.logForDebugging(`XTVERSION: terminal identified as "${r.name}"`); this.props.stdout.write(ENABLE_KITTY_KEYBOARD);
} else { this.props.stdout.write(ENABLE_MODIFY_OTHER_KEYS);
defaultCallbacks.logForDebugging('XTVERSION: no reply (terminal ignored query)'); }
} // Probe terminal identity. XTVERSION survives SSH (query/reply goes
// through the pty), unlike TERM_PROGRAM. Used for wheel-scroll base
// detection when env vars are absent. Fire-and-forget: the DA1
// sentinel bounds the round-trip, and if the terminal ignores the
// query, flush() still resolves and name stays undefined.
// Deferred to next tick so it fires AFTER the current synchronous
// init sequence completes — avoids interleaving with alt-screen/mouse
// tracking enable writes that may happen in the same render cycle.
setImmediate(() => {
void Promise.all([this.querier.send(xtversion()), this.querier.flush()]).then(([r]) => {
if (r) {
setXtversionName(r.name);
defaultCallbacks.logForDebugging(`XTVERSION: terminal identified as "${r.name}"`);
} else {
defaultCallbacks.logForDebugging('XTVERSION: no reply (terminal ignored query)');
}
});
}); });
}); }
} }
this.rawModeEnabledCount++; this.rawModeEnabledCount++;
@ -366,13 +384,16 @@ export default class App extends PureComponent<Props, State> {
return; return;
} }
this.props.stdout.write(DISABLE_MODIFY_OTHER_KEYS); if (this.rawModeActive) {
this.props.stdout.write(DISABLE_KITTY_KEYBOARD); this.props.stdout.write(DISABLE_MODIFY_OTHER_KEYS);
// Disable terminal focus reporting (DECSET 1004) this.props.stdout.write(DISABLE_KITTY_KEYBOARD);
this.props.stdout.write(DFE); // Disable terminal focus reporting (DECSET 1004)
// Disable bracketed paste mode this.props.stdout.write(DFE);
this.props.stdout.write(DBP); // Disable bracketed paste mode
stdin.setRawMode(false); this.props.stdout.write(DBP);
stdin.setRawMode(false);
this.rawModeActive = false;
}
stdin.removeListener('readable', this.handleReadable); stdin.removeListener('readable', this.handleReadable);
stdin.unref(); stdin.unref();
} }
@ -480,7 +501,7 @@ export default class App extends PureComponent<Props, State> {
}; };
handleExit = (error?: Error): void => { handleExit = (error?: Error): void => {
if (this.isRawModeSupported()) { if (this.rawModeEnabledCount > 0) {
this.handleSetRawMode(false); this.handleSetRawMode(false);
} }

View File

@ -0,0 +1,53 @@
import { describe, expect, test } from 'bun:test';
import {
isWindowsRawModeUnsafe,
supportsStdinRawMode,
} from '../raw-mode-support.js';
describe('isWindowsRawModeUnsafe', () => {
test('does not disable raw mode for ordinary Windows shells', () => {
expect(
isWindowsRawModeUnsafe({
ComSpec: 'C:\\Windows\\System32\\cmd.exe',
}, 'win32'),
).toBe(false);
expect(
isWindowsRawModeUnsafe({
PSModulePath: 'C:\\Program Files\\PowerShell\\Modules',
}, 'win32'),
).toBe(false);
});
test('disables raw mode for MSYS2 and Cygwin terminals', () => {
expect(isWindowsRawModeUnsafe({ MSYSTEM: 'UCRT64' }, 'win32')).toBe(true);
expect(isWindowsRawModeUnsafe({ TERM: 'cygwin' }, 'win32')).toBe(true);
expect(isWindowsRawModeUnsafe({ TERM_PROGRAM: 'mintty' }, 'win32')).toBe(
true,
);
expect(isWindowsRawModeUnsafe({ SHELL: '/usr/bin/bash' }, 'win32')).toBe(
false,
);
expect(
isWindowsRawModeUnsafe({ SHELL: '/usr/bin/msys2_shell.cmd' }, 'win32'),
).toBe(true);
});
test('supports an explicit emergency disable switch', () => {
expect(
isWindowsRawModeUnsafe({
CLAUDE_CODE_DISABLE_STDIN_RAW_MODE: '1',
}, 'win32'),
).toBe(true);
});
});
describe('supportsStdinRawMode', () => {
test('requires a TTY stream with setRawMode', () => {
expect(supportsStdinRawMode({ isTTY: false } as NodeJS.ReadStream)).toBe(
false,
);
expect(supportsStdinRawMode({ isTTY: true } as NodeJS.ReadStream)).toBe(
false,
);
});
});

View File

@ -0,0 +1,43 @@
function isEnvTruthy(value: string | undefined): boolean {
return value === '1' || value === 'true';
}
export function isWindowsRawModeUnsafe(
env: NodeJS.ProcessEnv = process.env,
platform: NodeJS.Platform = process.platform,
): boolean {
if (platform !== 'win32') {
return false;
}
if (isEnvTruthy(env.CLAUDE_CODE_DISABLE_STDIN_RAW_MODE)) {
return true;
}
const term = env.TERM?.toLowerCase() ?? '';
const termProgram = env.TERM_PROGRAM?.toLowerCase() ?? '';
const msystem = env.MSYSTEM?.toLowerCase() ?? '';
const shell = env.SHELL?.toLowerCase() ?? '';
return (
term === 'cygwin' ||
term.includes('mintty') ||
termProgram.includes('mintty') ||
msystem.length > 0 ||
shell.includes('msys') ||
shell.includes('cygwin') ||
shell.includes('mingw')
);
}
export function supportsStdinRawMode(stdin: NodeJS.ReadStream): boolean {
if (!stdin.isTTY) {
return false;
}
if (isWindowsRawModeUnsafe(process.env, process.platform)) {
return false;
}
return typeof stdin.setRawMode === 'function';
}

View File

@ -13,7 +13,7 @@
const { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, chmodSync } = const { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, chmodSync } =
require("fs") require("fs")
const { spawnSync } = require("child_process") const { spawn, spawnSync } = require("child_process")
const { setDefaultResultOrder } = require("node:dns") const { setDefaultResultOrder } = require("node:dns")
const path = require("path") const path = require("path")
const os = require("os") const os = require("os")
@ -97,6 +97,110 @@ function getBinaryPath() {
return path.resolve(dir, subdir, binary) return path.resolve(dir, subdir, binary)
} }
function safeResolve(...parts) {
const filtered = parts.filter(Boolean)
if (filtered.length === 0) return null
return path.resolve(...filtered)
}
function removePowerShellShimIfOwned(shimPath) {
if (!shimPath || !existsSync(shimPath)) return false
const cmdShimPath = shimPath.replace(/\.ps1$/i, ".cmd")
if (!existsSync(cmdShimPath)) return false
let content
try {
content = readFileSync(shimPath, "utf8")
} catch {
return false
}
// Only remove npm-generated shims for this package. PowerShell resolves
// csc.ps1 before csc.cmd, and enterprise ExecutionPolicy often blocks .ps1
// launchers while cmd.exe works. Removing this shim makes PowerShell fall
// through to the same .cmd launcher used by cmd.exe.
const normalized = content.replace(/\\/g, "/")
if (!normalized.includes("@costrict") && !normalized.includes("dist/cli-node.js")) {
return false
}
try {
rmSync(shimPath, { force: true })
console.log(`[postinstall] Removed PowerShell shim ${shimPath}; PowerShell will use csc.cmd`)
return true
} catch (error) {
console.warn(
`[postinstall] Could not remove PowerShell shim ${shimPath}: ${
error instanceof Error ? error.message : String(error)
}`,
)
return false
}
}
function cleanupWindowsPowerShellShim() {
if (process.platform !== "win32") return
const dirs = getWindowsPowerShellShimDirs()
const seen = new Set()
for (const dir of dirs) {
const shimPath = path.join(dir, "csc.ps1")
if (seen.has(shimPath)) continue
seen.add(shimPath)
removePowerShellShimIfOwned(shimPath)
}
}
function getWindowsPowerShellShimDirs() {
return [
safeResolve(process.env.npm_config_prefix),
safeResolve(process.env.npm_config_local_prefix, "node_modules", ".bin"),
safeResolve(process.env.INIT_CWD, "node_modules", ".bin"),
safeResolve(projectRoot, "node_modules", ".bin"),
].filter(Boolean)
}
function scheduleWindowsPowerShellShimCleanup() {
if (process.platform !== "win32") return
const dirs = getWindowsPowerShellShimDirs()
if (dirs.length === 0) return
const script = String.raw`
const { existsSync, readFileSync, rmSync } = require("fs");
const path = require("path");
const dirs = JSON.parse(process.argv[1] || "[]");
setTimeout(() => {
for (const dir of dirs) {
const shimPath = path.join(dir, "csc.ps1");
const cmdShimPath = path.join(dir, "csc.cmd");
if (!existsSync(shimPath) || !existsSync(cmdShimPath)) continue;
let content = "";
try {
content = readFileSync(shimPath, "utf8").replace(/\\/g, "/");
} catch {
continue;
}
if (!content.includes("@costrict") && !content.includes("dist/cli-node.js")) continue;
try {
rmSync(shimPath, { force: true });
} catch {}
}
}, 3000);
`
try {
const child = spawn(process.execPath, ["-e", script, JSON.stringify(dirs)], {
detached: true,
stdio: "ignore",
windowsHide: true,
})
child.unref()
} catch {
// Best effort only. Immediate cleanup above covers the normal npm flow.
}
}
// --- Download helpers --- // --- Download helpers ---
function proxyEnvSet() { function proxyEnvSet() {
@ -341,6 +445,8 @@ async function downloadAndExtract() {
} }
async function main() { async function main() {
cleanupWindowsPowerShellShim()
scheduleWindowsPowerShellShimCleanup()
await downloadAndExtract() await downloadAndExtract()
} }

View File

@ -233,6 +233,7 @@ import { getGhAuthStatus } from "./utils/github/ghAuthStatus.js";
import { safeParseJSON } from "./utils/json.js"; import { safeParseJSON } from "./utils/json.js";
import { logError } from "./utils/log.js"; import { logError } from "./utils/log.js";
import { getModelDeprecationWarning } from "./utils/model/deprecation.js"; import { getModelDeprecationWarning } from "./utils/model/deprecation.js";
import { shouldUseNonInteractiveSession } from "./utils/nonInteractiveSession.js";
import { import {
getDefaultMainLoopModel, getDefaultMainLoopModel,
getUserSpecifiedModelSetting, getUserSpecifiedModelSetting,
@ -1134,15 +1135,16 @@ export async function main() {
} }
} }
// Check for -p/--print and --init-only flags early to set isInteractiveSession before init() // Check for -p/--print and --init-only flags early to set isInteractiveSession before init()
// This is needed because telemetry initialization calls auth functions that need this flag // This is needed because telemetry initialization calls auth functions that need this flag
const cliArgs = process.argv.slice(2); const cliArgs = process.argv.slice(2);
const hasPrintFlag = cliArgs.includes("-p") || cliArgs.includes("--print"); const forceInteractive = isEnvTruthy(process.env.CLAUDE_CODE_FORCE_INTERACTIVE);
const hasInitOnlyFlag = cliArgs.includes("--init-only"); const isNonInteractive = shouldUseNonInteractiveSession({
const hasSdkUrl = cliArgs.some((arg) => arg.startsWith("--sdk-url")); args: cliArgs,
const forceInteractive = isEnvTruthy(process.env.CLAUDE_CODE_FORCE_INTERACTIVE); forceInteractive,
const isNonInteractive = stdinIsTTY: process.stdin.isTTY,
hasPrintFlag || hasInitOnlyFlag || hasSdkUrl || (!forceInteractive && !process.stdout.isTTY); stdoutIsTTY: process.stdout.isTTY,
});
// Stop capturing early input for non-interactive modes // Stop capturing early input for non-interactive modes
if (isNonInteractive) { if (isNonInteractive) {

View File

@ -0,0 +1,80 @@
import { describe, expect, test } from 'bun:test'
import { shouldUseNonInteractiveSession } from '../nonInteractiveSession.js'
describe('shouldUseNonInteractiveSession', () => {
test('uses non-interactive mode for explicit headless flags', () => {
expect(
shouldUseNonInteractiveSession({
args: ['--print'],
forceInteractive: false,
stdinIsTTY: true,
stdoutIsTTY: true,
}),
).toBe(true)
expect(
shouldUseNonInteractiveSession({
args: ['-p'],
forceInteractive: true,
stdinIsTTY: true,
stdoutIsTTY: true,
}),
).toBe(true)
expect(
shouldUseNonInteractiveSession({
args: ['--init-only'],
forceInteractive: false,
stdinIsTTY: true,
stdoutIsTTY: true,
}),
).toBe(true)
expect(
shouldUseNonInteractiveSession({
args: ['--sdk-url=http://localhost:1234'],
forceInteractive: false,
stdinIsTTY: true,
stdoutIsTTY: true,
}),
).toBe(true)
})
test('does not enter print mode for PowerShell shims that preserve stdin TTY', () => {
expect(
shouldUseNonInteractiveSession({
args: [],
forceInteractive: false,
stdinIsTTY: true,
stdoutIsTTY: false,
}),
).toBe(false)
})
test('keeps automatic non-interactive mode when both stdio sides are non-TTY', () => {
expect(
shouldUseNonInteractiveSession({
args: [],
forceInteractive: false,
stdinIsTTY: false,
stdoutIsTTY: false,
}),
).toBe(true)
expect(
shouldUseNonInteractiveSession({
args: [],
forceInteractive: false,
stdinIsTTY: undefined,
stdoutIsTTY: undefined,
}),
).toBe(true)
})
test('forceInteractive only suppresses automatic non-interactive detection', () => {
expect(
shouldUseNonInteractiveSession({
args: [],
forceInteractive: true,
stdinIsTTY: false,
stdoutIsTTY: false,
}),
).toBe(false)
})
})

View File

@ -13,6 +13,39 @@
import { lastGrapheme } from './intl.js' import { lastGrapheme } from './intl.js'
function isEnvTruthy(value: string | undefined): boolean {
return value === '1' || value === 'true'
}
function supportsEarlyInputRawMode(): boolean {
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function') {
return false
}
if (process.platform !== 'win32') {
return true
}
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_STDIN_RAW_MODE)) {
return false
}
const term = process.env.TERM?.toLowerCase() ?? ''
const termProgram = process.env.TERM_PROGRAM?.toLowerCase() ?? ''
const msystem = process.env.MSYSTEM?.toLowerCase() ?? ''
const shell = process.env.SHELL?.toLowerCase() ?? ''
return !(
term === 'cygwin' ||
term.includes('mintty') ||
termProgram.includes('mintty') ||
msystem.length > 0 ||
shell.includes('msys') ||
shell.includes('cygwin') ||
shell.includes('mingw')
)
}
// Buffer for early input characters // Buffer for early input characters
let earlyInputBuffer = '' let earlyInputBuffer = ''
// Flag to track if we're currently capturing // Flag to track if we're currently capturing
@ -33,7 +66,7 @@ export function startCapturingEarlyInput(): void {
// be in print mode. Raw mode disables ISIG (terminal Ctrl+C → SIGINT), // be in print mode. Raw mode disables ISIG (terminal Ctrl+C → SIGINT),
// which would make -p uninterruptible. // which would make -p uninterruptible.
if ( if (
!process.stdin.isTTY || !supportsEarlyInputRawMode() ||
isCapturing || isCapturing ||
process.argv.includes('-p') || process.argv.includes('-p') ||
process.argv.includes('--print') process.argv.includes('--print')

View File

@ -0,0 +1,27 @@
export type NonInteractiveSessionOptions = {
args: string[]
forceInteractive: boolean
stdinIsTTY: boolean | undefined
stdoutIsTTY: boolean | undefined
}
export function shouldUseNonInteractiveSession({
args,
forceInteractive,
stdinIsTTY,
stdoutIsTTY,
}: NonInteractiveSessionOptions): boolean {
if (args.includes('-p') || args.includes('--print')) return true
if (args.includes('--init-only')) return true
if (args.some(arg => arg.startsWith('--sdk-url'))) return true
if (forceInteractive) return false
const hasInteractiveStdin = stdinIsTTY === true
const hasInteractiveStdout = stdoutIsTTY === true
// Some Windows launchers, notably npm's generated PowerShell .ps1 shim,
// can hide stdout TTY status while stdin is still an interactive terminal.
// Treat that as interactive so a bare `csc` does not fall into --print mode.
return !hasInteractiveStdout && !hasInteractiveStdin
}