11import * as readline from "node:readline" ;
2- import { stdin , stdout } from "node:process" ;
2+ import { stdin , stdout , stderr } from "node:process" ;
33import { readFileSync , writeFileSync , mkdirSync , existsSync } from "node:fs" ;
44import { join , dirname } from "node:path" ;
55import type { Command } from "commander" ;
@@ -12,11 +12,11 @@ import {
1212 setBrandedHelpSuppressed ,
1313} from "../lib/ui.js" ;
1414import { isJSONMode } from "../lib/output.js" ;
15- import { ScrollbackBuffer } from "../lib/scrollback.js" ;
1615import { setReplMode } from "../index.js" ;
1716import { getRPCNetworkIds } from "../lib/networks.js" ;
1817import { configDir , load as loadConfig } from "../lib/config.js" ;
1918import { getSetupMethod } from "../lib/onboarding.js" ;
19+ import { bgRgb , rgb , noColor } from "../lib/colors.js" ;
2020
2121const COMMAND_NAMES = [
2222 "apps" ,
@@ -205,6 +205,7 @@ export async function startREPL(program: Command): Promise<void> {
205205 const printIntro = ( ) : void => {
206206 if ( isJSONMode ( ) ) return ;
207207 process . stdout . write ( brandedHelp ( { force : true } ) ) ;
208+ console . log ( "" ) ;
208209 console . log ( ` ${ brand ( "◆" ) } ${ bold ( "Welcome to Alchemy CLI" ) } ` ) ;
209210 console . log ( ` ${ green ( "✓" ) } ${ dim ( `Configured auth: ${ formatSetupMethodLabel ( ) } ` ) } ` ) ;
210211 console . log ( ` ${ dim ( "Run commands directly (no 'alchemy' prefix)." ) } ` ) ;
@@ -217,11 +218,57 @@ export async function startREPL(program: Command): Promise<void> {
217218 console . log ( "" ) ;
218219 console . log ( ` ${ dim ( "Press TAB for autocomplete. Type 'exit' or 'quit' to leave." ) } ` ) ;
219220 console . log ( "" ) ;
221+ console . log ( "" ) ;
222+ } ;
223+
224+ const PROMPT_STR = "\x1b[38;2;54;63;249m›\x1b[39m " ;
225+ const submittedCommandBg = bgRgb ( 64 , 64 , 68 ) ;
226+ const submittedCommandFg = rgb ( 232 , 232 , 236 ) ;
227+ const OUTPUT_INDENT = " " ;
228+ const styleSubmittedCommand = ( command : string ) : string => {
229+ if ( ! stdout . isTTY || noColor ) return command ;
230+ return submittedCommandBg ( submittedCommandFg ( ` ${ command } ` ) ) ;
220231 } ;
232+ const runWithIndentedOutput = async ( fn : ( ) => Promise < void > ) : Promise < void > => {
233+ if ( isJSONMode ( ) || ! stdout . isTTY ) {
234+ await fn ( ) ;
235+ return ;
236+ }
237+
238+ const createIndentedWriter = ( orig : typeof stdout . write ) : typeof stdout . write => {
239+ let atLineStart = true ;
240+ return function ( chunk : Uint8Array | string , ...rest : unknown [ ] ) : boolean {
241+ const str = typeof chunk === "string" ? chunk : Buffer . from ( chunk ) . toString ( "utf8" ) ;
242+ if ( ! str ) return true ;
243+
244+ let out = "" ;
245+ for ( const ch of str ) {
246+ if ( atLineStart && ch !== "\n" && ch !== "\r" ) {
247+ out += OUTPUT_INDENT ;
248+ atLineStart = false ;
249+ }
250+ out += ch ;
251+ if ( ch === "\n" || ch === "\r" ) {
252+ atLineStart = true ;
253+ }
254+ }
221255
222- const scrollback = new ScrollbackBuffer ( ) ;
223- const PROMPT_STR = "alchemy \x1b[38;2;54;63;249m◆\x1b[39m " ;
224- scrollback . setPrompt ( PROMPT_STR ) ;
256+ return orig ( out , ...( rest as [ BufferEncoding , ( ) => void ] ) ) ;
257+ } as typeof stdout . write ;
258+ } ;
259+
260+ const origStdoutWrite = stdout . write . bind ( stdout ) ;
261+ const origStderrWrite = stderr . write . bind ( stderr ) ;
262+ stdout . write = createIndentedWriter ( origStdoutWrite ) ;
263+ stderr . write = createIndentedWriter ( origStderrWrite ) ;
264+
265+ try {
266+ await fn ( ) ;
267+ } finally {
268+ stdout . write = origStdoutWrite as typeof stdout . write ;
269+ stderr . write = origStderrWrite as typeof stderr . write ;
270+ }
271+ } ;
225272
226273 const rl = readline . createInterface ( {
227274 input : stdin ,
@@ -231,40 +278,6 @@ export async function startREPL(program: Command): Promise<void> {
231278 removeHistoryDuplicates : true ,
232279 } ) ;
233280
234- // ── Mouse-event stdin filtering ────────────────────────────────────
235- // Grab the data listener installed by readline/emitKeypressEvents and
236- // replace it with a wrapper that strips SGR mouse sequences. The
237- // original listener (the keypress parser) never sees mouse bytes.
238- const MOUSE_SEQ_RE = / \x1b \[ < ( \d + ) ; \d + ; \d + [ M m ] / g;
239- const dataListeners = stdin . listeners ( "data" ) as ( ( ...args : unknown [ ] ) => void ) [ ] ;
240- const origDataListener = dataListeners [ dataListeners . length - 1 ] ;
241- stdin . removeListener ( "data" , origDataListener ) ;
242-
243- const filteredDataListener = ( chunk : string | Buffer ) : void => {
244- const str = typeof chunk === "string" ? chunk : chunk . toString ( "utf8" ) ;
245- let match : RegExpExecArray | null ;
246- MOUSE_SEQ_RE . lastIndex = 0 ;
247- while ( ( match = MOUSE_SEQ_RE . exec ( str ) ) !== null ) {
248- const button = parseInt ( match [ 1 ] , 10 ) ;
249- if ( button === 64 ) {
250- scrollback . scrollUp ( ) ;
251- } else if ( button === 65 ) {
252- scrollback . scrollDown ( ) ;
253- if ( ! scrollback . isScrolled ) {
254- // Reached the bottom via mouse scroll — readline doesn't know the
255- // screen was redrawn, so force it to redraw the prompt.
256- // eslint-disable-next-line @typescript-eslint/no-explicit-any
257- const r = rl as any ;
258- if ( r . prevRows !== undefined ) r . prevRows = 0 ;
259- if ( r . _refreshLine ) r . _refreshLine ( ) ;
260- }
261- }
262- }
263- const cleaned = str . replace ( MOUSE_SEQ_RE , "" ) ;
264- if ( cleaned ) origDataListener ( cleaned ) ;
265- } ;
266- stdin . on ( "data" , filteredDataListener ) ;
267-
268281 // ── History & inline suggestion ────────────────────────────────────
269282 const initialHistory = loadReplHistory ( ) ;
270283 const rlWithHistory = rl as readline . Interface & { history ?: string [ ] } ;
@@ -287,15 +300,6 @@ export async function startREPL(program: Command): Promise<void> {
287300 readline . moveCursor ( stdout , - suggestion . length , 0 ) ;
288301 } ;
289302
290- const rlWithRefresh = rl as readline . Interface & { _refreshLine ?: ( ) => void } ;
291- const originalRefreshLine = rlWithRefresh . _refreshLine ?. bind ( rl ) ;
292- if ( originalRefreshLine ) {
293- rlWithRefresh . _refreshLine = ( ) : void => {
294- originalRefreshLine ( ) ;
295- renderInlineSuggestion ( ) ;
296- } ;
297- }
298-
299303 const acceptInlineCompletion = ( ) : void => {
300304 const line = rl . line ;
301305 const cursor = typeof rl . cursor === "number" ? rl . cursor : line . length ;
@@ -325,14 +329,13 @@ export async function startREPL(program: Command): Promise<void> {
325329
326330 if ( originalTTYWrite ) {
327331 rlWithTTYWrite . _ttyWrite = ( s : string , key : readline . Key ) : void => {
332+ if ( key ?. name === "return" && ! rl . line . trim ( ) ) {
333+ return ;
334+ }
328335 if ( key ?. name === "tab" ) {
329336 acceptInlineCompletion ( ) ;
330337 return ;
331338 }
332- if ( scrollback . isScrolled ) {
333- scrollback . snapToBottom ( ) ;
334- if ( rlWithRefresh . _refreshLine ) rlWithRefresh . _refreshLine ( ) ;
335- }
336339 originalTTYWrite ( s , key ) ;
337340 } ;
338341 }
@@ -359,6 +362,12 @@ export async function startREPL(program: Command): Promise<void> {
359362 rl . prompt ( ) ;
360363 } ;
361364
365+ const printPostOutputSpacing = ( ) : void => {
366+ if ( ! isJSONMode ( ) && stdout . isTTY ) {
367+ console . log ( "" ) ;
368+ }
369+ } ;
370+
362371 const onLine = async ( line : string ) : Promise < void > => {
363372 const trimmed = line . trim ( ) ;
364373 if ( ! trimmed ) {
@@ -371,6 +380,13 @@ export async function startREPL(program: Command): Promise<void> {
371380 return ;
372381 }
373382
383+ if ( ! isJSONMode ( ) && stdout . isTTY ) {
384+ readline . moveCursor ( stdout , 0 , - 1 ) ;
385+ readline . clearLine ( stdout , 0 ) ;
386+ stdout . write ( styleSubmittedCommand ( trimmed ) + "\n" ) ;
387+ console . log ( "" ) ;
388+ }
389+
374390 const words = trimmed . split ( / \s + / ) ;
375391
376392 // Friendly REPL help shortcuts:
@@ -379,10 +395,13 @@ export async function startREPL(program: Command): Promise<void> {
379395 if ( words [ 0 ] === "help" ) {
380396 const target = words . slice ( 1 ) ;
381397 try {
382- await program . parseAsync ( [ "node" , "alchemy" , ...target , "--help" ] ) ;
398+ await runWithIndentedOutput ( async ( ) => {
399+ await program . parseAsync ( [ "node" , "alchemy" , ...target , "--help" ] ) ;
400+ } ) ;
383401 } catch {
384402 // Commander help/errors are already handled by exitOverride
385403 }
404+ printPostOutputSpacing ( ) ;
386405 prompt ( ) ;
387406 return ;
388407 }
@@ -395,11 +414,14 @@ export async function startREPL(program: Command): Promise<void> {
395414 }
396415
397416 try {
398- await program . parseAsync ( [ "node" , "alchemy" , ...words ] ) ;
417+ await runWithIndentedOutput ( async ( ) => {
418+ await program . parseAsync ( [ "node" , "alchemy" , ...words ] ) ;
419+ } ) ;
399420 } catch {
400421 // Commander errors are already handled by exitOverride
401422 }
402423
424+ printPostOutputSpacing ( ) ;
403425 prompt ( ) ;
404426 } ;
405427
@@ -412,9 +434,6 @@ export async function startREPL(program: Command): Promise<void> {
412434 setReplMode ( false ) ;
413435 setBrandedHelpSuppressed ( false ) ;
414436 stdin . off ( "keypress" , onKeypress ) ;
415- stdin . removeListener ( "data" , filteredDataListener ) ;
416- stdin . on ( "data" , origDataListener ) ;
417- scrollback . dispose ( ) ;
418437 resolve ( ) ;
419438 } ) ;
420439 printIntro ( ) ;
0 commit comments