1- import path from 'path' ;
21import fs from 'fs' ;
32import os from 'os' ;
3+ import path from 'path' ;
4+ import { getAppdataPath } from 'cli/lib/appdata' ;
45
5- function resolvePm2 ( ) : typeof import ( 'pm2' ) {
6+ function resolvePm2 ( ) : typeof import ( 'pm2' ) {
67 try {
78 return require ( 'pm2' ) ;
89 } catch ( error ) {
@@ -26,7 +27,9 @@ function resolvePm2(): typeof import( 'pm2' ) {
2627 }
2728
2829 throw new Error (
29- `pm2 module not found. Please ensure pm2 is installed in the CLI dependencies. Tried paths: ${ possiblePaths . join ( ', ' ) } `
30+ `pm2 module not found. Please ensure pm2 is installed in the CLI dependencies. Tried paths: ${ possiblePaths . join (
31+ ', '
32+ ) } `
3033 ) ;
3134 }
3235}
@@ -133,7 +136,8 @@ export async function startDaemon(): Promise< void > {
133136 return ;
134137 }
135138 await connect ( ) ;
136- disconnect ( ) ;
139+ // Keep connection open - subsequent operations will reuse it
140+ // The isConnected flag prevents duplicate connections
137141}
138142
139143export async function stopDaemon ( ) : Promise < void > {
@@ -262,3 +266,116 @@ process.on( 'exit', cleanup );
262266process . on ( 'SIGINT' , cleanup ) ;
263267process . on ( 'SIGTERM' , cleanup ) ;
264268
269+ /**
270+ * Proxy Server Management Functions
271+ *
272+ * The proxy runs as a PM2-managed CLI process. When `studio proxy start` is called,
273+ * PM2 starts the CLI with the proxy command and keeps it running persistently.
274+ */
275+
276+ const PROXY_PROCESS_NAME = 'studio-proxy' ;
277+
278+ /**
279+ * Start the proxy server via PM2
280+ * This launches the CLI with `proxy start --managed` which runs the proxy servers
281+ */
282+ export async function startProxyProcess ( cliPath : string ) : Promise < ProcessDescription > {
283+ await ensureDaemonRunning ( ) ;
284+
285+ return new Promise ( ( resolve , reject ) => {
286+ const processConfig : pm2 . StartOptions = {
287+ name : PROXY_PROCESS_NAME ,
288+ script : cliPath ,
289+ args : [ 'proxy' , 'boot' , '--managed' ] ,
290+ instances : 1 ,
291+ exec_mode : 'fork' ,
292+ autorestart : true ,
293+ max_restarts : 10 ,
294+ min_uptime : '10s' ,
295+ restart_delay : 3000 ,
296+ kill_timeout : 5000 ,
297+ uid : 0 , // Run as root to bind to ports 80 and 443
298+ env : {
299+ // Pass the real user's home directory so proxy can find appdata
300+ // When running as root, os.homedir() returns /var/root instead of the user's home
301+ STUDIO_USER_HOME : os . homedir ( ) ,
302+ // Pass the actual appdata file path directly from CLI
303+ STUDIO_APPDATA_PATH : getAppdataPath ( ) ,
304+ } ,
305+ } ;
306+
307+ pm2 . start ( processConfig , ( error , apps ) => {
308+ disconnect ( ) ;
309+ if ( error ) {
310+ reject ( error ) ;
311+ return ;
312+ }
313+
314+ if ( ! apps || apps . length === 0 ) {
315+ reject ( new Error ( 'Failed to start proxy process' ) ) ;
316+ return ;
317+ }
318+
319+ resolve ( apps [ 0 ] as ProcessDescription ) ;
320+ } ) ;
321+ } ) ;
322+ }
323+
324+ /**
325+ * Check if the proxy process is running
326+ */
327+ export async function isProxyProcessRunning ( ) : Promise < boolean > {
328+ try {
329+ if ( ! isDaemonRunning ( ) ) {
330+ return false ;
331+ }
332+
333+ const processes = await listProcesses ( false ) ;
334+ return processes . some ( ( p ) => p . name === PROXY_PROCESS_NAME && p . status === 'online' ) ;
335+ } catch ( error ) {
336+ console . error ( 'Error checking if proxy is running:' , error ) ;
337+ return false ;
338+ }
339+ }
340+
341+ /**
342+ * Get the proxy process status
343+ */
344+ export async function getProxyProcessStatus ( ) : Promise < ProcessDescription | null > {
345+ return describeProcess ( PROXY_PROCESS_NAME ) ;
346+ }
347+
348+ /**
349+ * Stop the proxy server
350+ */
351+ export async function stopProxyProcess ( ) : Promise < void > {
352+ await stopProcess ( PROXY_PROCESS_NAME ) ;
353+ }
354+
355+ /**
356+ * Restart the proxy server
357+ */
358+ export async function restartProxyProcess ( ) : Promise < void > {
359+ await restartProcess ( PROXY_PROCESS_NAME ) ;
360+ }
361+
362+ /**
363+ * Delete the proxy process from PM2
364+ */
365+ export async function deleteProxyProcess ( ) : Promise < void > {
366+ await deleteProcess ( PROXY_PROCESS_NAME ) ;
367+ }
368+
369+ /**
370+ * Ensure the proxy is running, start it if not (idempotent)
371+ */
372+ export async function ensureProxyRunning ( cliPath : string ) : Promise < void > {
373+ const isRunning = await isProxyProcessRunning ( ) ;
374+ if ( isRunning ) {
375+ console . log ( 'Proxy is already running' ) ;
376+ return ;
377+ }
378+
379+ console . log ( 'Starting proxy server...' ) ;
380+ await startProxyProcess ( cliPath ) ;
381+ }
0 commit comments