@aliwey/bmo 2.1.1 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/bmo.js +64 -16
- package/package.json +1 -1
- package/scripts/web_cmd.js +66 -10
package/bin/bmo.js
CHANGED
|
@@ -58,6 +58,34 @@ async function isPortOpen(port, host = '127.0.0.1') {
|
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function killProcessByPort(port) {
|
|
62
|
+
try {
|
|
63
|
+
if (os.platform() === 'win32') {
|
|
64
|
+
const output = execSync(`netstat -ano`, { encoding: 'utf8' });
|
|
65
|
+
const lines = output.split('\n');
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
if (line.includes(`:${port}`) && line.includes('LISTENING')) {
|
|
68
|
+
const parts = line.trim().split(/\s+/);
|
|
69
|
+
const pid = parts[parts.length - 1];
|
|
70
|
+
if (pid && pid !== '0') {
|
|
71
|
+
try {
|
|
72
|
+
execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' });
|
|
73
|
+
} catch (e) {}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
try {
|
|
79
|
+
execSync(`fuser -k ${port}/tcp`, { stdio: 'ignore' });
|
|
80
|
+
} catch {
|
|
81
|
+
try {
|
|
82
|
+
execSync(`kill -9 $(lsof -t -i:${port})`, { stdio: 'ignore' });
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {}
|
|
87
|
+
}
|
|
88
|
+
|
|
61
89
|
/** Resolve the embedded or system Python executable */
|
|
62
90
|
function getPython() {
|
|
63
91
|
const embeddedWin = path.join(BMO_HOME, 'python', 'python.exe');
|
|
@@ -152,31 +180,51 @@ Data lives in: ${BMO_HOME_DISPLAY}
|
|
|
152
180
|
// ── bmo --update [version] ────────────────────────────────────────────────
|
|
153
181
|
if (cmd === '--update' || cmd === '-update' || cmd === 'update') {
|
|
154
182
|
const ver = args[0] ? `@${args[0]}` : '@latest';
|
|
155
|
-
console.log(`Stopping any running BMO
|
|
183
|
+
console.log(`Stopping any running BMO processes and freeing ports...`);
|
|
184
|
+
|
|
185
|
+
// Kill processes on ports 3456 and 4097
|
|
186
|
+
killProcessByPort(3456);
|
|
187
|
+
killProcessByPort(4097);
|
|
188
|
+
|
|
156
189
|
try {
|
|
157
190
|
if (os.platform() === 'win32') {
|
|
158
|
-
execSync(`powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*main.py*' -or $_.CommandLine -like '*cli.py*' -or $_.CommandLine -like '*bmo*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"`, { stdio: 'ignore' });
|
|
191
|
+
execSync(`powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*main.py*' -or $_.CommandLine -like '*cli.py*' -or $_.CommandLine -like '*bmo*' -or $_.CommandLine -like '*cloudflared*' -or $_.CommandLine -like '*server.js*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"`, { stdio: 'ignore' });
|
|
159
192
|
} else {
|
|
160
|
-
execSync(`pkill -f "main.py|cli.py|bmo" || true`, { stdio: 'ignore' });
|
|
193
|
+
execSync(`pkill -f "main.py|cli.py|bmo|cloudflared|server.js" || true`, { stdio: 'ignore' });
|
|
161
194
|
}
|
|
162
195
|
} catch (e) {
|
|
163
196
|
// Ignore failures
|
|
164
197
|
}
|
|
165
198
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
199
|
+
if (os.platform() === 'win32') {
|
|
200
|
+
console.log('Launching updater in a new window to release folder/file locks...');
|
|
201
|
+
|
|
202
|
+
const psKillCmd = `powershell -Command \\"Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*main.py*' -or $_.CommandLine -like '*cli.py*' -or $_.CommandLine -like '*bmo*' -or $_.CommandLine -like '*cloudflared*' -or $_.CommandLine -like '*server.js*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }\\"`;
|
|
203
|
+
const cmdStr = `timeout /t 2 /nobreak >nul && ${psKillCmd} && echo [BMO Updater] Upgrading to @aliwey/bmo${ver}... && npm install -g @aliwey/bmo${ver} && echo [OK] BMO updated successfully! Press any key to close. && pause`;
|
|
204
|
+
|
|
205
|
+
spawn('cmd.exe', ['/c', 'start', 'cmd.exe', '/c', cmdStr], {
|
|
206
|
+
detached: true,
|
|
207
|
+
stdio: 'ignore'
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
console.log('Updater launched. This window will now close to release file locks.');
|
|
211
|
+
process.exit(0);
|
|
212
|
+
} else {
|
|
213
|
+
console.log(`Uninstalling old version of @aliwey/bmo...`);
|
|
214
|
+
try {
|
|
215
|
+
execSync('npm uninstall -g @aliwey/bmo', { stdio: 'inherit' });
|
|
216
|
+
} catch (e) {
|
|
217
|
+
// Ignore uninstall failures if it wasn't already installed
|
|
218
|
+
}
|
|
219
|
+
console.log(`Installing @aliwey/bmo${ver}...`);
|
|
220
|
+
try {
|
|
221
|
+
execSync(`npm install -g @aliwey/bmo${ver}`, { stdio: 'inherit' });
|
|
222
|
+
} catch {
|
|
223
|
+
console.error('Update failed. Try manually: npm install -g @aliwey/bmo');
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
process.exit(0);
|
|
178
227
|
}
|
|
179
|
-
process.exit(0);
|
|
180
228
|
}
|
|
181
229
|
|
|
182
230
|
// ── bmo init ──────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliwey/bmo",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
4
4
|
"description": "BMO — AI coding assistant with Telegram, CLI & Web sync. One command, all frontends.",
|
|
5
5
|
"keywords": ["ai", "coding-assistant", "telegram-bot", "cli", "opencode", "bfp"],
|
|
6
6
|
"homepage": "https://github.com/aliwey/bmo",
|
package/scripts/web_cmd.js
CHANGED
|
@@ -37,10 +37,38 @@ function getCloudflaredExe() {
|
|
|
37
37
|
: path.join(BMO_BIN, 'cloudflared');
|
|
38
38
|
if (fs.existsSync(embedded)) return embedded;
|
|
39
39
|
try { execSync('cloudflared --version', { stdio: 'ignore' }); return 'cloudflared'; } catch {}
|
|
40
|
-
console.error('
|
|
40
|
+
console.error('[Error] cloudflared not found. Run: bmo init');
|
|
41
41
|
process.exit(1);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function killProcessByPort(port) {
|
|
45
|
+
try {
|
|
46
|
+
if (os.platform() === 'win32') {
|
|
47
|
+
const output = execSync(`netstat -ano`, { encoding: 'utf8' });
|
|
48
|
+
const lines = output.split('\n');
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (line.includes(`:${port}`) && line.includes('LISTENING')) {
|
|
51
|
+
const parts = line.trim().split(/\s+/);
|
|
52
|
+
const pid = parts[parts.length - 1];
|
|
53
|
+
if (pid && pid !== '0') {
|
|
54
|
+
try {
|
|
55
|
+
execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' });
|
|
56
|
+
} catch (e) {}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
try {
|
|
62
|
+
execSync(`fuser -k ${port}/tcp`, { stdio: 'ignore' });
|
|
63
|
+
} catch {
|
|
64
|
+
try {
|
|
65
|
+
execSync(`kill -9 $(lsof -t -i:${port})`, { stdio: 'ignore' });
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (e) {}
|
|
70
|
+
}
|
|
71
|
+
|
|
44
72
|
function loadTasks() {
|
|
45
73
|
try { return JSON.parse(fs.readFileSync(TASKS_FILE, 'utf8')); } catch { return {}; }
|
|
46
74
|
}
|
|
@@ -70,19 +98,30 @@ function waitForTunnelUrl(proc) {
|
|
|
70
98
|
const tasks = loadTasks();
|
|
71
99
|
const existing = tasks.webchat_tunnel_url;
|
|
72
100
|
if (existing) {
|
|
73
|
-
console.log(`\n
|
|
101
|
+
console.log(`\n[Info] Webchat already running!`);
|
|
74
102
|
console.log(` Local: http://127.0.0.1:${WEBCHAT_PORT}`);
|
|
75
103
|
console.log(` Public: ${existing}\n`);
|
|
76
104
|
process.exit(0);
|
|
105
|
+
} else {
|
|
106
|
+
console.log(`[Info] Port ${WEBCHAT_PORT} is in use by an unrecognized process. Freeing port...`);
|
|
107
|
+
killProcessByPort(WEBCHAT_PORT);
|
|
108
|
+
await sleep(1000); // Wait for the port to release
|
|
77
109
|
}
|
|
78
110
|
}
|
|
79
111
|
|
|
80
112
|
// Start webchat server
|
|
81
|
-
console.log('
|
|
113
|
+
console.log('Starting webchat server...');
|
|
82
114
|
const webchatDir = path.join(PKG_DIR, 'webchat');
|
|
115
|
+
|
|
116
|
+
// Ensure logs directory exists
|
|
117
|
+
const logDir = path.join(BMO_HOME, 'logs');
|
|
118
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
119
|
+
const webchatLogPath = path.join(logDir, 'webchat.log');
|
|
120
|
+
const logStream = fs.openSync(webchatLogPath, 'a');
|
|
121
|
+
|
|
83
122
|
const webchat = spawn('node', ['server.js'], {
|
|
84
123
|
cwd: webchatDir,
|
|
85
|
-
stdio: 'ignore',
|
|
124
|
+
stdio: ['ignore', logStream, logStream],
|
|
86
125
|
detached: true,
|
|
87
126
|
env: {
|
|
88
127
|
...process.env,
|
|
@@ -94,15 +133,32 @@ function waitForTunnelUrl(proc) {
|
|
|
94
133
|
webchat.unref();
|
|
95
134
|
|
|
96
135
|
// Wait for webchat to be ready
|
|
136
|
+
let started = false;
|
|
97
137
|
for (let i = 0; i < 20; i++) {
|
|
98
138
|
await sleep(500);
|
|
99
|
-
if (await isPortOpen(WEBCHAT_PORT))
|
|
100
|
-
|
|
139
|
+
if (await isPortOpen(WEBCHAT_PORT)) {
|
|
140
|
+
started = true;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!started) {
|
|
146
|
+
console.error('[Error] Webchat failed to start');
|
|
147
|
+
try {
|
|
148
|
+
const logContent = fs.readFileSync(webchatLogPath, 'utf8').trim();
|
|
149
|
+
const lines = logContent.split('\n');
|
|
150
|
+
const lastLines = lines.slice(-15).join('\n');
|
|
151
|
+
console.error('\nLast log output:');
|
|
152
|
+
console.error(lastLines);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.error('Could not read webchat log file.');
|
|
155
|
+
}
|
|
156
|
+
process.exit(1);
|
|
101
157
|
}
|
|
102
|
-
console.log('
|
|
158
|
+
console.log('[OK] Webchat server running on port', WEBCHAT_PORT);
|
|
103
159
|
|
|
104
160
|
// Start cloudflared tunnel
|
|
105
|
-
console.log('
|
|
161
|
+
console.log('Starting cloudflared tunnel...');
|
|
106
162
|
const cfExe = getCloudflaredExe();
|
|
107
163
|
const cf = spawn(cfExe, ['tunnel', '--url', `http://localhost:${WEBCHAT_PORT}`], {
|
|
108
164
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -114,7 +170,7 @@ function waitForTunnelUrl(proc) {
|
|
|
114
170
|
try {
|
|
115
171
|
tunnelUrl = await waitForTunnelUrl(cf);
|
|
116
172
|
} catch {
|
|
117
|
-
console.error('
|
|
173
|
+
console.error('[Error] Could not get tunnel URL from cloudflared');
|
|
118
174
|
process.exit(1);
|
|
119
175
|
}
|
|
120
176
|
|
|
@@ -127,7 +183,7 @@ function waitForTunnelUrl(proc) {
|
|
|
127
183
|
saveTasks(tasks);
|
|
128
184
|
|
|
129
185
|
console.log('\n╭────────────────────────────────────────────╮');
|
|
130
|
-
console.log('│
|
|
186
|
+
console.log('│ [OK] BMO Webchat is live! │');
|
|
131
187
|
console.log(`│ Local: http://127.0.0.1:${WEBCHAT_PORT} │`);
|
|
132
188
|
console.log(`│ Public: ${tunnelUrl.padEnd(34)} │`);
|
|
133
189
|
console.log('╰────────────────────────────────────────────╯\n');
|