@xingjisoft/neutrino 0.3.0 → 0.4.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 +33 -549
- package/assets/neutrino.ico +0 -0
- package/assets/neutrino.svg +7 -0
- package/assets/pro/backend_output_model.txt +5 -0
- package/assets/pro/claude-module-prompt.txt +65 -0
- package/assets/pro/common.txt +9 -0
- package/assets/pro/confirm_model.txt +5 -0
- package/assets/pro/front_output_model.txt +9 -0
- package/assets/pro/genimage_model.txt +1 -0
- package/assets/pro/main_model.txt +5 -0
- package/assets/pro/model-template.json +376 -0
- package/assets/pro/modeling_model.txt +5 -0
- package/assets/pro/plan_model.txt +8 -0
- package/assets/pro/thinking_model.txt +3 -0
- package/dist/agents.js +22 -3
- package/dist/app.js +199 -26
- package/dist/background.js +358 -0
- package/dist/bin.js +184 -44
- package/dist/browser.js +57 -0
- package/dist/config.js +174 -26
- package/dist/header.js +2 -1
- package/dist/index.js +1 -1
- package/dist/llm/approval.js +6 -2
- package/dist/llm/client.js +197 -28
- package/dist/llm/subagent.js +37 -1
- package/dist/llm/tools.js +3 -1
- package/dist/migration.js +33 -0
- package/dist/pro/agents.js +104 -0
- package/dist/pro/config.js +88 -0
- package/dist/pro/preflight.js +172 -0
- package/dist/screens/agentConversation.js +48 -5
- package/dist/screens/agentHistory.js +4 -2
- package/dist/screens/chat.js +14 -8
- package/dist/screens/modelSelect.js +15 -4
- package/dist/screens/settings.js +52 -9
- package/dist/screens/start.js +48 -15
- package/dist/state.js +1 -0
- package/dist/terminal/restore.js +3 -12
- package/dist/tray.js +138 -0
- package/dist/ui.js +58 -0
- package/dist/web/hub.js +26 -4
- package/dist/web/server.js +495 -74
- package/dist/web/service.js +135 -0
- package/dist/web/session.js +278 -25
- package/dist/web/settings.js +97 -24
- package/dist/web/terminalBridge.js +250 -0
- package/package.json +37 -37
- package/web/README.md +150 -16
- package/web/app.js +490 -25
- package/web/i18n.js +396 -345
- package/web/index.html +120 -15
- package/web/neutrino-pro.gif +0 -0
- package/web/neutrino.png +0 -0
- package/web/pro.js +184 -0
- package/web/settings.js +373 -247
- package/web/style.css +398 -0
package/dist/bin.js
CHANGED
|
@@ -3,7 +3,10 @@ import process from "node:process";
|
|
|
3
3
|
import { App } from "./app.js";
|
|
4
4
|
import { loadConfig } from "./config.js";
|
|
5
5
|
import { installCrashGuard } from "./terminal/restore.js";
|
|
6
|
-
import {
|
|
6
|
+
import { webUrls } from "./web/server.js";
|
|
7
|
+
import { runningBackground, startBackgroundService, stopBackgroundService, } from "./background.js";
|
|
8
|
+
import { runWebService } from "./web/service.js";
|
|
9
|
+
import { openInBrowser } from "./browser.js";
|
|
7
10
|
const DEFAULT_WEB_PORT = 8765;
|
|
8
11
|
/** Binding here makes the UI reachable from every interface. */
|
|
9
12
|
const WILDCARD_HOST = "0.0.0.0";
|
|
@@ -35,6 +38,11 @@ Usage:
|
|
|
35
38
|
neutrino Start the terminal UI (with the web UI).
|
|
36
39
|
neutrino --lan Start both, reachable from other devices.
|
|
37
40
|
neutrino web [--port <port>] Start only the web UI (serves saved history).
|
|
41
|
+
neutrino background start Run the web UI in the background; conversations
|
|
42
|
+
started there survive the terminal/CLI exiting.
|
|
43
|
+
neutrino background stop Stop the background service.
|
|
44
|
+
neutrino background status Show whether it is running and where.
|
|
45
|
+
neutrino background open Open its console in the default browser.
|
|
38
46
|
|
|
39
47
|
Options:
|
|
40
48
|
-p, --port <port> Web UI port. Default: ${DEFAULT_WEB_PORT}
|
|
@@ -43,18 +51,32 @@ Options:
|
|
|
43
51
|
devices on your LAN or VPN.
|
|
44
52
|
--no-web Do not start the web UI with the terminal UI.
|
|
45
53
|
--no-auth Serve without password login.
|
|
54
|
+
--no-tray Do not show the Windows notification-area icon of the
|
|
55
|
+
background service.
|
|
46
56
|
--web-port <n> Deprecated alias for --port while the TUI runs.
|
|
47
57
|
-h, --help Show this help.
|
|
48
58
|
|
|
49
59
|
Environment:
|
|
50
60
|
NEUTRINO_WEB_PORT Same as --port in either mode.
|
|
51
61
|
NEUTRINO_WEB_HOST Same as --host.
|
|
62
|
+
NEUTRINO_NO_TRAY Set to 1 to keep the background service out of the
|
|
63
|
+
Windows notification area.
|
|
52
64
|
|
|
53
65
|
The web UI starts by default alongside the terminal UI on loopback. Turn it
|
|
54
66
|
off for one run with --no-web, or permanently in Settings → Web console. It can
|
|
55
67
|
read every saved conversation and drive an agent with shell access, so it binds
|
|
56
|
-
to loopback unless you ask for --host/--lan.
|
|
57
|
-
|
|
68
|
+
to loopback unless you ask for --host/--lan. The same LAN access can be switched
|
|
69
|
+
on, and off, in the browser's Settings → Web access; the choice is saved for the
|
|
70
|
+
next launch, while --host/--lan win for the current one. Log in with the
|
|
71
|
+
permanent password (default: 114514). Change it in Settings. --no-auth disables
|
|
72
|
+
password login.
|
|
73
|
+
|
|
74
|
+
Background residency is off by default. Turn it on in Settings → Background
|
|
75
|
+
(shared by the regular and Pro workspaces). Switching it takes effect immediately,
|
|
76
|
+
including for browser tasks already running. The background service keeps
|
|
77
|
+
running when this terminal closes, so tasks started
|
|
78
|
+
from the browser are not interrupted. Stop it with 'neutrino background stop',
|
|
79
|
+
the tray icon, or Settings → Web console in the terminal.
|
|
58
80
|
`);
|
|
59
81
|
}
|
|
60
82
|
/** Read the value that follows a flag, so `--host` alone is an error. */
|
|
@@ -65,13 +87,28 @@ function requireValue(argv, index, flag) {
|
|
|
65
87
|
}
|
|
66
88
|
return value;
|
|
67
89
|
}
|
|
90
|
+
const BACKGROUND_ACTIONS = new Set(["run", "start", "stop", "status", "open"]);
|
|
68
91
|
function parseArgs(argv) {
|
|
69
|
-
const options = {
|
|
92
|
+
const options = {
|
|
93
|
+
mode: "tui", backgroundAction: null, port: null, lan: false, noWeb: false, noAuth: false, noTray: false,
|
|
94
|
+
};
|
|
70
95
|
for (let i = 0; i < argv.length; i += 1) {
|
|
71
96
|
const arg = argv[i];
|
|
72
97
|
if (arg === "web") {
|
|
73
98
|
options.mode = "web";
|
|
74
99
|
}
|
|
100
|
+
else if (arg === "background") {
|
|
101
|
+
options.mode = "background";
|
|
102
|
+
options.backgroundAction = "status";
|
|
103
|
+
const next = argv[i + 1];
|
|
104
|
+
if (next && !next.startsWith("-")) {
|
|
105
|
+
if (!BACKGROUND_ACTIONS.has(next)) {
|
|
106
|
+
throw new Error(`unknown background action "${next}"`);
|
|
107
|
+
}
|
|
108
|
+
options.backgroundAction = next;
|
|
109
|
+
i += 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
75
112
|
else if (arg === "--port" || arg === "-p") {
|
|
76
113
|
options.port = parsePort(argv[++i]);
|
|
77
114
|
}
|
|
@@ -99,6 +136,25 @@ function parseArgs(argv) {
|
|
|
99
136
|
else if (arg === "--no-auth") {
|
|
100
137
|
options.noAuth = true;
|
|
101
138
|
}
|
|
139
|
+
else if (arg === "--no-tray") {
|
|
140
|
+
options.noTray = true;
|
|
141
|
+
}
|
|
142
|
+
else if (arg === "--start") {
|
|
143
|
+
options.mode = "background";
|
|
144
|
+
options.backgroundAction = "start";
|
|
145
|
+
}
|
|
146
|
+
else if (arg === "--stop") {
|
|
147
|
+
options.mode = "background";
|
|
148
|
+
options.backgroundAction = "stop";
|
|
149
|
+
}
|
|
150
|
+
else if (arg === "--status") {
|
|
151
|
+
options.mode = "background";
|
|
152
|
+
options.backgroundAction = "status";
|
|
153
|
+
}
|
|
154
|
+
else if (arg === "--open") {
|
|
155
|
+
options.mode = "background";
|
|
156
|
+
options.backgroundAction = "open";
|
|
157
|
+
}
|
|
102
158
|
else if (arg === "--help" || arg === "-h") {
|
|
103
159
|
printHelp();
|
|
104
160
|
process.exit(0);
|
|
@@ -113,31 +169,64 @@ function parseArgs(argv) {
|
|
|
113
169
|
if (options.noWeb && options.mode === "web") {
|
|
114
170
|
throw new Error("--no-web cannot be used with the 'web' command");
|
|
115
171
|
}
|
|
172
|
+
if (options.noWeb && options.mode === "background") {
|
|
173
|
+
throw new Error("--no-web cannot be used with the 'background' command");
|
|
174
|
+
}
|
|
116
175
|
if (options.noWeb && (options.port !== null || options.host !== undefined || options.lan)) {
|
|
117
176
|
throw new Error("use either --no-web or web UI options, not both");
|
|
118
177
|
}
|
|
119
178
|
return options;
|
|
120
179
|
}
|
|
121
|
-
/** Print
|
|
122
|
-
function
|
|
123
|
-
process.stdout.write(`Neutrino
|
|
124
|
-
|
|
180
|
+
/** Print where an already-running detached console can be opened. */
|
|
181
|
+
function reportBackgroundAccess(status, started) {
|
|
182
|
+
process.stdout.write(`Neutrino ${status.resident ? "background " : ""}Web console ${started ? "started" : "already running"} `
|
|
183
|
+
+ `(pid ${status.pid}) on ${status.host}:${status.port}\n`);
|
|
184
|
+
const { urls } = webUrls(status.host, status.port);
|
|
185
|
+
const multiple = urls.length > 1;
|
|
125
186
|
process.stdout.write(multiple ? "Open the address that matches your device:\n" : "Open:\n");
|
|
126
|
-
for (const url of
|
|
187
|
+
for (const url of urls)
|
|
127
188
|
process.stdout.write(` ${url}\n`);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// Bound to every interface, yet nothing outside this machine is reachable:
|
|
131
|
-
// usually Wi-Fi is off, the VPN is down, or the only adapters left are
|
|
132
|
-
// virtual ones we deliberately hide.
|
|
189
|
+
if ((status.host === "0.0.0.0" || status.host === "::")
|
|
190
|
+
&& !urls.some((url) => !url.includes("//127.0.0.1:"))) {
|
|
133
191
|
process.stdout.write("note: no reachable network address found, so only this machine can connect.\n");
|
|
134
192
|
}
|
|
135
|
-
if (
|
|
136
|
-
process.stdout.write("Sign in with your permanent Web password (default: 114514)
|
|
193
|
+
if (status.auth_required) {
|
|
194
|
+
process.stdout.write("Sign in with your permanent Web password (default: 114514).\n");
|
|
137
195
|
}
|
|
138
196
|
else {
|
|
139
197
|
process.stdout.write("Password login is disabled.\n");
|
|
140
198
|
}
|
|
199
|
+
if (status.resident) {
|
|
200
|
+
process.stdout.write("It keeps running after this terminal exits. Stop it with 'neutrino background stop'"
|
|
201
|
+
+ (process.platform === "win32" ? " or the tray icon" : "") + ".\n");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The detached service itself (`neutrino background run`).
|
|
206
|
+
*
|
|
207
|
+
* It owns the Web console and every browser conversation for as long as the
|
|
208
|
+
* machine's user session keeps it alive. The token written into the status
|
|
209
|
+
* file is the only way another local process can ask it to stop.
|
|
210
|
+
*/
|
|
211
|
+
async function runBackgroundService(host, auth) {
|
|
212
|
+
await runWebService({ port: options.port ?? portFromEnv ?? DEFAULT_WEB_PORT, host, auth, tray: !options.noTray });
|
|
213
|
+
}
|
|
214
|
+
function printBackgroundStatus() {
|
|
215
|
+
const status = runningBackground();
|
|
216
|
+
if (!status) {
|
|
217
|
+
process.stdout.write("No background service is running.\n");
|
|
218
|
+
process.stdout.write("Start one with 'neutrino background start'.\n");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
process.stdout.write(`Neutrino background service: running (pid ${status.pid})\n`);
|
|
222
|
+
process.stdout.write(`Residency: ${status.resident ? "on" : "off (follows the terminal)"}\n`);
|
|
223
|
+
process.stdout.write(`Started: ${status.started_at}\n`);
|
|
224
|
+
process.stdout.write(`Address: ${status.url}\n`);
|
|
225
|
+
const { urls } = webUrls(status.host, status.port);
|
|
226
|
+
for (const url of urls) {
|
|
227
|
+
if (url.replace(/\/$/, "") !== status.url)
|
|
228
|
+
process.stdout.write(`Also: ${url}\n`);
|
|
229
|
+
}
|
|
141
230
|
}
|
|
142
231
|
let options;
|
|
143
232
|
try {
|
|
@@ -149,29 +238,82 @@ catch (err) {
|
|
|
149
238
|
process.exit(2);
|
|
150
239
|
}
|
|
151
240
|
// Installed before either mode starts: a crash must leave a usable terminal
|
|
152
|
-
// (TUI) and a log line (both), never a shell flooded with mouse reports.
|
|
153
|
-
|
|
241
|
+
// (TUI) and a log line (both), never a shell flooded with mouse reports. A
|
|
242
|
+
// detached service keeps its own SIGHUP handler so closing the terminal cannot
|
|
243
|
+
// take it down.
|
|
244
|
+
installCrashGuard({ surviveHangup: options.mode === "background" });
|
|
154
245
|
const host = options.lan ? WILDCARD_HOST : options.host || process.env.NEUTRINO_WEB_HOST || undefined;
|
|
155
246
|
const auth = !options.noAuth;
|
|
156
247
|
const portFromEnv = envPort();
|
|
157
248
|
if (options.mode === "web") {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
process.
|
|
165
|
-
const shutdown = () => {
|
|
166
|
-
handle.close().finally(() => process.exit(0));
|
|
167
|
-
};
|
|
168
|
-
process.on("SIGINT", shutdown);
|
|
169
|
-
process.on("SIGTERM", shutdown);
|
|
170
|
-
}
|
|
171
|
-
catch (err) {
|
|
172
|
-
process.stderr.write(`neutrino: failed to start web server: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
249
|
+
const result = await startBackgroundService({
|
|
250
|
+
port: options.port ?? portFromEnv ?? DEFAULT_WEB_PORT, host, auth,
|
|
251
|
+
tray: !options.noTray, resident: loadConfig().background === true, ownerPid: process.pid,
|
|
252
|
+
strictPort: options.port !== null,
|
|
253
|
+
});
|
|
254
|
+
if (!result.ok) {
|
|
255
|
+
process.stderr.write(`neutrino: failed to start web server: ${result.error}\n`);
|
|
173
256
|
process.exit(1);
|
|
174
257
|
}
|
|
258
|
+
reportBackgroundAccess(result.status, result.started);
|
|
259
|
+
process.stdout.write("Serving saved conversations. Press Ctrl+C to close this terminal.\n");
|
|
260
|
+
// The owner monitor in the service also handles an abrupt terminal close.
|
|
261
|
+
const keepAlive = setInterval(() => { }, 60_000);
|
|
262
|
+
const shutdown = () => { clearInterval(keepAlive); process.exit(0); };
|
|
263
|
+
process.on("SIGINT", shutdown);
|
|
264
|
+
process.on("SIGTERM", shutdown);
|
|
265
|
+
}
|
|
266
|
+
else if (options.mode === "background") {
|
|
267
|
+
const action = options.backgroundAction ?? "status";
|
|
268
|
+
if (action === "run") {
|
|
269
|
+
// The detached child. It does not own a terminal, so its stdio is ignored
|
|
270
|
+
// by the parent and every diagnostic goes to background.log.
|
|
271
|
+
await runBackgroundService(host, auth);
|
|
272
|
+
}
|
|
273
|
+
else if (action === "start") {
|
|
274
|
+
const result = await startBackgroundService({
|
|
275
|
+
port: options.port ?? portFromEnv ?? DEFAULT_WEB_PORT,
|
|
276
|
+
host,
|
|
277
|
+
auth,
|
|
278
|
+
tray: !options.noTray,
|
|
279
|
+
});
|
|
280
|
+
if (!result.ok) {
|
|
281
|
+
process.stderr.write(`neutrino: ${result.error}\n`);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
reportBackgroundAccess(result.status, result.started);
|
|
285
|
+
process.exitCode = 0;
|
|
286
|
+
}
|
|
287
|
+
else if (action === "stop") {
|
|
288
|
+
const result = await stopBackgroundService();
|
|
289
|
+
if (!result.ok) {
|
|
290
|
+
process.stderr.write(`neutrino: ${result.error}\n`);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
process.stdout.write(result.stopped ? "Background service stopped.\n" : "No background service is running.\n");
|
|
294
|
+
process.exitCode = 0;
|
|
295
|
+
}
|
|
296
|
+
else if (action === "open") {
|
|
297
|
+
const started = await startBackgroundService({
|
|
298
|
+
port: options.port ?? portFromEnv ?? DEFAULT_WEB_PORT,
|
|
299
|
+
host,
|
|
300
|
+
auth,
|
|
301
|
+
tray: !options.noTray,
|
|
302
|
+
});
|
|
303
|
+
if (!started.ok) {
|
|
304
|
+
process.stderr.write(`neutrino: ${started.error}\n`);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
if (started.started)
|
|
308
|
+
reportBackgroundAccess(started.status, true);
|
|
309
|
+
openInBrowser(started.status.url);
|
|
310
|
+
process.stdout.write(`Opening ${started.status.url}\n`);
|
|
311
|
+
process.exitCode = 0;
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
printBackgroundStatus();
|
|
315
|
+
process.exitCode = 0;
|
|
316
|
+
}
|
|
175
317
|
}
|
|
176
318
|
else {
|
|
177
319
|
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
@@ -181,22 +323,20 @@ else {
|
|
|
181
323
|
}
|
|
182
324
|
const app = new App();
|
|
183
325
|
const webPort = options.port ?? portFromEnv ?? DEFAULT_WEB_PORT;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
app.configureWeb(webPort, host, auth);
|
|
187
|
-
// Explicit web settings win over the saved preference; otherwise the web UI
|
|
188
|
-
// is part of a normal launch unless the user turned it off in Settings.
|
|
326
|
+
const saved = loadConfig();
|
|
327
|
+
app.configureWeb(webPort, host, auth, true, !options.noTray);
|
|
189
328
|
const requested = options.port !== null || options.host !== undefined || options.lan
|
|
190
329
|
|| options.noAuth || portFromEnv !== null || Boolean(process.env.NEUTRINO_WEB_HOST);
|
|
191
|
-
|
|
192
|
-
if (!options.noWeb && (requested || enabledByDefault)) {
|
|
330
|
+
if (!options.noWeb && (requested || saved.web_console !== false)) {
|
|
193
331
|
try {
|
|
194
|
-
const
|
|
195
|
-
|
|
332
|
+
const previous = runningBackground();
|
|
333
|
+
const status = await app.enableWeb(webPort, host, auth);
|
|
334
|
+
if (options.port !== null && options.port !== 0 && status.port !== webPort) {
|
|
335
|
+
process.stderr.write(`note: the Web console is already running on port ${status.port}; --port was not applied.\n`);
|
|
336
|
+
}
|
|
337
|
+
reportBackgroundAccess(status, previous?.pid !== status.pid);
|
|
196
338
|
}
|
|
197
339
|
catch (err) {
|
|
198
|
-
// A console that cannot bind (usually a second instance on 8765) must
|
|
199
|
-
// never take the terminal down with it.
|
|
200
340
|
const detail = err instanceof Error ? err.message : String(err);
|
|
201
341
|
process.stderr.write(`neutrino: web UI not started: ${detail}\n`);
|
|
202
342
|
process.stderr.write("The terminal UI will keep running. Pass --port <port> to use another port, or --no-web to skip the web UI.\n");
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
/**
|
|
4
|
+
* Where the browser console's Pro workspace lives, given the console's base
|
|
5
|
+
* URL.
|
|
6
|
+
*
|
|
7
|
+
* The base comes straight from the running server, but it is normalised here
|
|
8
|
+
* anyway: a trailing slash must not produce `//pro`, and a URL that already
|
|
9
|
+
* points at `/pro` must not become `/pro/pro`. A missing/blank base yields the
|
|
10
|
+
* path alone instead of throwing, so a half-configured console can still be
|
|
11
|
+
* reported instead of crashing the click that asked for it.
|
|
12
|
+
*/
|
|
13
|
+
export function proModeUrl(baseUrl) {
|
|
14
|
+
const base = String(baseUrl ?? "").trim().replace(/\/+$/, "");
|
|
15
|
+
if (/\/pro$/i.test(base))
|
|
16
|
+
return base;
|
|
17
|
+
return `${base}/pro`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Open `url` in the user's default browser. Fire-and-forget: the child is
|
|
21
|
+
* detached, its stdio is ignored and it is unref'd so the terminal is never
|
|
22
|
+
* held open (or blocked) by the browser. Every failure is swallowed — opening
|
|
23
|
+
* a browser is best-effort and must never take the TUI down.
|
|
24
|
+
*/
|
|
25
|
+
export function openInBrowser(url) {
|
|
26
|
+
try {
|
|
27
|
+
let command;
|
|
28
|
+
let args;
|
|
29
|
+
if (process.platform === "win32") {
|
|
30
|
+
// `start` is a cmd built-in. The empty "" is the (required) window title,
|
|
31
|
+
// so a quoted URL is never mistaken for one.
|
|
32
|
+
command = "cmd";
|
|
33
|
+
args = ["/c", "start", "", url];
|
|
34
|
+
}
|
|
35
|
+
else if (process.platform === "darwin") {
|
|
36
|
+
command = "open";
|
|
37
|
+
args = [url];
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
command = "xdg-open";
|
|
41
|
+
args = [url];
|
|
42
|
+
}
|
|
43
|
+
const child = spawn(command, args, {
|
|
44
|
+
detached: true,
|
|
45
|
+
stdio: "ignore",
|
|
46
|
+
windowsHide: true,
|
|
47
|
+
});
|
|
48
|
+
// A missing opener (xdg-open is not installed everywhere) is reported
|
|
49
|
+
// asynchronously; without a listener that 'error' event would crash the
|
|
50
|
+
// whole process.
|
|
51
|
+
child.on("error", () => { });
|
|
52
|
+
child.unref();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Best-effort only.
|
|
56
|
+
}
|
|
57
|
+
}
|