@amd-gaia/agent-ui 0.17.3 → 0.17.4
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/dist/assets/{index-B4Qzv7Ys.js → index-iAjQas0m.js} +78 -78
- package/dist/index.html +1 -1
- package/main.cjs +226 -43
- package/package.json +1 -1
- package/services/backend-installer.cjs +325 -62
- package/services/port-manager.cjs +234 -0
package/dist/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/png" href="./favicon.png" />
|
|
7
7
|
<title>GAIA</title>
|
|
8
|
-
<script type="module" crossorigin src="./assets/index-
|
|
8
|
+
<script type="module" crossorigin src="./assets/index-iAjQas0m.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="./assets/index-eQemgF08.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/main.cjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// services/notification-service.js — Desktop notifications + permission prompts (T5)
|
|
14
14
|
// preload.cjs — contextBridge for IPC channels (T0/T1)
|
|
15
15
|
|
|
16
|
-
const { app, BrowserWindow, shell } = require("electron");
|
|
16
|
+
const { app, BrowserWindow, dialog, shell } = require("electron");
|
|
17
17
|
const path = require("path");
|
|
18
18
|
const fs = require("fs");
|
|
19
19
|
const os = require("os");
|
|
@@ -23,16 +23,106 @@ const { spawn } = require("child_process");
|
|
|
23
23
|
const TrayManager = require("./services/tray-manager.cjs");
|
|
24
24
|
const AgentProcessManager = require("./services/agent-process-manager.cjs");
|
|
25
25
|
const NotificationService = require("./services/notification-service.cjs");
|
|
26
|
+
const PortManager = require("./services/port-manager.cjs");
|
|
26
27
|
const backendInstaller = require("./services/backend-installer.cjs");
|
|
27
28
|
const installerProgressDialog = require("./services/backend-installer-progress-dialog.cjs");
|
|
28
29
|
const autoUpdater = require("./services/auto-updater.cjs");
|
|
29
30
|
const agentSeeder = require("./services/agent-seeder.cjs");
|
|
30
31
|
|
|
32
|
+
// ── F7: Ozone hint (issue #782) ─────────────────────────────────────────────
|
|
33
|
+
// Electron-recommended switch for distro-agnostic Linux behaviour: picks
|
|
34
|
+
// Wayland on Wayland sessions, X11 elsewhere. Must be set before
|
|
35
|
+
// app.whenReady() fires.
|
|
36
|
+
app.commandLine.appendSwitch("ozone-platform-hint", "auto");
|
|
37
|
+
|
|
38
|
+
// ── F7: --no-sandbox on Linux (issue #782) ───────────────────────────────────
|
|
39
|
+
// chrome-sandbox is deleted from the packaged tree by after-pack.cjs so
|
|
40
|
+
// Chromium must use its unprivileged user-namespace sandbox on every launch
|
|
41
|
+
// path. The .desktop Exec= line already carries --no-sandbox via
|
|
42
|
+
// electron-builder.yml linux.executableArgs, but direct `./GAIA.AppImage`
|
|
43
|
+
// invocations bypass the .desktop entry. Appending the switch here makes
|
|
44
|
+
// all Linux launch paths behave identically.
|
|
45
|
+
if (process.platform === "linux") {
|
|
46
|
+
app.commandLine.appendSwitch("no-sandbox");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── F7: Log tee to ~/.gaia/electron-main.log (issue #782) ───────────────────
|
|
50
|
+
// Users often launch AppImages by double-click, not from a terminal, so
|
|
51
|
+
// console output vanishes. Mirror console.log/error to a file so the
|
|
52
|
+
// diagnostics bundler has something to attach.
|
|
53
|
+
(function installMainLogTee() {
|
|
54
|
+
try {
|
|
55
|
+
const gaiaDir = path.join(os.homedir(), ".gaia");
|
|
56
|
+
try { fs.mkdirSync(gaiaDir, { recursive: true }); } catch { /* ignore */ }
|
|
57
|
+
const logPath = path.join(gaiaDir, "electron-main.log");
|
|
58
|
+
|
|
59
|
+
// Rotate if > 5 MB — truncate to last ~5 MB on startup.
|
|
60
|
+
try {
|
|
61
|
+
const st = fs.statSync(logPath);
|
|
62
|
+
if (st.size > 5 * 1024 * 1024) {
|
|
63
|
+
const fd = fs.openSync(logPath, "r");
|
|
64
|
+
const keep = 5 * 1024 * 1024;
|
|
65
|
+
const buf = Buffer.alloc(keep);
|
|
66
|
+
// readSync can return fewer bytes than requested; only write
|
|
67
|
+
// what we actually read so we don't append zero-padding.
|
|
68
|
+
const bytesRead = fs.readSync(
|
|
69
|
+
fd,
|
|
70
|
+
buf,
|
|
71
|
+
0,
|
|
72
|
+
keep,
|
|
73
|
+
Math.max(0, st.size - keep),
|
|
74
|
+
);
|
|
75
|
+
fs.closeSync(fd);
|
|
76
|
+
fs.writeFileSync(logPath, buf.subarray(0, bytesRead));
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// ENOENT or permission — best-effort; just fall through.
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const stream = fs.createWriteStream(logPath, { flags: "a" });
|
|
83
|
+
stream.write(
|
|
84
|
+
`\n──── electron-main opened (${new Date().toISOString()}) pid=${process.pid} ────\n`
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const flushAndEnd = () => {
|
|
88
|
+
try { stream.end(); } catch { /* ignore */ }
|
|
89
|
+
};
|
|
90
|
+
process.on("exit", flushAndEnd);
|
|
91
|
+
|
|
92
|
+
const wrap = (origFn, level) => (...args) => {
|
|
93
|
+
try {
|
|
94
|
+
const line = args
|
|
95
|
+
.map((a) =>
|
|
96
|
+
typeof a === "string"
|
|
97
|
+
? a
|
|
98
|
+
: a instanceof Error
|
|
99
|
+
? (a.stack || a.message || String(a))
|
|
100
|
+
: JSON.stringify(a)
|
|
101
|
+
)
|
|
102
|
+
.join(" ");
|
|
103
|
+
stream.write(`[${new Date().toISOString()}] ${level} ${line}\n`);
|
|
104
|
+
} catch {
|
|
105
|
+
// swallow — we must not recurse into console.error from here
|
|
106
|
+
}
|
|
107
|
+
return origFn.apply(console, args);
|
|
108
|
+
};
|
|
109
|
+
console.log = wrap(console.log.bind(console), "INFO");
|
|
110
|
+
console.warn = wrap(console.warn.bind(console), "WARN");
|
|
111
|
+
console.error = wrap(console.error.bind(console), "ERROR");
|
|
112
|
+
} catch (err) {
|
|
113
|
+
// If this fails, we silently keep the original console — the app must
|
|
114
|
+
// not refuse to launch over a log-tee failure.
|
|
115
|
+
process.stderr.write(`[main] log tee failed: ${err.message}\n`);
|
|
116
|
+
}
|
|
117
|
+
})();
|
|
118
|
+
|
|
31
119
|
// ── Configuration ──────────────────────────────────────────────────────────
|
|
32
120
|
|
|
33
121
|
const APP_NAME = "GAIA";
|
|
34
|
-
|
|
35
|
-
|
|
122
|
+
// Default fallback only — at runtime we always allocate a free random port
|
|
123
|
+
// via PortManager.findFreePort() to avoid EADDRINUSE on zombie backends
|
|
124
|
+
// from prior aborted sessions (issue #782 / T5).
|
|
125
|
+
const DEFAULT_BACKEND_PORT = 4200;
|
|
36
126
|
const STARTUP_TIMEOUT = 30000;
|
|
37
127
|
|
|
38
128
|
// Parse CLI args (T11: --minimized flag for auto-start)
|
|
@@ -59,6 +149,10 @@ const windowConfig = appConfig.window || {
|
|
|
59
149
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
60
150
|
|
|
61
151
|
let backendProcess = null;
|
|
152
|
+
let backendPort = DEFAULT_BACKEND_PORT;
|
|
153
|
+
let healthCheckUrl = `http://localhost:${backendPort}/api/health`;
|
|
154
|
+
let backendStderrTail = [];
|
|
155
|
+
let isIntentionalKill = false;
|
|
62
156
|
let mainWindow = null;
|
|
63
157
|
|
|
64
158
|
/** @type {TrayManager | null} */
|
|
@@ -70,6 +164,11 @@ let agentProcessManager = null;
|
|
|
70
164
|
/** @type {NotificationService | null} */
|
|
71
165
|
let notificationService = null;
|
|
72
166
|
|
|
167
|
+
/** @type {PortManager} */
|
|
168
|
+
const portManager = new PortManager({
|
|
169
|
+
logger: { log: console.log.bind(console), error: console.error.bind(console) },
|
|
170
|
+
});
|
|
171
|
+
|
|
73
172
|
/**
|
|
74
173
|
* Set to true when the user explicitly quits (via tray "Quit" or Cmd+Q).
|
|
75
174
|
* Prevents minimize-to-tray from intercepting the close event.
|
|
@@ -86,7 +185,7 @@ let isQuitting = false;
|
|
|
86
185
|
* Returns the ChildProcess, or null if the gaia binary cannot be found
|
|
87
186
|
* (shouldn't happen post-ensureBackend, but we guard just in case).
|
|
88
187
|
*/
|
|
89
|
-
function startBackend() {
|
|
188
|
+
async function startBackend() {
|
|
90
189
|
const gaiaCmd = backendInstaller.findGaiaBin();
|
|
91
190
|
|
|
92
191
|
if (!gaiaCmd) {
|
|
@@ -96,11 +195,27 @@ function startBackend() {
|
|
|
96
195
|
return null;
|
|
97
196
|
}
|
|
98
197
|
|
|
99
|
-
|
|
198
|
+
// F5: always spawn on a free random port. Never reuse/probe — the
|
|
199
|
+
// probe-and-reuse path is spoofable and leaves orphans (issue #782).
|
|
200
|
+
try {
|
|
201
|
+
backendPort = await portManager.findFreePort();
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.warn(
|
|
204
|
+
`[main] findFreePort failed (${err.message}); falling back to ${DEFAULT_BACKEND_PORT}`
|
|
205
|
+
);
|
|
206
|
+
backendPort = DEFAULT_BACKEND_PORT;
|
|
207
|
+
}
|
|
208
|
+
healthCheckUrl = `http://localhost:${backendPort}/api/health`;
|
|
209
|
+
|
|
210
|
+
console.log(`Starting backend: ${gaiaCmd} chat --ui --ui-port ${backendPort}`);
|
|
211
|
+
|
|
212
|
+
// Reset per-spawn state so a fresh crash dialog doesn't mix tails.
|
|
213
|
+
backendStderrTail = [];
|
|
214
|
+
isIntentionalKill = false;
|
|
100
215
|
|
|
101
216
|
const child = spawn(
|
|
102
217
|
gaiaCmd,
|
|
103
|
-
["chat", "--ui", "--ui-port", String(
|
|
218
|
+
["chat", "--ui", "--ui-port", String(backendPort)],
|
|
104
219
|
{
|
|
105
220
|
cwd: os.homedir(), // Electron's cwd is "/" on macOS when launched from Finder
|
|
106
221
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -116,24 +231,85 @@ function startBackend() {
|
|
|
116
231
|
});
|
|
117
232
|
|
|
118
233
|
child.stderr.on("data", (data) => {
|
|
119
|
-
const
|
|
120
|
-
|
|
234
|
+
const chunk = data.toString();
|
|
235
|
+
chunk.split(/\r?\n/).forEach((line) => {
|
|
236
|
+
if (!line) return;
|
|
237
|
+
console.log(`[backend] ${line}`);
|
|
238
|
+
// Cap per-line length so pathological no-newline backend output
|
|
239
|
+
// can't balloon the in-memory tail or the crash-dialog body.
|
|
240
|
+
const capped =
|
|
241
|
+
line.length > 2048 ? line.slice(0, 2048) + "…[truncated]" : line;
|
|
242
|
+
backendStderrTail.push(capped);
|
|
243
|
+
if (backendStderrTail.length > 20) backendStderrTail.shift();
|
|
244
|
+
});
|
|
121
245
|
});
|
|
122
246
|
|
|
123
247
|
child.on("error", (err) => {
|
|
124
248
|
console.error("Failed to start backend:", err.message);
|
|
125
249
|
});
|
|
126
250
|
|
|
127
|
-
child.on("exit", (code) => {
|
|
251
|
+
child.on("exit", (code, signal) => {
|
|
128
252
|
if (code !== 0 && code !== null) {
|
|
129
|
-
console.error(`Backend exited with code ${code}`);
|
|
253
|
+
console.error(`Backend exited with code ${code} (signal=${signal})`);
|
|
130
254
|
}
|
|
255
|
+
const crashed = !isIntentionalKill && code !== 0 && code !== null;
|
|
131
256
|
backendProcess = null;
|
|
257
|
+
|
|
258
|
+
if (crashed && !isQuitting) {
|
|
259
|
+
// Fire-and-forget — don't block the event loop.
|
|
260
|
+
void handleBackendCrash(code, signal);
|
|
261
|
+
}
|
|
132
262
|
});
|
|
133
263
|
|
|
134
264
|
return child;
|
|
135
265
|
}
|
|
136
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Show a user-facing crash dialog with the last ~20 stderr lines and
|
|
269
|
+
* offer to write a diagnostics bundle. Invoked from child.on("exit")
|
|
270
|
+
* when the kill was NOT initiated by us.
|
|
271
|
+
*/
|
|
272
|
+
async function handleBackendCrash(code, signal) {
|
|
273
|
+
const tail = backendStderrTail.slice(-20).join("\n") || "(no stderr captured)";
|
|
274
|
+
const detail =
|
|
275
|
+
`The GAIA backend exited unexpectedly (code=${code}, signal=${signal}).\n\n` +
|
|
276
|
+
`Recent log output:\n${tail}`;
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const choice = await dialog.showMessageBox({
|
|
280
|
+
type: "error",
|
|
281
|
+
title: "GAIA backend crashed",
|
|
282
|
+
message: "GAIA backend crashed",
|
|
283
|
+
detail,
|
|
284
|
+
buttons: ["Copy diagnostics", "Quit"],
|
|
285
|
+
defaultId: 0,
|
|
286
|
+
cancelId: 1,
|
|
287
|
+
noLink: true,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
if (choice.response === 0) {
|
|
291
|
+
try {
|
|
292
|
+
const bundlePath = await portManager.writeDiagnosticsBundle();
|
|
293
|
+
await dialog.showMessageBox({
|
|
294
|
+
type: "info",
|
|
295
|
+
title: "Diagnostics saved",
|
|
296
|
+
message: "Diagnostics bundle written",
|
|
297
|
+
detail: `Attach this file to your bug report:\n${bundlePath}`,
|
|
298
|
+
buttons: ["OK"],
|
|
299
|
+
});
|
|
300
|
+
} catch (err) {
|
|
301
|
+
console.error("[main] Could not write diagnostics bundle:", err.message);
|
|
302
|
+
dialog.showErrorBox(
|
|
303
|
+
"Could not write diagnostics",
|
|
304
|
+
`Failed to write diagnostics bundle: ${err.message}`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} catch (err) {
|
|
309
|
+
console.error("[main] handleBackendCrash failed:", err.message);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
137
313
|
async function waitForBackend(timeoutMs) {
|
|
138
314
|
const start = Date.now();
|
|
139
315
|
const http = require("http");
|
|
@@ -141,7 +317,7 @@ async function waitForBackend(timeoutMs) {
|
|
|
141
317
|
while (Date.now() - start < timeoutMs) {
|
|
142
318
|
try {
|
|
143
319
|
await new Promise((resolve, reject) => {
|
|
144
|
-
const req = http.get(
|
|
320
|
+
const req = http.get(healthCheckUrl, (res) => {
|
|
145
321
|
if (res.statusCode === 200) {
|
|
146
322
|
resolve();
|
|
147
323
|
} else {
|
|
@@ -221,10 +397,12 @@ function createWindow() {
|
|
|
221
397
|
|
|
222
398
|
// Show window when ready (unless --minimized or startMinimized config)
|
|
223
399
|
mainWindow.once("ready-to-show", () => {
|
|
400
|
+
console.log("[main] ready-to-show fired");
|
|
224
401
|
const shouldStartMinimized =
|
|
225
402
|
startMinimized || (trayManager && trayManager.startMinimized);
|
|
226
403
|
|
|
227
404
|
if (!shouldStartMinimized) {
|
|
405
|
+
console.log("[main] mainWindow.show() called");
|
|
228
406
|
mainWindow.show();
|
|
229
407
|
} else {
|
|
230
408
|
console.log("[main] Starting minimized to tray");
|
|
@@ -241,9 +419,15 @@ async function loadApp() {
|
|
|
241
419
|
// Always load the bundled frontend from the asar. The backend only
|
|
242
420
|
// serves the API (no frontend files in the pip package), so loading
|
|
243
421
|
// http://localhost:4200/ would show raw JSON instead of the UI.
|
|
422
|
+
//
|
|
423
|
+
// Pass the real backend base URL as a query parameter so the renderer
|
|
424
|
+
// can reach whatever random port port-manager picked (see #851).
|
|
425
|
+
// Without this, the compiled bundle falls back to its hardcoded
|
|
426
|
+
// `http://localhost:4200/api` and every API call 404s.
|
|
244
427
|
const indexPath = path.join(distPath, "index.html");
|
|
245
|
-
|
|
246
|
-
|
|
428
|
+
const apiBase = `http://127.0.0.1:${backendPort}/api`;
|
|
429
|
+
console.log("Loading app from:", indexPath, "api:", apiBase);
|
|
430
|
+
await mainWindow.loadFile(indexPath, { query: { api: apiBase } });
|
|
247
431
|
} else {
|
|
248
432
|
// Show a simple loading/error page
|
|
249
433
|
mainWindow.loadURL(
|
|
@@ -254,7 +438,7 @@ async function loadApp() {
|
|
|
254
438
|
<div style="text-align:center;">
|
|
255
439
|
<h1>${APP_NAME}</h1>
|
|
256
440
|
<p>Waiting for backend to start...</p>
|
|
257
|
-
<p style="color:#888; font-size:12px;">Backend: http://localhost:${
|
|
441
|
+
<p style="color:#888; font-size:12px;">Backend: http://localhost:${backendPort}</p>
|
|
258
442
|
</div>
|
|
259
443
|
</body>
|
|
260
444
|
</html>`
|
|
@@ -271,7 +455,7 @@ function initializeServices() {
|
|
|
271
455
|
agentProcessManager = new AgentProcessManager(mainWindow);
|
|
272
456
|
|
|
273
457
|
// T1: Tray Manager (system tray icon + context menu)
|
|
274
|
-
trayManager = new TrayManager(mainWindow, { backendPort
|
|
458
|
+
trayManager = new TrayManager(mainWindow, { backendPort });
|
|
275
459
|
trayManager.create();
|
|
276
460
|
|
|
277
461
|
// T5: Notification Service (routes agent notifications to OS + renderer)
|
|
@@ -369,6 +553,7 @@ async function bootstrapBackend() {
|
|
|
369
553
|
try {
|
|
370
554
|
await backendInstaller.ensureBackend({
|
|
371
555
|
onProgress: progress.onProgress,
|
|
556
|
+
isPackaged: app.isPackaged,
|
|
372
557
|
});
|
|
373
558
|
progress.close();
|
|
374
559
|
console.log("[main] Backend bootstrap complete");
|
|
@@ -484,7 +669,7 @@ app.whenReady().then(async () => {
|
|
|
484
669
|
}
|
|
485
670
|
|
|
486
671
|
// Start the Python backend
|
|
487
|
-
backendProcess = startBackend();
|
|
672
|
+
backendProcess = await startBackend();
|
|
488
673
|
|
|
489
674
|
// Create the window (hidden until ready-to-show)
|
|
490
675
|
createWindow();
|
|
@@ -520,7 +705,7 @@ app.whenReady().then(async () => {
|
|
|
520
705
|
console.log("Waiting for backend to start...");
|
|
521
706
|
const ready = await waitForBackend(STARTUP_TIMEOUT);
|
|
522
707
|
if (ready) {
|
|
523
|
-
console.log("Backend API is ready on port",
|
|
708
|
+
console.log("Backend API is ready on port", backendPort);
|
|
524
709
|
} else {
|
|
525
710
|
console.warn("Backend did not respond within timeout.");
|
|
526
711
|
}
|
|
@@ -575,6 +760,9 @@ let cleanupDone = false;
|
|
|
575
760
|
|
|
576
761
|
app.on("before-quit", () => {
|
|
577
762
|
isQuitting = true;
|
|
763
|
+
// Mark any subsequent backend exit as intentional so we don't pop the
|
|
764
|
+
// crash dialog during normal shutdown.
|
|
765
|
+
isIntentionalKill = true;
|
|
578
766
|
});
|
|
579
767
|
|
|
580
768
|
app.on("will-quit", (event) => {
|
|
@@ -628,39 +816,34 @@ async function cleanup() {
|
|
|
628
816
|
trayManager = null;
|
|
629
817
|
}
|
|
630
818
|
|
|
631
|
-
// Stop the Python backend
|
|
819
|
+
// Stop the Python backend via the port-manager (SIGTERM → wait 3 s → SIGKILL).
|
|
820
|
+
// F5: previously we did this inline; extracted so main.cjs stays lean and
|
|
821
|
+
// to prevent orphan leaks across runs (issue #782).
|
|
632
822
|
if (backendProcess) {
|
|
633
823
|
console.log("Stopping backend process...");
|
|
634
|
-
const proc = backendProcess;
|
|
824
|
+
const proc = backendProcess;
|
|
635
825
|
backendProcess = null;
|
|
826
|
+
isIntentionalKill = true;
|
|
636
827
|
|
|
637
828
|
try {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
//
|
|
829
|
+
// Pass the ChildProcess handle (not a bare pid) so port-manager
|
|
830
|
+
// can short-circuit via `proc.exitCode` and close the PID-reuse
|
|
831
|
+
// TOCTOU window.
|
|
832
|
+
await portManager.killBackend(proc);
|
|
833
|
+
} catch (err) {
|
|
834
|
+
console.error("Error stopping backend:", err.message);
|
|
641
835
|
}
|
|
642
836
|
|
|
643
|
-
//
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
resolve
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
try {
|
|
653
|
-
proc.kill("SIGKILL");
|
|
654
|
-
} catch {
|
|
655
|
-
// Already dead
|
|
656
|
-
}
|
|
657
|
-
resolve();
|
|
658
|
-
}, 3000);
|
|
659
|
-
|
|
660
|
-
proc.once("exit", () => {
|
|
661
|
-
clearTimeout(forceKillTimer);
|
|
662
|
-
resolve();
|
|
837
|
+
// If the child object still reports alive (race), give its exit
|
|
838
|
+
// listener a brief moment to fire before we continue teardown.
|
|
839
|
+
if (proc.exitCode === null) {
|
|
840
|
+
await new Promise((resolve) => {
|
|
841
|
+
const timer = setTimeout(resolve, 500);
|
|
842
|
+
proc.once("exit", () => {
|
|
843
|
+
clearTimeout(timer);
|
|
844
|
+
resolve();
|
|
845
|
+
});
|
|
663
846
|
});
|
|
664
|
-
}
|
|
847
|
+
}
|
|
665
848
|
}
|
|
666
849
|
}
|
package/package.json
CHANGED