Setting the file. One moment.
Replatform Console · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page — line 60
This file
Number 45.27
Position 27 of 182
Type JavaScript
Size 13 KB
Lines 146 scripts/ replatform-console.js
JavaScript · 146 lines · 13 KB
=
require
(
'node:crypto'
);
10 const { spawn } = require ( 'node:child_process' );
11
12 const ROOT = path. resolve (process.env. REPLATFORM_MIGRATIONS_DIR || path. join (process. cwd (), 'migrations' ));
13 const CLI = path. join (__dirname, 'replatform.js' );
14 const PUBLIC = path. join (__dirname, '..' , 'console' );
15 const token = crypto. randomBytes ( 32 ). toString ( 'base64url' );
16 const clients = new Map ();
17 const active = new Map ();
18 const MAX_BODY = 52 * 1024 * 1024 ;
19 const safeProject = ( value ) => typeof value === 'string' && / ^ [A-Za-z0-9][A-Za-z0-9_-] {0,99}$ / . test (value);
20 const projectPath = ( project ) => { if ( ! safeProject (project)) throw new Error ( 'invalid project name' ); return path. join ( ROOT , project); };
21 const cursorPath = ( project ) => path. join ( projectPath (project), 'state' , 'runner' , 'console-cursor.json' );
22
23 function parseArgs () {
24 const args = process.argv. slice ( 2 ); let port = 4310 ;
25 for ( let i = 0 ; i < args. length ; i += 1 ) {
26 if (args[i] === '--port' ) port = Number (args[ ++ i]);
27 else if (args[i] === '--host' && args[ ++ i] !== '127.0.0.1' ) throw new Error ( 'the console only binds to 127.0.0.1' );
28 else if (args[i] !== '--host' ) throw new Error ( `unknown option: ${ args [ i ] }` );
29 }
30 if ( ! Number. isInteger (port) || port < 1 || port > 65535 ) throw new Error ( 'port must be 1–65535' );
31 return port;
32 }
33 function send ( res , status , body ) { res. writeHead (status, { 'content-type' : 'application/json; charset=utf-8' , 'cache-control' : 'no-store' }); res. end ( JSON . stringify (body)); }
34 function originOK ( req ) { const origin = req.headers.origin; return ! origin || origin === `http://127.0.0.1:${ server . address (). port }` ; }
35 function authorized ( req , url , mutation = false ) { return url.searchParams. get ( 'token' ) === token && ( ! mutation || originOK (req)); }
36 async function body ( req ) {
37 const chunks = []; let size = 0 ;
38 for await ( const chunk of req) { size += chunk. length ; if (size > MAX_BODY ) throw new Error ( 'request body is too large' ); chunks. push (chunk); }
39 try { return JSON . parse (Buffer. concat (chunks). toString ( 'utf8' ) || '{}' ); } catch { throw new Error ( 'request body must be JSON' ); }
40 }
41 function invoke ( args , { onLine } = {}) {
42 return new Promise (( resolve ) => {
43 const child = spawn (process.execPath, [ CLI , ... args], { cwd: process. cwd (), env: process.env, stdio: [ 'ignore' , 'pipe' , 'pipe' ] });
44 let stdout = '' ; let stderr = '' ; let partial = '' ;
45 child.stdout. on ( 'data' , ( chunk ) => { stdout += chunk; partial += chunk; const lines = partial. split ( ' \n ' ); partial = lines. pop (); for ( const line of lines) if (line) onLine ?.(line); });
46 child.stderr. on ( 'data' , ( chunk ) => { stderr += chunk; });
47 child. once ( 'close' , ( code , signal ) => resolve ({ child, code, signal, stdout, stderr }));
48 });
49 }
50 async function json ( args ) {
51 const result = await invoke (args); const line = result.stdout. trim (). split ( ' \n ' ). filter (Boolean). at ( - 1 );
52 let value = null ; try { value = line ? JSON . parse (line) : null ; } catch { /* protocol failure is returned below */ }
53 if (result.code !== 0 ) throw new Error (value?.message || result.stderr. trim () || `CLI failed (${ result . code })` );
54 if ( ! value) throw new Error ( 'CLI returned no JSON result' ); return value;
55 }
56 function broadcast ( project , event ) {
57 if (event.eventId) fs. mkdir (path. dirname ( cursorPath (project)), { recursive: true }). then (() => fs. writeFile ( cursorPath (project), `${ JSON . stringify ({ schemaVersion: 1 , eventId: event.eventId , timestamp: event.timestamp || new Date (). toISOString () }) } \n ` , 'utf8' )). catch (() => {});
58 for ( const res of clients. get (project) || []) res. write ( `id: ${ event . eventId || ''} \n event: migration \n data: ${ JSON . stringify ( event ) } \n\n ` );
59 }
60 function launch ( project , args ) {
61 if (active. has (project)) throw new Error ( 'this project already has a console-owned run' );
62 const child = spawn (process.execPath, [ CLI , ... args], { cwd: process. cwd (), env: process.env, stdio: [ 'ignore' , 'pipe' , 'pipe' ] });
63 active. set (project, child); let partial = '' ; let stderr = '' ; let sawResult = false ;
64 child.stdout. on ( 'data' , ( chunk ) => { partial += chunk; const lines = partial. split ( ' \n ' ); partial = lines. pop (); for ( const line of lines) {
65 try { const event = JSON . parse (line); if (event.type === 'result' ) sawResult = true ; broadcast (project, event); } catch { broadcast (project, { type: 'protocol_error' , message: 'The CLI emitted invalid JSONL.' }); }
66 }});
67 child.stderr. on ( 'data' , ( chunk ) => { stderr += chunk; });
68 child. once ( 'close' , ( code ) => {
69 active. delete (project);
70 if (partial) try { const event = JSON . parse (partial); if (event.type === 'result' ) sawResult = true ; broadcast (project, event); } catch {}
71 if ( ! sawResult) broadcast (project, { schemaVersion: 1 , eventId: `evt_console_${ crypto . randomUUID () }` , timestamp: new Date (). toISOString (), type: 'result' , state: 'failed' , message: stderr. trim () || `CLI exited with ${ code }` , result: { schemaVersion: 1 , runId: null , outcome: 'failed' , nextAction: 'Inspect the console diagnostics and correct the reported input or local prerequisite.' } });
72 });
73 return child;
74 }
75 function runArgs ( project , options , resume = false ) {
76 const args = [resume ? 'resume' : 'run' , projectPath (project), '--format' , 'jsonl' ];
77 if (resume) return args;
78 const pick = [[ 'sourceUrl' , '--source-url' ], [ 'sourceMode' , '--source-mode' ], [ 'deliveryMode' , '--delivery-mode' ], [ 'destination' , '--destination' ], [ 'managementImportMode' , '--management-import-mode' ], [ 'agent' , '--agent' ], [ 'wallTimeout' , '--wall-timeout' ], [ 'idleTimeout' , '--idle-timeout' ], [ 'workerStartupTimeout' , '--worker-startup-timeout' ], [ 'statusInterval' , '--status-interval' ]];
79 for ( const [ key , flag ] of pick) if (options[key] !== undefined && options[key] !== '' ) args. push (flag, String (options[key]));
80 for ( const name of options.files || []) { if ( ! / ^ [A-Za-z0-9][A-Za-z0-9._-] {0,200}$ / . test (name)) throw new Error ( 'invalid uploaded file name' ); args. push ( '--file' , path. join ( projectPath (project), 'intake' , name)); }
81 if (options.oneClick) args. push ( '--one-click' ); if (options.dryRun) args. push ( '--dry-run' ); return args;
82 }
83 function scrubStatus ( value ) { if (value?.eventStream) delete value.eventStream.path; return value; }
84 function setupCredentialInstructions ( project , request ) {
85 if (request?.kind !== 'manual_action' || request.stage !== 'setup_provisioning' || request.response?.mode !== 'none' ) return null ;
86 return {
87 command: `cd ${ projectPath ( project ) } \n bash ./scripts/mint-token.sh` ,
88 confirmation: 'Wait for the success message confirming that WIX_AUTH_TOKEN was written. The token itself must not be displayed.' ,
89 };
90 }
91 async function replay ( project , cursor , res ) {
92 const args = [ 'tail' , projectPath (project), '--format' , 'jsonl' ]; if (cursor) args. push ( '--since' , cursor);
93 const result = await invoke (args); for ( const line of result.stdout. trim (). split ( ' \n ' ). filter (Boolean)) { try { const event = JSON . parse (line); res. write ( `id: ${ event . eventId || ''} \n event: migration \n data: ${ JSON . stringify ( event ) } \n\n ` ); } catch {} }
94 }
95 async function staticFile ( res , name ) {
96 const map = { '/' : 'index.html' , '/app.js' : 'app.js' , '/app.css' : 'app.css' , '/json.css' : 'json.css' , '/account.css' : 'account.css' }; const file = map[name];
97 if ( ! file) return false ; const content = await fs. readFile (path. join ( PUBLIC , file));
98 res. writeHead ( 200 , { 'content-type' : file. endsWith ( '.js' ) ? 'text/javascript; charset=utf-8' : file. endsWith ( '.css' ) ? 'text/css; charset=utf-8' : 'text/html; charset=utf-8' , 'cache-control' : 'no-store' }); res. end (content); return true ;
99 }
100 const server = http. createServer ( async ( req , res ) => {
101 const url = new URL (req.url, `http://${ req . headers . host }` ); const mutation = [ 'POST' , 'PUT' , 'DELETE' ]. includes (req.method);
102 try {
103 // Static assets contain no state or secret. The shell itself remains token-gated.
104 if (req.method === 'GET' && (url.pathname === '/app.js' || url.pathname === '/app.css' || url.pathname === '/json.css' || url.pathname === '/account.css' )) return staticFile (res, url.pathname);
105 if ( ! authorized (req, url, mutation)) return send (res, 403 , { error: 'Local console token or origin was rejected.' });
106 if (req.method === 'GET' && ! url.pathname. startsWith ( '/api/' )) { if ( await staticFile (res, url.pathname)) return ; return send (res, 404 , { error: 'Not found' }); }
107 if (req.method === 'GET' && url.pathname === '/api/projects' ) return send (res, 200 , await json ([ 'projects' , '--format' , 'json' ]));
108 // This transient, token-gated response must never enter migration artifacts or event logs.
109 if (req.method === 'GET' && url.pathname === '/api/account' ) return send (res, 200 , await json ([ 'account' , '--format' , 'json' ]));
110 const project = url.searchParams. get ( 'project' );
111 if (req.method === 'GET' && url.pathname === '/api/status' ) return send (res, 200 , scrubStatus ( await json ([ 'status' , projectPath (project), '--format' , 'json' ])));
112 if (req.method === 'GET' && url.pathname === '/api/tasks' ) return send (res, 200 , await json ([ 'tasks' , projectPath (project), '--format' , 'json' ]));
113 if (req.method === 'GET' && url.pathname === '/api/doctor' ) return send (res, 200 , await json ([ 'doctor' , '--agent' , url.searchParams. get ( 'agent' ) || 'auto' , '--format' , 'json' ]));
114 if (req.method === 'GET' && url.pathname === '/api/requests' ) return send (res, 200 , await json ([ 'requests' , projectPath (project), '--format' , 'json' ]));
115 if (req.method === 'GET' && url.pathname === '/api/show' ) {
116 const item = url.searchParams. get ( 'item' ); const cursor = url.searchParams. get ( 'cursor' ) || '0' ;
117 const value = await json ([ 'show' , projectPath (project), item, '--cursor' , cursor, '--max-bytes' , '262144' , '--format' , 'json' ]);
118 const instructions = / ^ uar_ [0-9a-f-] +$ / . test (item || '' ) ? setupCredentialInstructions (project, value) : null ;
119 return send (res, 200 , instructions ? { ... value, operatorInstructions: instructions } : value);
120 }
121 if (req.method === 'GET' && url.pathname === '/api/events' ) {
122 projectPath (project); res. writeHead ( 200 , { 'content-type' : 'text/event-stream' , 'cache-control' : 'no-cache' , connection: 'keep-alive' }); res. write ( 'retry: 2000 \n\n ' );
123 await replay (project, req.headers[ 'last-event-id' ] || url.searchParams. get ( 'cursor' ), res);
124 const set = clients. get (project) || new Set (); set. add (res); clients. set (project, set); req. once ( 'close' , () => { set. delete (res); if ( ! set.size) clients. delete (project); }); return ;
125 }
126 const data = await body (req);
127 if (req.method === 'POST' && url.pathname === '/api/upload' ) {
128 const dir = path. join ( projectPath (data.project), 'intake' ); const name = path. basename ( String (data.name || '' ));
129 if ( ! / ^ [A-Za-z0-9][A-Za-z0-9._-] {0,200}$ / . test (name) || ! / ^ [A-Za-z0-9+/=] *$ / . test (data.contentBase64 || '' )) throw new Error ( 'invalid upload' );
130 const content = Buffer. from (data.contentBase64, 'base64' ); if ( ! content. length || content. length > 50 * 1024 * 1024 ) throw new Error ( 'upload must be between 1 byte and 50 MB' );
131 await fs. mkdir (dir, { recursive: true }); await fs. writeFile (path. join (dir, name), content, { flag: 'wx' }); return send (res, 201 , { name });
132 }
133 if (req.method === 'POST' && url.pathname === '/api/run' ) { const projectName = data.project; launch (projectName, runArgs (projectName, data.options || {}, data.resume === true )); return send (res, 202 , { project: projectName, outcome: 'starting' }); }
134 if (req.method === 'POST' && url.pathname === '/api/answer' ) {
135 projectPath (data.project); const args = [ 'answer' , '--project' , projectPath (data.project), '--request-id' , String (data.requestId), '--format' , 'json' ];
136 if (data.choice !== undefined ) args. push ( '--choice' , String (data.choice)); else if (data.text !== undefined ) args. push ( '--text' , String (data.text)); else if (data.acknowledged === true ) args. push ( '--acknowledge' ); else throw new Error ( 'an answer is required' );
137 if (data.note !== undefined && data.choice === undefined && data.acknowledged !== true ) throw new Error ( 'a note requires a choice or acknowledgement' );
138 if (data.note !== undefined ) args. push ( '--note' , String (data.note));
139 if (data.continue) { const streamArgs = args. filter (( item , index ) => ! (item === '--format' || (index && args[index - 1 ] === '--format' ))); streamArgs. push ( '--format' , 'jsonl' , '--continue' ); launch (data.project, streamArgs); return send (res, 202 , { project: data.project, outcome: 'resuming' }); }
140 return send (res, 200 , await json (args));
141 }
142 if (req.method === 'POST' && url.pathname === '/api/cancel' ) return send (res, 200 , await json ([ 'cancel' , projectPath (data.project), '--reason' , String (data.reason || 'canceled by operator' ), '--format' , 'json' ]));
143 return send (res, 404 , { error: 'Not found' });
144 } catch (error) { return send (res, 400 , { error: error.message || String (error) }); }
145 });
146 server. listen ( parseArgs (), '127.0.0.1' , () => console. log ( `RePlatform console: http://127.0.0.1:${ server . address (). port }/?token=${ token }` ));