11#!/usr/bin/env node
22/*
3- * remove-background-local launcher.
3+ * rm-bg / remove-background-local launcher.
44 *
5- * Bootstraps a Python virtualenv, installs the dependencies and starts the
6- * server, so `npx remove-background-local` is a single command. It still needs
7- * Python 3.9+ on the machine (Node cannot install Python for you), and the first
8- * run downloads the default model.
5+ * Subcommands:
6+ * rm-bg web Start the web server in the foreground (Ctrl+C to stop)
7+ * rm-bg start Start the server in the background
8+ * rm-bg stop Stop the background server
9+ * rm-bg init Set up the environment and download the default model
10+ * rm-bg desktop Open as a desktop app (Electron)
11+ * rm-bg models ls List models and which are downloaded
12+ * rm-bg models pull --model X Download a model
13+ * rm-bg models rm --model X Delete a downloaded model
14+ * rm-bg update Update to the latest published version
15+ * rm-bg help Show this help
16+ *
17+ * Needs Python 3.9+ already installed (Node cannot install Python for you).
918 */
1019"use strict" ;
1120
1221const { spawnSync, spawn } = require ( "child_process" ) ;
1322const fs = require ( "fs" ) ;
1423const os = require ( "os" ) ;
1524const path = require ( "path" ) ;
25+ const http = require ( "http" ) ;
1626
17- const APP_DIR = path . join ( __dirname , ".." ) ; // package root (has server.py)
27+ const APP_DIR = path . join ( __dirname , ".." ) ;
1828const HOME = process . env . RBL_HOME || path . join ( os . homedir ( ) , ".remove-background-local" ) ;
1929const VENV_DIR = path . join ( HOME , "venv" ) ;
2030const IS_WIN = process . platform === "win32" ;
2131const VENV_PY = IS_WIN ? path . join ( VENV_DIR , "Scripts" , "python.exe" ) : path . join ( VENV_DIR , "bin" , "python" ) ;
32+ const PID_FILE = path . join ( HOME , "server.pid" ) ;
33+ const LOG_FILE = path . join ( HOME , "server.log" ) ;
2234const PORT = process . env . PORT || "7860" ;
2335const HOST = process . env . HOST || "127.0.0.1" ;
36+ const URL = `http://${ HOST } :${ PORT } ` ;
2437
25- function log ( msg ) { process . stdout . write ( ">> " + msg + "\n" ) ; }
38+ function log ( m ) { process . stdout . write ( ">> " + m + "\n" ) ; }
39+ function err ( m ) { process . stderr . write ( m + "\n" ) ; }
2640function run ( cmd , args , opts ) { return spawnSync ( cmd , args , Object . assign ( { stdio : "inherit" } , opts || { } ) ) ; }
2741
2842function findPython ( ) {
@@ -33,54 +47,156 @@ function findPython() {
3347 return null ;
3448}
3549function venvHealthy ( ) {
36- if ( ! fs . existsSync ( VENV_PY ) ) return false ;
37- return spawnSync ( VENV_PY , [ "-c" , "import sys" ] ) . status === 0 ;
50+ return fs . existsSync ( VENV_PY ) && spawnSync ( VENV_PY , [ "-c" , "import sys" ] ) . status === 0 ;
3851}
3952function depsInstalled ( ) {
4053 return spawnSync ( VENV_PY , [ "-c" , "import fastapi, uvicorn, rembg, PIL, multipart, onnxruntime" ] ) . status === 0 ;
4154}
42- function openBrowser ( url ) {
43- const cmd = IS_WIN ? "cmd" : process . platform === "darwin" ? "open" : "xdg-open" ;
44- const args = IS_WIN ? [ "/c" , "start" , "" , url ] : [ url ] ;
45- try { spawn ( cmd , args , { stdio : "ignore" , detached : true } ) . unref ( ) ; } catch ( e ) { /* ignore */ }
46- }
47-
48- function main ( ) {
55+ function ensureSetup ( ) {
4956 const py = findPython ( ) ;
5057 if ( ! py ) {
51- process . stderr . write (
52- "\nremove-background-local needs Python 3.9 or newer.\n" +
53- "Install it from https://www.python.org/downloads/ (or `brew install python`) and try again.\n\n"
54- ) ;
58+ err ( "\nremove-background-local needs Python 3.9 or newer.\nInstall it from https://www.python.org/downloads/ (or `brew install python`) and try again.\n" ) ;
5559 process . exit ( 1 ) ;
5660 }
57-
5861 fs . mkdirSync ( HOME , { recursive : true } ) ;
59-
6062 if ( ! venvHealthy ( ) ) {
6163 log ( "Creating Python environment (first run)..." ) ;
62- if ( run ( py , [ "-m" , "venv" , VENV_DIR ] ) . status !== 0 ) { process . stderr . write ( "Failed to create the virtualenv.\n " ) ; process . exit ( 1 ) ; }
64+ if ( run ( py , [ "-m" , "venv" , VENV_DIR ] ) . status !== 0 ) { err ( "Failed to create the virtualenv." ) ; process . exit ( 1 ) ; }
6365 }
6466 if ( ! depsInstalled ( ) ) {
6567 log ( "Installing dependencies (first run can take 2-5 min)..." ) ;
6668 run ( VENV_PY , [ "-m" , "pip" , "install" , "--upgrade" , "pip" ] ) ;
6769 if ( run ( VENV_PY , [ "-m" , "pip" , "install" , "-r" , path . join ( APP_DIR , "requirements.txt" ) ] ) . status !== 0 ) {
68- process . stderr . write ( "Failed to install dependencies.\n " ) ; process . exit ( 1 ) ;
70+ err ( "Failed to install dependencies." ) ; process . exit ( 1 ) ;
6971 }
7072 }
73+ }
74+ function serverEnv ( ) {
75+ return Object . assign ( { } , process . env , { HOST , PORT } ) ;
76+ }
77+ function openBrowser ( url ) {
78+ const cmd = IS_WIN ? "cmd" : process . platform === "darwin" ? "open" : "xdg-open" ;
79+ const args = IS_WIN ? [ "/c" , "start" , "" , url ] : [ url ] ;
80+ try { spawn ( cmd , args , { stdio : "ignore" , detached : true } ) . unref ( ) ; } catch ( e ) { /* ignore */ }
81+ }
82+ function isUp ( ) {
83+ return new Promise ( ( res ) => {
84+ const req = http . get ( URL + "/health" , ( r ) => { r . resume ( ) ; res ( r . statusCode === 200 ) ; } ) ;
85+ req . on ( "error" , ( ) => res ( false ) ) ;
86+ req . setTimeout ( 1500 , ( ) => { req . destroy ( ) ; res ( false ) ; } ) ;
87+ } ) ;
88+ }
89+ async function waitUp ( timeoutMs ) {
90+ const t0 = Date . now ( ) ;
91+ while ( Date . now ( ) - t0 < ( timeoutMs || 120000 ) ) { if ( await isUp ( ) ) return true ; await new Promise ( r => setTimeout ( r , 800 ) ) ; }
92+ return false ;
93+ }
94+ function readPid ( ) { try { return parseInt ( fs . readFileSync ( PID_FILE , "utf8" ) . trim ( ) , 10 ) || 0 ; } catch { return 0 ; } }
95+ function pidAlive ( pid ) { try { process . kill ( pid , 0 ) ; return true ; } catch { return false ; } }
7196
72- const url = `http://${ HOST } :${ PORT } ` ;
73- log ( `Starting server on ${ url } ` ) ;
97+ // ---- commands ----
98+ function cmdWeb ( ) {
99+ ensureSetup ( ) ;
100+ log ( `Starting server on ${ URL } ` ) ;
74101 log ( "(Ctrl+C to stop)" ) ;
75- setTimeout ( ( ) => openBrowser ( url ) , 2500 ) ;
76-
77- const child = spawn ( VENV_PY , [ path . join ( APP_DIR , "server.py" ) ] , {
102+ setTimeout ( ( ) => openBrowser ( URL ) , 2500 ) ;
103+ const child = spawn ( VENV_PY , [ path . join ( APP_DIR , "server.py" ) ] , { stdio : "inherit" , env : serverEnv ( ) } ) ;
104+ try { fs . writeFileSync ( PID_FILE , String ( child . pid ) ) ; } catch { }
105+ child . on ( "exit" , ( code ) => { try { fs . unlinkSync ( PID_FILE ) ; } catch { } process . exit ( code || 0 ) ; } ) ;
106+ process . on ( "SIGINT" , ( ) => child . kill ( "SIGINT" ) ) ;
107+ process . on ( "SIGTERM" , ( ) => child . kill ( "SIGTERM" ) ) ;
108+ }
109+ async function cmdStart ( ) {
110+ const pid = readPid ( ) ;
111+ if ( pid && pidAlive ( pid ) ) { log ( `Already running (pid ${ pid } ) on ${ URL } ` ) ; return ; }
112+ ensureSetup ( ) ;
113+ log ( "Starting server in the background..." ) ;
114+ const out = fs . openSync ( LOG_FILE , "a" ) ;
115+ const child = spawn ( VENV_PY , [ path . join ( APP_DIR , "server.py" ) ] , { stdio : [ "ignore" , out , out ] , env : serverEnv ( ) , detached : true } ) ;
116+ fs . writeFileSync ( PID_FILE , String ( child . pid ) ) ;
117+ child . unref ( ) ;
118+ const ok = await waitUp ( 120000 ) ;
119+ if ( ok ) { log ( `Running on ${ URL } (pid ${ child . pid } )` ) ; log ( `Logs: ${ LOG_FILE } — stop with: rm-bg stop` ) ; openBrowser ( URL ) ; }
120+ else { log ( `Started (pid ${ child . pid } ); still warming up. Logs: ${ LOG_FILE } ` ) ; }
121+ process . exit ( 0 ) ;
122+ }
123+ function cmdStop ( ) {
124+ const pid = readPid ( ) ;
125+ if ( ! pid || ! pidAlive ( pid ) ) { log ( "No background server is running." ) ; try { fs . unlinkSync ( PID_FILE ) ; } catch { } return ; }
126+ try { process . kill ( pid , "SIGTERM" ) ; log ( `Stopped server (pid ${ pid } ).` ) ; } catch ( e ) { err ( "Could not stop: " + e . message ) ; }
127+ try { fs . unlinkSync ( PID_FILE ) ; } catch { }
128+ }
129+ function cmdInit ( ) {
130+ ensureSetup ( ) ;
131+ log ( "Downloading the default model..." ) ;
132+ run ( VENV_PY , [ path . join ( APP_DIR , "server.py" ) , "models" , "pull" ] ) ;
133+ log ( "Ready. Start it with: rm-bg start (or rm-bg web)" ) ;
134+ }
135+ function cmdModels ( rest ) {
136+ ensureSetup ( ) ;
137+ const r = run ( VENV_PY , [ path . join ( APP_DIR , "server.py" ) , "models" , ...rest ] ) ;
138+ process . exit ( r . status || 0 ) ;
139+ }
140+ function cmdUpdate ( ) {
141+ log ( "Updating remove-background-local from npm..." ) ;
142+ const r = run ( IS_WIN ? "npm.cmd" : "npm" , [ "install" , "-g" , "remove-background-local@latest" ] ) ;
143+ if ( r . status !== 0 ) err ( "Update failed. If you run it with npx, just use `npx -y remove-background-local@latest`." ) ;
144+ }
145+ function cmdDesktop ( ) {
146+ ensureSetup ( ) ;
147+ const desktopDir = path . join ( HOME , "desktop" ) ;
148+ const electronBin = path . join ( desktopDir , "node_modules" , ".bin" , IS_WIN ? "electron.cmd" : "electron" ) ;
149+ if ( ! fs . existsSync ( electronBin ) ) {
150+ log ( "Installing the desktop runtime (Electron) the first time..." ) ;
151+ fs . mkdirSync ( desktopDir , { recursive : true } ) ;
152+ if ( ! fs . existsSync ( path . join ( desktopDir , "package.json" ) ) ) {
153+ fs . writeFileSync ( path . join ( desktopDir , "package.json" ) , JSON . stringify ( { name : "rbl-desktop" , private : true } , null , 2 ) ) ;
154+ }
155+ const r = run ( IS_WIN ? "npm.cmd" : "npm" , [ "install" , "electron@latest" ] , { cwd : desktopDir } ) ;
156+ if ( r . status !== 0 || ! fs . existsSync ( electronBin ) ) { err ( "Could not install Electron. You can still use `rm-bg web`." ) ; process . exit ( 1 ) ; }
157+ }
158+ log ( "Opening desktop app..." ) ;
159+ const child = spawn ( electronBin , [ path . join ( APP_DIR , "electron" , "main.js" ) ] , {
78160 stdio : "inherit" ,
79- env : Object . assign ( { } , process . env , { HOST , PORT } ) ,
161+ env : Object . assign ( { } , process . env , { RBL_PY : VENV_PY , RBL_APP : APP_DIR , HOST , PORT } ) ,
80162 } ) ;
81163 child . on ( "exit" , ( code ) => process . exit ( code || 0 ) ) ;
82- process . on ( "SIGINT" , ( ) => child . kill ( "SIGINT" ) ) ;
83- process . on ( "SIGTERM" , ( ) => child . kill ( "SIGTERM" ) ) ;
84164}
165+ function cmdHelp ( ) {
166+ process . stdout . write ( `
167+ rm-bg — remove-background-local
85168
169+ Usage:
170+ rm-bg web Start the web server (foreground, Ctrl+C to stop)
171+ rm-bg start Start the server in the background
172+ rm-bg stop Stop the background server
173+ rm-bg init Set up and download the default model
174+ rm-bg desktop Open as a desktop app (Electron)
175+ rm-bg models ls List models and which are downloaded
176+ rm-bg models pull --model X Download a model
177+ rm-bg models rm --model X Delete a downloaded model
178+ rm-bg update Update to the latest version
179+ rm-bg help Show this help
180+
181+ Env: HOST (default 127.0.0.1), PORT (default 7860)
182+ ` ) ;
183+ }
184+
185+ function main ( ) {
186+ const argv = process . argv . slice ( 2 ) ;
187+ const cmd = ( argv [ 0 ] || "web" ) . toLowerCase ( ) ;
188+ const rest = argv . slice ( 1 ) ;
189+ switch ( cmd ) {
190+ case "web" : case "serve" : return cmdWeb ( ) ;
191+ case "start" : case "up" : return void cmdStart ( ) ;
192+ case "stop" : case "down" : return cmdStop ( ) ;
193+ case "init" : case "setup" : return cmdInit ( ) ;
194+ case "desktop" : case "app" : return cmdDesktop ( ) ;
195+ case "models" : case "model" : return cmdModels ( rest ) ;
196+ case "update" : case "upgrade" : return cmdUpdate ( ) ;
197+ case "help" : case "-h" : case "--help" : return cmdHelp ( ) ;
198+ default :
199+ err ( `Unknown command: ${ cmd } \n` ) ; cmdHelp ( ) ; process . exit ( 1 ) ;
200+ }
201+ }
86202main ( ) ;
0 commit comments