33'use strict' ;
44
55const fs = require ( 'node:fs' ) ;
6+ const os = require ( 'node:os' ) ;
67const path = require ( 'node:path' ) ;
78const { spawn } = require ( 'node:child_process' ) ;
89
@@ -73,9 +74,9 @@ function runNpm(args, options = {}) {
7374 } ) ;
7475}
7576
76- async function ensureBackendBuild ( ) {
77- if ( fs . existsSync ( BACKEND_ENTRY ) ) return ;
78- log ( 'Backend dist not found, building TypeScript backend...' ) ;
77+ /** 每次启动均编译后端,避免沿用旧的 server/dist */
78+ async function buildBackendLatest ( ) {
79+ log ( 'Building TypeScript backend (latest sources) ...' ) ;
7980 await runNpm ( [ 'run' , 'build' ] ) ;
8081}
8182
@@ -133,12 +134,34 @@ async function stopProcess(proc) {
133134 }
134135}
135136
137+ /**
138+ * IDE 集成终端通常无真实 TTY,Ink 无法在同一进程内运行。在 Windows 上通过「新控制台窗口」启动 TUI。
139+ */
140+ function launchTuiInNewWindowsConsole ( baseUrl ) {
141+ const batPath = path . join ( os . tmpdir ( ) , `secbot-tui-${ process . pid } -${ Date . now ( ) } .bat` ) ;
142+ const safeUrl = baseUrl . replace ( / " / g, '' ) . replace ( / \r ? \n / g, '' ) ;
143+ const safeDir = TUI_DIR . replace ( / " / g, '""' ) ;
144+ const body = [
145+ '@echo off' ,
146+ `set "SECBOT_API_URL=${ safeUrl } "` ,
147+ `cd /d "${ safeDir } "` ,
148+ 'call npm.cmd run tui' ,
149+ ] . join ( '\r\n' ) ;
150+ fs . writeFileSync ( batPath , `${ body } \r\n` , 'utf8' ) ;
151+
152+ const child = spawn ( 'cmd.exe' , [ '/c' , 'start' , 'Secbot TUI' , batPath ] , {
153+ cwd : ROOT ,
154+ stdio : 'ignore' ,
155+ windowsHide : false ,
156+ detached : true ,
157+ } ) ;
158+ child . unref ( ) ;
159+ }
160+
136161async function main ( ) {
137- if ( ! process . stdin . isTTY || ! process . stdout . isTTY ) {
138- log ( 'Current terminal has no real TTY; open CMD/PowerShell/Windows Terminal to run TUI.' ) ;
139- }
162+ const hasTTY = Boolean ( process . stdin . isTTY && process . stdout . isTTY ) ;
140163
141- await ensureBackendBuild ( ) ;
164+ await buildBackendLatest ( ) ;
142165 await ensureTuiDeps ( ) ;
143166
144167 const port = String ( process . env . PORT || 8000 ) ;
@@ -174,25 +197,43 @@ async function main() {
174197 log ( `Using existing backend at ${ baseUrl } ` ) ;
175198 }
176199
177- log ( 'Starting terminal TUI...' ) ;
178- const tuiNpm = npmInvocation ( [ 'run' , 'tui' ] ) ;
179- const tuiProc = spawn ( tuiNpm . cmd , tuiNpm . argv , {
180- cwd : TUI_DIR ,
181- env : tuiEnv ,
182- stdio : 'inherit' ,
183- shell : tuiNpm . shell ,
184- windowsHide : true ,
185- } ) ;
186-
187200 const shutdown = async ( ) => {
188201 if ( startedBackend ) {
189202 await stopProcess ( backendProc ) ;
190203 }
191204 } ;
192205
206+ let tuiProc = null ;
207+ let tuiInSameTerminal = true ;
208+
209+ if ( ! hasTTY && process . platform === 'win32' ) {
210+ log ( '当前无真实 TTY(常见于 Cursor/VS Code 集成终端),将在新控制台窗口中启动 TUI。' ) ;
211+ log ( 'Starting terminal TUI in a new window...' ) ;
212+ launchTuiInNewWindowsConsole ( baseUrl ) ;
213+ log ( 'TUI 已在新窗口启动;本终端将保持后端运行,按 Ctrl+C 可停止后端。' ) ;
214+ tuiInSameTerminal = false ;
215+ } else if ( ! hasTTY ) {
216+ log ( '当前终端无真实 TTY,无法在此进程内启动 Ink TUI。' ) ;
217+ log ( '请在系统终端中执行:npm run start:stack,或 Windows 下双击 scripts\\start-cli.bat' ) ;
218+ await shutdown ( ) ;
219+ process . exit ( 1 ) ;
220+ } else {
221+ log ( 'Starting terminal TUI...' ) ;
222+ const tuiNpm = npmInvocation ( [ 'run' , 'tui' ] ) ;
223+ tuiProc = spawn ( tuiNpm . cmd , tuiNpm . argv , {
224+ cwd : TUI_DIR ,
225+ env : tuiEnv ,
226+ stdio : 'inherit' ,
227+ shell : tuiNpm . shell ,
228+ windowsHide : true ,
229+ } ) ;
230+ }
231+
193232 process . on ( 'SIGINT' , async ( ) => {
194233 try {
195- if ( tuiProc . exitCode === null ) tuiProc . kill ( 'SIGINT' ) ;
234+ if ( tuiInSameTerminal && tuiProc && tuiProc . exitCode === null ) {
235+ tuiProc . kill ( 'SIGINT' ) ;
236+ }
196237 await shutdown ( ) ;
197238 } finally {
198239 process . exit ( 130 ) ;
@@ -201,13 +242,21 @@ async function main() {
201242
202243 process . on ( 'SIGTERM' , async ( ) => {
203244 try {
204- if ( tuiProc . exitCode === null ) tuiProc . kill ( 'SIGTERM' ) ;
245+ if ( tuiInSameTerminal && tuiProc && tuiProc . exitCode === null ) {
246+ tuiProc . kill ( 'SIGTERM' ) ;
247+ }
205248 await shutdown ( ) ;
206249 } finally {
207250 process . exit ( 143 ) ;
208251 }
209252 } ) ;
210253
254+ if ( ! tuiInSameTerminal ) {
255+ // 后端子进程已启动时,本进程需保持运行直至用户 Ctrl+C(见 SIGINT);否则仅新开 TUI 窗口时也可仅靠子进程存活
256+ await new Promise ( ( ) => { } ) ;
257+ return ;
258+ }
259+
211260 const tuiCode = await new Promise ( ( resolve , reject ) => {
212261 tuiProc . once ( 'error' , reject ) ;
213262 tuiProc . once ( 'close' , ( code ) => resolve ( code ?? 0 ) ) ;
0 commit comments