@chatpanel/bridge 0.1.2 → 0.2.1
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/README.md +23 -7
- package/package.json +29 -7
- package/scripts/build-binaries.sh +21 -0
- package/src/engines/claude.js +32 -0
- package/src/server.js +113 -8
- package/src/service.js +147 -0
package/README.md
CHANGED
|
@@ -14,22 +14,38 @@ the ones the bridge reports as available.
|
|
|
14
14
|
|
|
15
15
|
## Run it
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
### Option A — download the app (no Node.js needed)
|
|
18
|
+
|
|
19
|
+
Grab the standalone binary for your OS from the
|
|
20
|
+
[latest release](https://github.com/chatpanel/chatpanel-bridge/releases/latest) —
|
|
21
|
+
it bundles its own runtime, so **nothing to install**. Run it once to set it up to
|
|
22
|
+
start automatically at login and run in the background:
|
|
19
23
|
|
|
20
24
|
```bash
|
|
21
|
-
|
|
25
|
+
# macOS / Linux (make it executable first on macOS)
|
|
26
|
+
chmod +x chatpanel-bridge-macos-arm64
|
|
27
|
+
./chatpanel-bridge-macos-arm64 --install
|
|
28
|
+
|
|
29
|
+
# Windows (PowerShell)
|
|
30
|
+
.\chatpanel-bridge-windows-x64.exe --install
|
|
22
31
|
```
|
|
23
32
|
|
|
24
|
-
|
|
33
|
+
That's it — open the ChatPanel side panel and your agents appear. Manage it with
|
|
34
|
+
`--status` (is it set up?) and `--uninstall` (remove auto-start). Run with no flags
|
|
35
|
+
to start it once in the foreground instead.
|
|
25
36
|
|
|
26
|
-
|
|
37
|
+
> Until these binaries are code-signed, macOS Gatekeeper / Windows SmartScreen may
|
|
38
|
+
> warn on first run (on macOS, right-click the file → **Open**). Signing is on the way.
|
|
39
|
+
|
|
40
|
+
### Option B — via npm (needs Node.js 18+)
|
|
27
41
|
|
|
28
42
|
```bash
|
|
29
|
-
|
|
30
|
-
chatpanel-bridge # → http://127.0.0.1:4319
|
|
43
|
+
npx @chatpanel/bridge # → http://127.0.0.1:4319
|
|
31
44
|
```
|
|
32
45
|
|
|
46
|
+
…then leave it running and open the ChatPanel side panel. Prefer a persistent
|
|
47
|
+
command? `npm i -g @chatpanel/bridge` then `chatpanel-bridge`.
|
|
48
|
+
|
|
33
49
|
Prerequisites (the agents you want to use must already be set up):
|
|
34
50
|
|
|
35
51
|
- **Claude Code**: be signed in (`claude`) or set `ANTHROPIC_API_KEY`.
|
package/package.json
CHANGED
|
@@ -1,18 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/bridge",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local bridge that exposes the AI coding agents installed on your machine — Claude Code (Agent SDK), Codex (CLI), and Gemini CLI — to the ChatPanel Chrome extension over a localhost SSE endpoint. Bring your own agent.",
|
|
6
|
-
"keywords": [
|
|
6
|
+
"keywords": [
|
|
7
|
+
"chatpanel",
|
|
8
|
+
"claude-code",
|
|
9
|
+
"codex",
|
|
10
|
+
"gemini-cli",
|
|
11
|
+
"chrome-extension",
|
|
12
|
+
"ai-agents",
|
|
13
|
+
"bridge"
|
|
14
|
+
],
|
|
7
15
|
"homepage": "https://chatpanel.net",
|
|
8
|
-
"repository": {
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/chatpanel/chatpanel-bridge.git"
|
|
19
|
+
},
|
|
9
20
|
"license": "MIT",
|
|
10
|
-
"bin": {
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
"bin": {
|
|
22
|
+
"chatpanel-bridge": "src/server.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"scripts",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
".env.example"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
13
34
|
"scripts": {
|
|
14
35
|
"start": "node src/server.js",
|
|
15
|
-
"dev": "node --watch src/server.js"
|
|
36
|
+
"dev": "node --watch src/server.js",
|
|
37
|
+
"build:bin": "bash scripts/build-binaries.sh"
|
|
16
38
|
},
|
|
17
39
|
"dependencies": {
|
|
18
40
|
"@anthropic-ai/claude-agent-sdk": "^0.1.0"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Compile the bridge into standalone, single-file binaries (no Node required to
|
|
3
|
+
# run them). Needs Bun: https://bun.sh
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
cd "$(dirname "$0")/.."
|
|
6
|
+
mkdir -p dist
|
|
7
|
+
rm -f dist/chatpanel-bridge-*
|
|
8
|
+
|
|
9
|
+
targets=(
|
|
10
|
+
"bun-darwin-arm64:chatpanel-bridge-macos-arm64"
|
|
11
|
+
"bun-darwin-x64:chatpanel-bridge-macos-x64"
|
|
12
|
+
"bun-linux-x64:chatpanel-bridge-linux-x64"
|
|
13
|
+
"bun-windows-x64:chatpanel-bridge-windows-x64.exe"
|
|
14
|
+
)
|
|
15
|
+
for t in "${targets[@]}"; do
|
|
16
|
+
target="${t%%:*}"; out="${t##*:}"
|
|
17
|
+
echo "→ building dist/$out ($target)"
|
|
18
|
+
bun build src/server.js --compile --target="$target" --outfile "dist/$out"
|
|
19
|
+
done
|
|
20
|
+
echo "✓ binaries in dist/"
|
|
21
|
+
ls -la dist
|
package/src/engines/claude.js
CHANGED
|
@@ -122,6 +122,38 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
122
122
|
emit({ type: 'done', text: streamedAny ? '' : resultText });
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
// A fast, tool-free single-shot completion — used for prompt autocomplete. No
|
|
126
|
+
// claude_code preset, no tools, no local config: just a quick text continuation
|
|
127
|
+
// from a fast model (Haiku by default). Returns the completion string.
|
|
128
|
+
export async function complete({ prompt, system, model }) {
|
|
129
|
+
const sdk = await loadSdk();
|
|
130
|
+
if (!sdk) throw new Error('Claude Agent SDK not installed.');
|
|
131
|
+
const { query } = sdk;
|
|
132
|
+
let text = '';
|
|
133
|
+
const iterator = query({
|
|
134
|
+
prompt,
|
|
135
|
+
options: {
|
|
136
|
+
cwd: os.homedir(),
|
|
137
|
+
permissionMode: 'default',
|
|
138
|
+
allowedTools: [], // no tools — pure text completion
|
|
139
|
+
maxTurns: 1,
|
|
140
|
+
settingSources: [], // skip CLAUDE.md / MCP for a tiny completion
|
|
141
|
+
systemPrompt: system || "Continue the user's text briefly. Reply with only the continuation.",
|
|
142
|
+
model: model || 'haiku',
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
for await (const message of iterator) {
|
|
146
|
+
if (message.type === 'assistant') {
|
|
147
|
+
for (const block of message.message.content) {
|
|
148
|
+
if (block.type === 'text') text += block.text;
|
|
149
|
+
}
|
|
150
|
+
} else if (message.type === 'result' && message.subtype === 'success' && !text) {
|
|
151
|
+
text = message.result || '';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return text.trim();
|
|
155
|
+
}
|
|
156
|
+
|
|
125
157
|
function toolSummary(block) {
|
|
126
158
|
const i = block.input || {};
|
|
127
159
|
if (i.command) return String(i.command).slice(0, 60);
|
package/src/server.js
CHANGED
|
@@ -18,8 +18,9 @@ import { createServer } from 'node:http';
|
|
|
18
18
|
import * as claude from './engines/claude.js';
|
|
19
19
|
import * as codex from './engines/codex.js';
|
|
20
20
|
import * as gemini from './engines/gemini.js';
|
|
21
|
+
import { installService, uninstallService, serviceStatus } from './service.js';
|
|
21
22
|
|
|
22
|
-
const VERSION = '0.1
|
|
23
|
+
const VERSION = '0.2.1';
|
|
23
24
|
const HOST = process.env.CHATPANEL_BRIDGE_HOST || '127.0.0.1';
|
|
24
25
|
const PORT = Number(process.env.CHATPANEL_BRIDGE_PORT) || 4319;
|
|
25
26
|
|
|
@@ -124,6 +125,46 @@ async function handleChat(req, res) {
|
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
// POST /complete → { agent, prompt, model? } → { text } — a fast, single-shot
|
|
129
|
+
// completion for prompt autocomplete. Uses the engine's complete() if it has one
|
|
130
|
+
// (Claude: Haiku, no tools), else a one-shot chat collected into text.
|
|
131
|
+
async function handleComplete(req, res) {
|
|
132
|
+
let body;
|
|
133
|
+
try {
|
|
134
|
+
body = await readBody(req);
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return json(res, 400, { error: 'Bad JSON: ' + e.message });
|
|
137
|
+
}
|
|
138
|
+
const target = ENGINES[body.agent];
|
|
139
|
+
if (!target) return json(res, 404, { error: `Unknown agent "${body.agent}"` });
|
|
140
|
+
const prompt = String(body.prompt || '').slice(0, 6000);
|
|
141
|
+
if (!prompt) return json(res, 400, { error: 'Empty prompt' });
|
|
142
|
+
const model = body.model || '';
|
|
143
|
+
// The extension sends a strict "continue, don't answer" system prompt (with any
|
|
144
|
+
// page context already in `prompt`); fall back to a sensible default.
|
|
145
|
+
const system =
|
|
146
|
+
String(body.system || '').slice(0, 2000) ||
|
|
147
|
+
'You autocomplete an unfinished message the user is typing. Output ONLY the ' +
|
|
148
|
+
'few words that come next. Do not answer it. No quotes, no repetition.';
|
|
149
|
+
try {
|
|
150
|
+
let text = '';
|
|
151
|
+
if (typeof target.engine.complete === 'function') {
|
|
152
|
+
text = await target.engine.complete({ prompt, system, model });
|
|
153
|
+
} else {
|
|
154
|
+
await target.engine.chat(
|
|
155
|
+
{ messages: [{ role: 'user', content: prompt }], system, options: { model } },
|
|
156
|
+
(obj) => {
|
|
157
|
+
if (obj.type === 'delta') text += obj.text || '';
|
|
158
|
+
else if (obj.type === 'done' && obj.text) text += obj.text;
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return json(res, 200, { text: (text || '').trim() });
|
|
163
|
+
} catch (e) {
|
|
164
|
+
return json(res, 502, { error: e?.message || String(e) });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
127
168
|
const server = createServer(async (req, res) => {
|
|
128
169
|
cors(req, res);
|
|
129
170
|
if (req.method === 'OPTIONS') {
|
|
@@ -134,6 +175,7 @@ const server = createServer(async (req, res) => {
|
|
|
134
175
|
try {
|
|
135
176
|
if (req.method === 'GET' && url.pathname === '/health') return handleHealth(res);
|
|
136
177
|
if (req.method === 'POST' && url.pathname === '/chat') return handleChat(req, res);
|
|
178
|
+
if (req.method === 'POST' && url.pathname === '/complete') return handleComplete(req, res);
|
|
137
179
|
json(res, 404, { error: 'Not found' });
|
|
138
180
|
} catch (e) {
|
|
139
181
|
json(res, 500, { error: e?.message || String(e) });
|
|
@@ -145,11 +187,74 @@ function log(level, msg) {
|
|
|
145
187
|
fn(`[chatpanel-bridge] ${msg}`);
|
|
146
188
|
}
|
|
147
189
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
190
|
+
function startServer() {
|
|
191
|
+
server.listen(PORT, HOST, async () => {
|
|
192
|
+
log('info', `listening on http://${HOST}:${PORT}`);
|
|
193
|
+
for (const [, { engine, label }] of Object.entries(ENGINES)) {
|
|
194
|
+
const a = await engine.available().catch(() => ({ ok: false }));
|
|
195
|
+
log('info', ` ${a.ok ? '✓' : '✕'} ${label}${a.ok ? '' : ' — ' + (a.reason || 'unavailable')}`);
|
|
196
|
+
}
|
|
197
|
+
log('info', 'Open the ChatPanel side panel; installed agents (Claude Code, Codex, Gemini CLI) appear automatically.');
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function printHelp() {
|
|
202
|
+
console.log(`ChatPanel Bridge v${VERSION}
|
|
203
|
+
|
|
204
|
+
Usage:
|
|
205
|
+
chatpanel-bridge start the bridge (foreground) on ${HOST}:${PORT}
|
|
206
|
+
chatpanel-bridge --install run automatically at login, in the background
|
|
207
|
+
chatpanel-bridge --uninstall remove the login auto-start
|
|
208
|
+
chatpanel-bridge --status show whether auto-start is set up
|
|
209
|
+
chatpanel-bridge --version print the version
|
|
210
|
+
|
|
211
|
+
Env: CHATPANEL_BRIDGE_HOST, CHATPANEL_BRIDGE_PORT`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Handle CLI commands before starting the server. Returns true if a command ran.
|
|
215
|
+
function runCli() {
|
|
216
|
+
const argv = process.argv;
|
|
217
|
+
const has = (...flags) => flags.some((f) => argv.includes(f));
|
|
218
|
+
|
|
219
|
+
if (has('--help', '-h')) {
|
|
220
|
+
printHelp();
|
|
221
|
+
return true;
|
|
153
222
|
}
|
|
154
|
-
|
|
155
|
-
|
|
223
|
+
if (has('--version', '-v')) {
|
|
224
|
+
console.log(VERSION);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
if (has('--install')) {
|
|
228
|
+
try {
|
|
229
|
+
installService();
|
|
230
|
+
log('info', 'Installed. The bridge now starts automatically at login and is running in the background.');
|
|
231
|
+
} catch (e) {
|
|
232
|
+
log('error', 'Install failed: ' + (e?.message || e));
|
|
233
|
+
process.exitCode = 1;
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
if (has('--uninstall')) {
|
|
238
|
+
try {
|
|
239
|
+
uninstallService();
|
|
240
|
+
log('info', 'Removed the login auto-start.');
|
|
241
|
+
} catch (e) {
|
|
242
|
+
log('error', 'Uninstall failed: ' + (e?.message || e));
|
|
243
|
+
process.exitCode = 1;
|
|
244
|
+
}
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
if (has('--status')) {
|
|
248
|
+
let on = false;
|
|
249
|
+
try {
|
|
250
|
+
on = serviceStatus();
|
|
251
|
+
} catch (e) {
|
|
252
|
+
log('error', String(e?.message || e));
|
|
253
|
+
}
|
|
254
|
+
log('info', `auto-start: ${on ? 'installed' : 'not installed'}`);
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!runCli()) startServer();
|
package/src/service.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Background auto-start for the ChatPanel Bridge.
|
|
2
|
+
//
|
|
3
|
+
// So non-technical users never touch a terminal: download the app, run it once
|
|
4
|
+
// with --install, and it launches at login and stays running.
|
|
5
|
+
//
|
|
6
|
+
// chatpanel-bridge --install register login auto-start + start now
|
|
7
|
+
// chatpanel-bridge --uninstall remove it
|
|
8
|
+
// chatpanel-bridge --status is it registered?
|
|
9
|
+
//
|
|
10
|
+
// macOS → LaunchAgent · Windows → Scheduled Task (ONLOGON) · Linux → systemd user.
|
|
11
|
+
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
|
|
17
|
+
const LABEL = 'net.chatpanel.bridge';
|
|
18
|
+
const DISPLAY = 'ChatPanel Bridge';
|
|
19
|
+
|
|
20
|
+
// The command that launches THIS bridge. A compiled single-file binary launches
|
|
21
|
+
// itself (no args); running under node/bun launches the interpreter + this script.
|
|
22
|
+
export function resolveLaunch() {
|
|
23
|
+
const exe = process.execPath;
|
|
24
|
+
const base = path.basename(exe).toLowerCase();
|
|
25
|
+
const underInterpreter = base.startsWith('node') || base.startsWith('bun');
|
|
26
|
+
if (underInterpreter && process.argv[1]) {
|
|
27
|
+
return { program: exe, args: [path.resolve(process.argv[1])] };
|
|
28
|
+
}
|
|
29
|
+
return { program: exe, args: [] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function logPaths() {
|
|
33
|
+
const dir = path.join(os.homedir(), '.chatpanel');
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
return { out: path.join(dir, 'bridge.log'), err: path.join(dir, 'bridge.err.log') };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function run(cmd, args, opts = {}) {
|
|
39
|
+
return spawnSync(cmd, args, { encoding: 'utf8', ...opts });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------- macOS
|
|
43
|
+
const macPlist = () => path.join(os.homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
|
|
44
|
+
|
|
45
|
+
function macInstall() {
|
|
46
|
+
const { program, args } = resolveLaunch();
|
|
47
|
+
const { out, err } = logPaths();
|
|
48
|
+
const progArgs = [program, ...args].map((a) => ` <string>${a}</string>`).join('\n');
|
|
49
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
50
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
51
|
+
<plist version="1.0">
|
|
52
|
+
<dict>
|
|
53
|
+
<key>Label</key><string>${LABEL}</string>
|
|
54
|
+
<key>ProgramArguments</key>
|
|
55
|
+
<array>
|
|
56
|
+
${progArgs}
|
|
57
|
+
</array>
|
|
58
|
+
<key>RunAtLoad</key><true/>
|
|
59
|
+
<key>KeepAlive</key><true/>
|
|
60
|
+
<key>StandardOutPath</key><string>${out}</string>
|
|
61
|
+
<key>StandardErrorPath</key><string>${err}</string>
|
|
62
|
+
</dict>
|
|
63
|
+
</plist>
|
|
64
|
+
`;
|
|
65
|
+
const p = macPlist();
|
|
66
|
+
mkdirSync(path.dirname(p), { recursive: true });
|
|
67
|
+
writeFileSync(p, plist);
|
|
68
|
+
run('launchctl', ['unload', p]); // ignore if not loaded
|
|
69
|
+
const r = run('launchctl', ['load', '-w', p]);
|
|
70
|
+
if (r.status !== 0) throw new Error((r.stderr || '').trim() || 'launchctl load failed');
|
|
71
|
+
}
|
|
72
|
+
function macUninstall() {
|
|
73
|
+
const p = macPlist();
|
|
74
|
+
run('launchctl', ['unload', '-w', p]);
|
|
75
|
+
if (existsSync(p)) rmSync(p);
|
|
76
|
+
}
|
|
77
|
+
function macStatus() {
|
|
78
|
+
return (run('launchctl', ['list']).stdout || '').includes(LABEL);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------- Windows
|
|
82
|
+
const WIN_TASK = 'ChatPanelBridge';
|
|
83
|
+
|
|
84
|
+
function winInstall() {
|
|
85
|
+
const { program, args } = resolveLaunch();
|
|
86
|
+
const tr = [`"${program}"`, ...args.map((a) => `"${a}"`)].join(' ');
|
|
87
|
+
const r = run('schtasks', ['/Create', '/TN', WIN_TASK, '/TR', tr, '/SC', 'ONLOGON', '/RL', 'LIMITED', '/F']);
|
|
88
|
+
if (r.status !== 0) throw new Error((r.stderr || '').trim() || 'schtasks create failed');
|
|
89
|
+
run('schtasks', ['/Run', '/TN', WIN_TASK]); // start now
|
|
90
|
+
}
|
|
91
|
+
function winUninstall() {
|
|
92
|
+
run('schtasks', ['/Delete', '/TN', WIN_TASK, '/F']);
|
|
93
|
+
}
|
|
94
|
+
function winStatus() {
|
|
95
|
+
return run('schtasks', ['/Query', '/TN', WIN_TASK]).status === 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------- Linux (systemd user)
|
|
99
|
+
const linUnit = () => path.join(os.homedir(), '.config', 'systemd', 'user', 'chatpanel-bridge.service');
|
|
100
|
+
|
|
101
|
+
function linInstall() {
|
|
102
|
+
const { program, args } = resolveLaunch();
|
|
103
|
+
const exec = [program, ...args].map((a) => (/\s/.test(a) ? `"${a}"` : a)).join(' ');
|
|
104
|
+
const unit = `[Unit]
|
|
105
|
+
Description=${DISPLAY}
|
|
106
|
+
After=network.target
|
|
107
|
+
|
|
108
|
+
[Service]
|
|
109
|
+
ExecStart=${exec}
|
|
110
|
+
Restart=on-failure
|
|
111
|
+
|
|
112
|
+
[Install]
|
|
113
|
+
WantedBy=default.target
|
|
114
|
+
`;
|
|
115
|
+
const p = linUnit();
|
|
116
|
+
mkdirSync(path.dirname(p), { recursive: true });
|
|
117
|
+
writeFileSync(p, unit);
|
|
118
|
+
run('systemctl', ['--user', 'daemon-reload']);
|
|
119
|
+
const r = run('systemctl', ['--user', 'enable', '--now', 'chatpanel-bridge']);
|
|
120
|
+
if (r.status !== 0) throw new Error((r.stderr || '').trim() || 'systemctl enable failed');
|
|
121
|
+
}
|
|
122
|
+
function linUninstall() {
|
|
123
|
+
run('systemctl', ['--user', 'disable', '--now', 'chatpanel-bridge']);
|
|
124
|
+
const p = linUnit();
|
|
125
|
+
if (existsSync(p)) rmSync(p);
|
|
126
|
+
}
|
|
127
|
+
function linStatus() {
|
|
128
|
+
return (run('systemctl', ['--user', 'is-enabled', 'chatpanel-bridge']).stdout || '').trim() === 'enabled';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------- dispatch
|
|
132
|
+
function byPlatform(mac, win, lin) {
|
|
133
|
+
if (process.platform === 'darwin') return mac();
|
|
134
|
+
if (process.platform === 'win32') return win();
|
|
135
|
+
if (process.platform === 'linux') return lin();
|
|
136
|
+
throw new Error(`Auto-start isn't supported on ${process.platform} yet — run the bridge directly.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function installService() {
|
|
140
|
+
return byPlatform(macInstall, winInstall, linInstall);
|
|
141
|
+
}
|
|
142
|
+
export function uninstallService() {
|
|
143
|
+
return byPlatform(macUninstall, winUninstall, linUninstall);
|
|
144
|
+
}
|
|
145
|
+
export function serviceStatus() {
|
|
146
|
+
return byPlatform(macStatus, winStatus, linStatus);
|
|
147
|
+
}
|