Setting the file. One moment.
Screen · N8n:testmax N8n AI · n8n-io/n8n · Skills Docs
ContentsBack to the top of the page sandbox/screen.mjs
sandbox/ screen.mjs
JavaScript · 100 lines · 4 KB
{ createWriteStream, existsSync, readFileSync, writeFileSync }
from
'node:fs'
;
13 import { join } from 'node:path' ;
14
15 const [ mode , dir ] = process.argv. slice ( 2 );
16 const command = JSON . parse ( readFileSync ( join (dir, 'command.json' ), 'utf8' ));
17 const READY = '/tmp/screen-ready' ;
18 const sleep = ( ms ) => new Promise (( r ) => setTimeout (r, ms));
19 const tmux = ( ... args ) => spawnSync ( 'tmux' , args, { stdio: 'inherit' }).status;
20 const screenText = () =>
21 spawnSync ( 'tmux' , [ 'capture-pane' , '-p' , '-t' , 'run' ], { encoding: 'utf8' }).stdout ?? '' ;
22
23 // The CLIs connect MCP servers in the background. A prompt on the command line can start
24 // before the n8n tools exist, so the prompt is pasted when the input box is ready.
25 async function typePrompt ({ input , readyText , settleMs = 0 }) {
26 const deadline = Date. now () + 60_000 ;
27 while (readyText && ! screenText (). includes (readyText) && Date. now () < deadline) await sleep ( 250 );
28 await sleep (settleMs);
29 tmux ( 'set-buffer' , '-b' , 'prompt' , '--' , input);
30 tmux ( 'paste-buffer' , '-p' , '-d' , '-b' , 'prompt' , '-t' , 'run' );
31 await sleep ( 500 );
32 tmux ( 'send-keys' , '-t' , 'run' , 'Enter' );
33 }
34
35 if (mode === 'record' ) {
36 const out = createWriteStream ( join (dir, 'screen.cast' ));
37 out. write (
38 JSON . stringify ({
39 version: 2 ,
40 width: command.cols,
41 height: command.rows,
42 timestamp: Math. floor (Date. now () / 1000 ),
43 env: { TERM: 'xterm-256color' },
44 }) + ' \n ' ,
45 );
46 const start = process.hrtime. bigint ();
47 const decoder = new TextDecoder ( 'utf-8' );
48 process.stdin. on ( 'data' , ( chunk ) => {
49 const text = decoder. decode (chunk, { stream: true });
50 if ( ! text) return ;
51 const t = Number (process.hrtime. bigint () - start) / 1e9 ;
52 out. write ( JSON . stringify ([ Number (t. toFixed ( 6 )), 'o' , text]) + ' \n ' );
53 });
54 process.stdin. on ( 'end' , () => out. end ());
55 } else if (mode === 'launch' ) {
56 while ( ! existsSync ( READY )) await sleep ( 50 );
57 const [ bin , ... args ] = command.argv;
58 const child = spawn (bin, args, { stdio: 'inherit' });
59 child. on ( 'exit' , ( code ) => {
60 writeFileSync ( join (dir, 'exited' ), String (code ?? 1 ));
61 process. exit (code ?? 1 );
62 });
63 // A CLI that cannot start emits error, not exit. Without the marker, the run waits for ever.
64 child. on ( 'error' , ( error ) => {
65 console. error ( `Cannot start ${ bin }: ${ error . message }` );
66 writeFileSync ( join (dir, 'exited' ), '127' );
67 process. exit ( 127 );
68 });
69 } else if (mode === 'run' ) {
70 const self = new URL ( import . meta .url).pathname;
71 // mouse and focus-events stop the tmux tips that Claude Code shows in the recorded screen.
72 writeFileSync (
73 '/tmp/tmux.conf' ,
74 'set -g status off \n set -g default-terminal xterm-256color \n set -g mouse on \n set -g focus-events on \n ' ,
75 );
76 tmux (
77 '-f' ,
78 '/tmp/tmux.conf' ,
79 'new-session' ,
80 '-d' ,
81 '-s' ,
82 'run' ,
83 '-x' ,
84 String (command.cols),
85 '-y' ,
86 String (command.rows),
87 `node ${ self } launch ${ dir }` ,
88 );
89 // A watcher in another terminal size must not resize the recorded screen. tmux 3.5 stops
90 // when this option is in the startup config, so it is set after the session starts.
91 tmux ( 'set-option' , '-g' , 'window-size' , 'manual' );
92 tmux ( 'pipe-pane' , '-O' , '-t' , 'run' , `node ${ self } record ${ dir }` );
93 writeFileSync ( READY , '' );
94 if (command.input) await typePrompt (command);
95 while ( ! existsSync ( join (dir, 'stop' )) && ! existsSync ( join (dir, 'exited' ))) await sleep ( 500 );
96 // Keep the final screen in the recording for a moment.
97 await sleep ( 2500 );
98 tmux ( 'kill-server' );
99 await sleep ( 500 );
100 }