|
34 | 34 |
|
35 | 35 | const COMMAND_OUTPUT_LIMIT = 10_000 |
36 | 36 | const promptIdentifier = '@36261@' |
37 | | -const cwdIdentifier = '@76593@' |
38 | 37 |
|
39 | 38 | type PersistentProcess = |
40 | 39 | | { |
@@ -340,122 +339,8 @@ export const runTerminalCommand = async ( |
340 | 339 |
|
341 | 340 | const echoLinePattern = new RegExp(`${promptIdentifier}[^\n]*\n`, 'g') |
342 | 341 | const commandDonePattern = new RegExp( |
343 | | - `^${cwdIdentifier}(.*)${cwdIdentifier}[\\s\\S]*${promptIdentifier}` |
| 342 | + `^${promptIdentifier}(.*)${promptIdentifier}[\\s\\S]*${promptIdentifier}` |
344 | 343 | ) |
345 | | -/** |
346 | | - * Executes a single command in a PTY process and returns the result when complete. |
347 | | - * |
348 | | - * This function handles the low-level details of running a command in a pseudo-terminal, |
349 | | - * including parsing the output to separate command echoes from actual output, detecting |
350 | | - * command completion, and extracting the exit code and final working directory. |
351 | | - * |
352 | | - * @param ptyProcess - The IPty instance to execute the command in |
353 | | - * @param command - The shell command to execute |
354 | | - * @param cwd - The working directory to change to before executing the command (relative to project root) |
355 | | - * @param onChunk - Callback function called for each chunk of output as it's received |
356 | | - * |
357 | | - * @returns Promise that resolves with: |
358 | | - * - `commandOutput`: The complete output from the command (excluding echo lines) |
359 | | - * - `finalCwd`: The working directory after command execution |
360 | | - * - `exitCode`: The command's exit code (0 for success, non-zero for failure, null if os is Windows) |
361 | | - * |
362 | | - * @example |
363 | | - * ```typescript |
364 | | - * const result = await runSinglePtyCommand( |
365 | | - * ptyProcess, |
366 | | - * 'ls -la', |
367 | | - * '.', |
368 | | - * (chunk) => console.log('Output chunk:', chunk) |
369 | | - * ); |
370 | | - * console.log('Exit code:', result.exitCode); |
371 | | - * console.log('Final directory:', result.finalCwd); |
372 | | - * ``` |
373 | | - * |
374 | | - * @internal This is a low-level utility function used by other terminal command runners. |
375 | | - * It handles platform-specific differences between Windows and Unix-like systems. |
376 | | - * |
377 | | - * The function works by: |
378 | | - * 1. Setting up a data listener on the PTY process |
379 | | - * 2. Filtering out command echo lines (the command being typed) |
380 | | - * 3. Detecting command completion markers (cwdIdentifier and promptIdentifier) |
381 | | - * 4. Parsing exit codes from the shell's status messages |
382 | | - * 5. Extracting the final working directory from the output |
383 | | - */ |
384 | | -function runSinglePtyCommand( |
385 | | - ptyProcess: IPty, |
386 | | - command: string, |
387 | | - cwd: string, |
388 | | - onChunk: (data: string) => void |
389 | | -): Promise<{ |
390 | | - commandOutput: string |
391 | | - finalCwd: string |
392 | | - exitCode: number | null |
393 | | -}> { |
394 | | - const isWindows = os.platform() === 'win32' |
395 | | - let commandOutput = '' |
396 | | - let buffer = promptIdentifier |
397 | | - let echoLinesRemaining = isWindows ? 1 : command.split('\n').length |
398 | | - |
399 | | - const resultPromise = new Promise<{ |
400 | | - commandOutput: string |
401 | | - finalCwd: string |
402 | | - exitCode: number | null |
403 | | - }>((resolve) => { |
404 | | - const dataDisposable = ptyProcess.onData((data: string) => { |
405 | | - buffer += data |
406 | | - const suffix = suffixPrefixOverlap(buffer, promptIdentifier) |
407 | | - let toProcess = buffer.slice(0, buffer.length - suffix.length) |
408 | | - buffer = suffix |
409 | | - |
410 | | - const matches = toProcess.match(echoLinePattern) |
411 | | - if (matches) { |
412 | | - for (let i = 0; i < matches.length && echoLinesRemaining > 0; i++) { |
413 | | - echoLinesRemaining = Math.max(echoLinesRemaining - 1, 0) |
414 | | - // Process normal output line |
415 | | - toProcess = toProcess.replace(echoLinePattern, '') |
416 | | - } |
417 | | - } |
418 | | - |
419 | | - const indexOfPromptIdentifier = toProcess.indexOf(cwdIdentifier) |
420 | | - if (indexOfPromptIdentifier !== -1) { |
421 | | - buffer = toProcess.slice(indexOfPromptIdentifier) + buffer |
422 | | - toProcess = toProcess.slice(0, indexOfPromptIdentifier) |
423 | | - } |
424 | | - |
425 | | - onChunk(toProcess) |
426 | | - commandOutput += toProcess |
427 | | - |
428 | | - const commandDone = buffer.match(commandDonePattern) |
429 | | - if (commandDone && echoLinesRemaining === 0) { |
430 | | - // Command is done |
431 | | - dataDisposable.dispose() |
432 | | - |
433 | | - const exitCode = buffer.includes('Command completed') |
434 | | - ? 0 |
435 | | - : (() => { |
436 | | - const match = buffer.match( |
437 | | - /Command failed with exit code (\d+)\./ |
438 | | - ) |
439 | | - return match ? parseInt(match[1]) : null |
440 | | - })() |
441 | | - |
442 | | - const newWorkingDirectory = commandDone[1] |
443 | | - |
444 | | - resolve({ commandOutput, finalCwd: newWorkingDirectory, exitCode }) |
445 | | - } |
446 | | - }) |
447 | | - }) |
448 | | - |
449 | | - // Write the command |
450 | | - const cdCommand = `cd ${path.resolve(getProjectRoot(), cwd)}` |
451 | | - const commandWithCheck = isWindows |
452 | | - ? `${cdCommand} & ${command} & echo ${cwdIdentifier}%cd%${cwdIdentifier}` |
453 | | - : `${cdCommand}; ${command}; ec=$?; printf "${cwdIdentifier}$(pwd)${cwdIdentifier}"; if [ $ec -eq 0 ]; then printf "Command completed."; else printf "Command failed with exit code $ec."; fi` |
454 | | - ptyProcess.write(`${commandWithCheck}\r\n`) |
455 | | - |
456 | | - return resultPromise |
457 | | -} |
458 | | - |
459 | 344 | export const runCommandPty = ( |
460 | 345 | persistentProcess: PersistentProcess & { |
461 | 346 | type: 'pty' |
@@ -519,82 +404,114 @@ export const runCommandPty = ( |
519 | 404 |
|
520 | 405 | persistentProcess.timerId = timer |
521 | 406 |
|
522 | | - runSinglePtyCommand(ptyProcess, command, cwd, (data: string) => { |
523 | | - commandOutput += data |
524 | | - process.stdout.write(data) |
525 | | - }).then(async ({ finalCwd: newWorkingDirectory, exitCode }) => { |
526 | | - const statusMessage = |
527 | | - exitCode === null |
528 | | - ? '' |
529 | | - : exitCode === 0 |
530 | | - ? 'Complete' |
531 | | - : `Failed with exit code: ${exitCode}` |
| 407 | + const dataDisposable = ptyProcess.onData((data: string) => { |
| 408 | + buffer += data |
| 409 | + const suffix = suffixPrefixOverlap(buffer, promptIdentifier) |
| 410 | + let toProcess = buffer.slice(0, buffer.length - suffix.length) |
| 411 | + buffer = suffix |
532 | 412 |
|
533 | | - if (timer) { |
534 | | - clearTimeout(timer) |
| 413 | + const matches = toProcess.match(echoLinePattern) |
| 414 | + if (matches) { |
| 415 | + for (let i = 0; i < matches.length && echoLinesRemaining > 0; i++) { |
| 416 | + echoLinesRemaining = Math.max(echoLinesRemaining - 1, 0) |
| 417 | + // Process normal output line |
| 418 | + toProcess = toProcess.replace(echoLinePattern, '') |
| 419 | + } |
535 | 420 | } |
536 | | - if (mode === 'assistant') { |
| 421 | + |
| 422 | + const indexOfPromptIdentifier = toProcess.indexOf(promptIdentifier) |
| 423 | + if (indexOfPromptIdentifier !== -1) { |
| 424 | + buffer = toProcess.slice(indexOfPromptIdentifier) + buffer |
| 425 | + toProcess = toProcess.slice(0, indexOfPromptIdentifier) |
| 426 | + } |
| 427 | + |
| 428 | + process.stdout.write(toProcess) |
| 429 | + commandOutput += toProcess |
| 430 | + |
| 431 | + const commandDone = buffer.match(commandDonePattern) |
| 432 | + if (commandDone && echoLinesRemaining === 0) { |
| 433 | + // Command is done |
| 434 | + if (timer) { |
| 435 | + clearTimeout(timer) |
| 436 | + } |
| 437 | + dataDisposable.dispose() |
| 438 | + |
| 439 | + const exitCode = buffer.includes('Command completed') |
| 440 | + ? 0 |
| 441 | + : (() => { |
| 442 | + const match = buffer.match(/Command failed with exit code (\d+)\./) |
| 443 | + return match ? parseInt(match[1]) : null |
| 444 | + })() |
| 445 | + const statusMessage = buffer.includes('Command completed') |
| 446 | + ? 'Complete' |
| 447 | + : `Failed with exit code: ${exitCode}` |
| 448 | + |
| 449 | + const newWorkingDirectory = commandDone[1] |
| 450 | + if (mode === 'assistant') { |
| 451 | + ptyProcess.write(`cd ${getWorkingDirectory()}\r\n`) |
| 452 | + |
| 453 | + resolve({ |
| 454 | + result: formatResult( |
| 455 | + command, |
| 456 | + commandOutput, |
| 457 | + `cwd: ${path.resolve(projectRoot, cwd)}\n\n${statusMessage}` |
| 458 | + ), |
| 459 | + stdout: commandOutput, |
| 460 | + exitCode, |
| 461 | + }) |
| 462 | + return |
| 463 | + } |
| 464 | + |
| 465 | + let outsideProject = false |
| 466 | + const currentWorkingDirectory = getWorkingDirectory() |
| 467 | + let finalCwd = currentWorkingDirectory |
| 468 | + if (newWorkingDirectory !== currentWorkingDirectory) { |
| 469 | + trackEvent(AnalyticsEvent.CHANGE_DIRECTORY, { |
| 470 | + from: currentWorkingDirectory, |
| 471 | + to: newWorkingDirectory, |
| 472 | + isSubdir: isSubdir(currentWorkingDirectory, newWorkingDirectory), |
| 473 | + }) |
| 474 | + if (path.relative(projectRoot, newWorkingDirectory).startsWith('..')) { |
| 475 | + outsideProject = true |
| 476 | + console.log(` |
| 477 | +Unable to cd outside of the project root (${projectRoot}) |
| 478 | + |
| 479 | +If you want to change the project root: |
| 480 | +1. Exit Codebuff (type "exit") |
| 481 | +2. Navigate into the target directory (type "cd ${newWorkingDirectory}") |
| 482 | +3. Restart Codebuff`) |
| 483 | + ptyProcess.write(`cd ${currentWorkingDirectory}\r\n`) |
| 484 | + } else { |
| 485 | + setWorkingDirectory(newWorkingDirectory) |
| 486 | + finalCwd = newWorkingDirectory |
| 487 | + } |
| 488 | + } |
| 489 | + |
537 | 490 | resolve({ |
538 | 491 | result: formatResult( |
539 | 492 | command, |
540 | 493 | commandOutput, |
541 | 494 | buildArray([ |
542 | | - `cwd: ${path.resolve(projectRoot, cwd)}`, |
543 | | - statusMessage, |
544 | | - ]).join('\n\n') |
| 495 | + `cwd: ${currentWorkingDirectory}`, |
| 496 | + `${statusMessage}\n`, |
| 497 | + outsideProject && |
| 498 | + `Detected final cwd outside project root. Reset cwd to ${currentWorkingDirectory}`, |
| 499 | + `Final **user** cwd: ${finalCwd} (Assistant's cwd is still project root)`, |
| 500 | + ]).join('\n') |
545 | 501 | ), |
546 | 502 | stdout: commandOutput, |
547 | 503 | exitCode, |
548 | 504 | }) |
| 505 | + return |
549 | 506 | } |
550 | | - let outsideProject = false |
551 | | - const currentWorkingDirectory = getWorkingDirectory() |
552 | | - let finalCwd = currentWorkingDirectory |
553 | | - if (newWorkingDirectory !== currentWorkingDirectory) { |
554 | | - trackEvent(AnalyticsEvent.CHANGE_DIRECTORY, { |
555 | | - from: currentWorkingDirectory, |
556 | | - to: newWorkingDirectory, |
557 | | - isSubdir: isSubdir(currentWorkingDirectory, newWorkingDirectory), |
558 | | - }) |
559 | | - if (path.relative(projectRoot, newWorkingDirectory).startsWith('..')) { |
560 | | - outsideProject = true |
561 | | - console.log(` |
562 | | -Unable to cd outside of the project root (${projectRoot}) |
563 | | - |
564 | | -If you want to change the project root: |
565 | | -1. Exit Codebuff (type "exit") |
566 | | -2. Navigate into the target directory (type "cd ${newWorkingDirectory}") |
567 | | -3. Restart Codebuff`) |
568 | | - await runSinglePtyCommand( |
569 | | - ptyProcess, |
570 | | - `cd ${currentWorkingDirectory}`, |
571 | | - '.', |
572 | | - () => {} |
573 | | - ) |
574 | | - } else { |
575 | | - setWorkingDirectory(newWorkingDirectory) |
576 | | - finalCwd = newWorkingDirectory |
577 | | - } |
578 | | - } |
579 | | - |
580 | | - resolve({ |
581 | | - result: formatResult( |
582 | | - command, |
583 | | - commandOutput, |
584 | | - buildArray([ |
585 | | - `cwd: ${currentWorkingDirectory}`, |
586 | | - `${statusMessage}\n`, |
587 | | - outsideProject && |
588 | | - `Detected final cwd outside project root. Reset cwd to ${currentWorkingDirectory}`, |
589 | | - `Final **user** cwd: ${finalCwd} (Assistant's cwd is still project root)`, |
590 | | - ]).join('\n') |
591 | | - ), |
592 | | - stdout: commandOutput, |
593 | | - exitCode, |
594 | | - }) |
595 | 507 | }) |
596 | 508 |
|
597 | | - return |
| 509 | + // Write the command |
| 510 | + const cdCommand = `cd ${path.resolve(projectRoot, cwd)}` |
| 511 | + const commandWithCheck = isWindows |
| 512 | + ? `${cdCommand} & ${command} & echo ${promptIdentifier}%cd%${promptIdentifier}` |
| 513 | + : `${cdCommand}; ${command}; ec=$?; printf "${promptIdentifier}$(pwd)${promptIdentifier}"; if [ $ec -eq 0 ]; then printf "Command completed."; else printf "Command failed with exit code $ec."; fi` |
| 514 | + ptyProcess.write(`${commandWithCheck}\r`) |
598 | 515 | } |
599 | 516 |
|
600 | 517 | const runCommandChildProcess = ( |
|
0 commit comments