Skip to content

Commit 439a87b

Browse files
tecnomanuclaude
andcommitted
Release v1.7.0 — rm-bg CLI & desktop app
- New 'rm-bg' command (bin alias) with subcommands: web, start, stop, init, desktop, models (ls/pull/rm), update, help. Background start/stop via a pidfile. - Desktop app via Electron ('rm-bg desktop'): runs the Python server and shows the same UI in a native window; Electron is installed on first use into ~/.remove-background-local. New electron/main.js. - Backend model CLI: 'python server.py models ls|pull|rm'. - README: rm-bg command reference and desktop note. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent dcaba2d commit 439a87b

7 files changed

Lines changed: 303 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## v1.7.0 — rm-bg CLI & desktop app
4+
5+
- `rm-bg` command with subcommands: web, start, stop, init, desktop, models (ls/pull/rm), update
6+
- Desktop app via Electron (`rm-bg desktop`) showing the same UI in a native window
7+
- `python server.py models ls|pull|rm` backend CLI
8+
39
## v1.6.0 — Manage models & npx launcher
410

511
- Delete downloaded models from disk on the Models page

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,26 @@ Open in your browser: **http://127.0.0.1:7860**
8181
8282
> **Moved the folder?** A Python virtualenv stores absolute paths, so a copied/moved `.venv` is broken. `run.sh` detects this automatically and rebuilds the environment — you don't have to do anything.
8383
84+
## Commands (`rm-bg`)
85+
86+
When installed via npm (globally or with `npx`), you get the `rm-bg` command:
87+
88+
```bash
89+
rm-bg web # start the web server (foreground)
90+
rm-bg start # start it in the background
91+
rm-bg stop # stop the background server
92+
rm-bg init # set up and download the default model
93+
rm-bg desktop # open as a desktop app (Electron)
94+
rm-bg models ls # list models and which are downloaded
95+
rm-bg models pull --model birefnet-general # download a model
96+
rm-bg models rm --model birefnet-general # delete a downloaded model
97+
rm-bg update # update to the latest version
98+
rm-bg help # show all commands
99+
```
100+
101+
> **Desktop app:** `rm-bg desktop` shows the exact same UI in a native window.
102+
> The first run downloads Electron once into `~/.remove-background-local`.
103+
84104
## Usage
85105

86106
1. Drag one or more images onto the box (or click to choose, or paste with Cmd+V)

bin/cli.js

Lines changed: 148 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,42 @@
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

1221
const { spawnSync, spawn } = require("child_process");
1322
const fs = require("fs");
1423
const os = require("os");
1524
const 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, "..");
1828
const HOME = process.env.RBL_HOME || path.join(os.homedir(), ".remove-background-local");
1929
const VENV_DIR = path.join(HOME, "venv");
2030
const IS_WIN = process.platform === "win32";
2131
const 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");
2234
const PORT = process.env.PORT || "7860";
2335
const 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"); }
2640
function run(cmd, args, opts) { return spawnSync(cmd, args, Object.assign({ stdio: "inherit" }, opts || {})); }
2741

2842
function findPython() {
@@ -33,54 +47,156 @@ function findPython() {
3347
return null;
3448
}
3549
function 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
}
3952
function 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+
}
86202
main();

electron/main.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Electron main process for `rm-bg desktop`.
2+
// Starts the Python server (unless one is already running) and shows the same
3+
// web UI in a native window. Launched by bin/cli.js with RBL_PY / RBL_APP set.
4+
"use strict";
5+
6+
const { app, BrowserWindow, shell } = require("electron");
7+
const { spawn } = require("child_process");
8+
const http = require("http");
9+
const path = require("path");
10+
11+
const PY = process.env.RBL_PY || "python3";
12+
const APP_DIR = process.env.RBL_APP || path.join(__dirname, "..");
13+
const HOST = process.env.HOST || "127.0.0.1";
14+
const PORT = process.env.PORT || "7860";
15+
const URL = `http://${HOST}:${PORT}`;
16+
17+
let server = null;
18+
let startedByUs = false;
19+
20+
function isUp() {
21+
return new Promise((res) => {
22+
const req = http.get(URL + "/health", (r) => { r.resume(); res(r.statusCode === 200); });
23+
req.on("error", () => res(false));
24+
req.setTimeout(1200, () => { req.destroy(); res(false); });
25+
});
26+
}
27+
async function waitUp() {
28+
for (let i = 0; i < 200; i++) { if (await isUp()) return true; await new Promise((r) => setTimeout(r, 800)); }
29+
return false;
30+
}
31+
function startServer() {
32+
server = spawn(PY, [path.join(APP_DIR, "server.py")], {
33+
env: Object.assign({}, process.env, { HOST, PORT }),
34+
stdio: "ignore",
35+
});
36+
startedByUs = true;
37+
}
38+
function stopServer() {
39+
if (server && startedByUs) { try { server.kill(); } catch (e) { /* ignore */ } server = null; }
40+
}
41+
42+
const SPLASH =
43+
"data:text/html," + encodeURIComponent(
44+
"<body style='margin:0;background:#0f0f12;color:#9b9bab;font-family:-apple-system,sans-serif;" +
45+
"display:flex;align-items:center;justify-content:center;height:100vh'>" +
46+
"<div style='text-align:center'><div style='font-size:18px;color:#e7e7ee'>rm.background local</div>" +
47+
"<div style='margin-top:8px;font-size:13px'>Starting the engine, one moment…</div></div></body>");
48+
49+
async function createWindow() {
50+
const win = new BrowserWindow({
51+
width: 1200, height: 820, minWidth: 720, minHeight: 520,
52+
backgroundColor: "#0f0f12", title: "rm.background local",
53+
webPreferences: { contextIsolation: true },
54+
});
55+
// Open external links (e.g. the cafecito footer) in the system browser.
56+
win.webContents.setWindowOpenHandler(({ url }) => {
57+
if (!url.startsWith(URL)) { shell.openExternal(url); return { action: "deny" }; }
58+
return { action: "allow" };
59+
});
60+
win.loadURL(SPLASH);
61+
const ok = await waitUp();
62+
if (ok) win.loadURL(URL);
63+
else win.loadURL("data:text/html," + encodeURIComponent(
64+
"<body style='margin:0;background:#0f0f12;color:#ff6b6b;font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh'>" +
65+
"Could not start the server.</body>"));
66+
}
67+
68+
app.whenReady().then(async () => {
69+
if (!(await isUp())) startServer();
70+
createWindow();
71+
app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
72+
});
73+
app.on("window-all-closed", () => { stopServer(); if (process.platform !== "darwin") app.quit(); });
74+
app.on("quit", stopServer);

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
{
22
"name": "remove-background-local",
3-
"version": "1.6.0",
3+
"version": "1.7.0",
44
"description": "Remove image backgrounds locally — FastAPI + rembg (ISNet/BiRefNet), drag & drop web UI, 100% offline. Unofficial, not affiliated with remove.bg.",
55
"bin": {
6-
"remove-background-local": "bin/cli.js"
6+
"remove-background-local": "bin/cli.js",
7+
"rm-bg": "bin/cli.js"
78
},
89
"scripts": {
910
"start": "node bin/cli.js"
1011
},
1112
"files": [
1213
"bin/",
14+
"electron/",
1315
"server.py",
1416
"requirements.txt",
1517
"run.sh",

0 commit comments

Comments
 (0)