@amd-gaia/agent-ui 0.17.5 → 0.17.6
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-BJlmVnvj.js +453 -0
- package/dist/assets/{index-CzObDlwu.css → index-DDqg4jHs.css} +1 -1
- package/dist/index.html +2 -2
- package/main.cjs +91 -1
- package/package.json +1 -1
- package/services/backend-installer-progress-dialog.cjs +79 -45
- package/services/backend-installer.cjs +130 -15
- package/dist/assets/index-CdffaS1a.js +0 -438
package/main.cjs
CHANGED
|
@@ -17,7 +17,7 @@ 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");
|
|
20
|
-
const { spawn } = require("child_process");
|
|
20
|
+
const { spawn, spawnSync } = require("child_process");
|
|
21
21
|
const { pathToFileURL } = require("url");
|
|
22
22
|
|
|
23
23
|
// ── Shared log path ───────────────────────────────────────────────────────────
|
|
@@ -248,6 +248,70 @@ async function startBackend() {
|
|
|
248
248
|
backendPort = DEFAULT_BACKEND_PORT;
|
|
249
249
|
}
|
|
250
250
|
healthCheckUrl = `http://localhost:${backendPort}/api/health`;
|
|
251
|
+
// Clean up any stale backend PID left from previous runs. The AppImage
|
|
252
|
+
// may be double-clicked multiple times or crash, leaving orphaned
|
|
253
|
+
// backend processes. We store a PID file under ~/.gaia/backend.pid and
|
|
254
|
+
// attempt to SIGTERM any still-running PID before starting a new backend.
|
|
255
|
+
try {
|
|
256
|
+
const pidFile = path.join(_GAIA_DIR, "backend.pid");
|
|
257
|
+
if (fs.existsSync(pidFile)) {
|
|
258
|
+
try {
|
|
259
|
+
const existingPid = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
|
|
260
|
+
if (!Number.isNaN(existingPid)) {
|
|
261
|
+
console.log(`[main] Found existing backend pidfile (${existingPid}) — verifying process identity`);
|
|
262
|
+
|
|
263
|
+
// Verify the PID belongs to a GAIA backend before signalling it.
|
|
264
|
+
// Prefer a conservative match on the process command to avoid
|
|
265
|
+
// accidentally killing unrelated user processes (TOCTOU mitigation).
|
|
266
|
+
let isBackend = false;
|
|
267
|
+
try {
|
|
268
|
+
if (process.platform === "linux") {
|
|
269
|
+
const procCmd = `/proc/${existingPid}/cmdline`;
|
|
270
|
+
if (fs.existsSync(procCmd)) {
|
|
271
|
+
const raw = fs.readFileSync(procCmd, "utf8");
|
|
272
|
+
// /proc/<pid>/cmdline is NUL-separated; use a stricter match to
|
|
273
|
+
// avoid killing unrelated Python processes (Jupyter, LSP, etc.).
|
|
274
|
+
// Legitimate backend uses: `gaia chat --ui --ui-port <port>`
|
|
275
|
+
const looksLikeGaiaBackend = raw.includes("gaia") && (raw.includes("chat") || raw.includes("--ui-port"));
|
|
276
|
+
if (looksLikeGaiaBackend) {
|
|
277
|
+
isBackend = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} else if (process.platform === "darwin") {
|
|
281
|
+
try {
|
|
282
|
+
const out = spawnSync("ps", ["-p", String(existingPid), "-o", "command="], { encoding: "utf8" });
|
|
283
|
+
const cmd = (out && out.stdout) ? out.stdout : "";
|
|
284
|
+
const looksLikeGaiaBackend = cmd.includes("gaia") && (cmd.includes("chat") || cmd.includes("--ui-port"));
|
|
285
|
+
if (looksLikeGaiaBackend) {
|
|
286
|
+
isBackend = true;
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
// fallthrough
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.warn(`[main] Could not verify pid ${existingPid}: ${err.message}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!isBackend) {
|
|
297
|
+
console.log(`[main] PID ${existingPid} does not appear to be a GAIA backend; skipping kill`);
|
|
298
|
+
} else {
|
|
299
|
+
try {
|
|
300
|
+
await portManager.killBackend(existingPid);
|
|
301
|
+
console.log(`[main] Cleaned up previous backend pid ${existingPid}`);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.warn(`[main] Could not clean previous backend pid ${existingPid}: ${err.message}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.warn(`[main] Failed reading backend pidfile: ${err.message}`);
|
|
309
|
+
}
|
|
310
|
+
try { fs.unlinkSync(pidFile); } catch { /* ignore */ }
|
|
311
|
+
}
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.warn(`[main] PID cleanup check failed: ${err.message}`);
|
|
314
|
+
}
|
|
251
315
|
|
|
252
316
|
console.log(`Starting backend: ${gaiaCmd} chat --ui --ui-port ${backendPort}`);
|
|
253
317
|
|
|
@@ -290,6 +354,18 @@ async function startBackend() {
|
|
|
290
354
|
console.error("Failed to start backend:", err.message);
|
|
291
355
|
});
|
|
292
356
|
|
|
357
|
+
// Write a PID file so subsequent AppImage launches can detect and
|
|
358
|
+
// cleanup this backend if it becomes orphaned. The pidfile is removed
|
|
359
|
+
// when the child exits.
|
|
360
|
+
try {
|
|
361
|
+
try { fs.mkdirSync(_GAIA_DIR, { recursive: true }); } catch { /* ignore */ }
|
|
362
|
+
const pidFile = path.join(_GAIA_DIR, "backend.pid");
|
|
363
|
+
fs.writeFileSync(pidFile, String(child.pid), { mode: 0o600 });
|
|
364
|
+
console.log(`[main] Wrote backend pidfile ${pidFile} (pid=${child.pid})`);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
console.warn(`[main] Could not write backend pidfile: ${err.message}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
293
369
|
child.on("exit", (code, signal) => {
|
|
294
370
|
if (code !== 0 && code !== null) {
|
|
295
371
|
console.error(`Backend exited with code ${code} (signal=${signal})`);
|
|
@@ -301,6 +377,20 @@ async function startBackend() {
|
|
|
301
377
|
// Fire-and-forget — don't block the event loop.
|
|
302
378
|
void handleBackendCrash(code, signal);
|
|
303
379
|
}
|
|
380
|
+
|
|
381
|
+
// Remove pidfile on exit to avoid leaving stale PID behind.
|
|
382
|
+
try {
|
|
383
|
+
const pidFile = path.join(_GAIA_DIR, "backend.pid");
|
|
384
|
+
if (fs.existsSync(pidFile)) {
|
|
385
|
+
const p = fs.readFileSync(pidFile, "utf8").trim();
|
|
386
|
+
if (p && parseInt(p, 10) === child.pid) {
|
|
387
|
+
try { fs.unlinkSync(pidFile); } catch { /* ignore */ }
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} catch (err) {
|
|
391
|
+
// Non-fatal — just log and continue.
|
|
392
|
+
console.warn(`[main] Failed to remove backend pidfile: ${err.message}`);
|
|
393
|
+
}
|
|
304
394
|
});
|
|
305
395
|
|
|
306
396
|
return child;
|
package/package.json
CHANGED
|
@@ -345,55 +345,89 @@ async function showFailureDialog(parentWindow, errorInfo = {}) {
|
|
|
345
345
|
.filter(Boolean)
|
|
346
346
|
.join("\n");
|
|
347
347
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
348
|
+
// Loop so that certain actions (Copy/Open log) return control to the
|
|
349
|
+
// same dialog rather than exiting the flow. The dialog includes a
|
|
350
|
+
// one-click "Install uv" action which attempts the packaged rescue
|
|
351
|
+
// installer and then returns 'retry' on success so the caller can
|
|
352
|
+
// re-run the full backend install.
|
|
353
|
+
const buttons = [
|
|
354
|
+
"Install uv (auto)",
|
|
355
|
+
"Retry",
|
|
356
|
+
"Manual install instructions",
|
|
357
|
+
"Copy log path",
|
|
358
|
+
"Open log file",
|
|
359
|
+
"Quit",
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
// Helper to show the dialog and handle the response.
|
|
363
|
+
const show = async () => {
|
|
364
|
+
const result = await dialog.showMessageBox(parentWindow || null, {
|
|
365
|
+
type: "error",
|
|
366
|
+
title: "GAIA install failed",
|
|
367
|
+
message,
|
|
368
|
+
detail,
|
|
369
|
+
buttons,
|
|
370
|
+
defaultId: 0,
|
|
371
|
+
cancelId: buttons.length - 1,
|
|
372
|
+
noLink: true,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
switch (result.response) {
|
|
376
|
+
case 0: {
|
|
377
|
+
// Run the full packaged install flow (ensureBackend) so the user
|
|
378
|
+
// gets a single-click recovery that attempts the entire backend
|
|
379
|
+
// install rather than only uv provisioning. Show progress UI
|
|
380
|
+
// while the operation runs. On success, tell the caller to retry
|
|
381
|
+
// (which will detect READY and proceed); on failure, re-show the
|
|
382
|
+
// dialog with augmented details.
|
|
383
|
+
const { window, onProgress, close } = createProgressWindow();
|
|
384
|
+
try {
|
|
385
|
+
await installer.ensureBackend({ onProgress, isPackaged: true });
|
|
386
|
+
try { close(); } catch {}
|
|
387
|
+
return "retry";
|
|
388
|
+
} catch (err) {
|
|
389
|
+
try { close(); } catch {}
|
|
390
|
+
const nextInfo = Object.assign({}, errorInfo, {
|
|
391
|
+
message: err.message || String(err),
|
|
392
|
+
stage: err.stage || "ensure-backend",
|
|
393
|
+
suggestion: err.suggestion || errorInfo.suggestion,
|
|
394
|
+
});
|
|
395
|
+
return showFailureDialog(parentWindow, nextInfo);
|
|
396
|
+
}
|
|
373
397
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
398
|
+
case 1:
|
|
399
|
+
return "retry";
|
|
400
|
+
case 2: {
|
|
401
|
+
try {
|
|
402
|
+
await shell.openExternal("https://amd-gaia.ai/quickstart#cli-install");
|
|
403
|
+
} catch {
|
|
404
|
+
// ignore
|
|
405
|
+
}
|
|
406
|
+
return "manual";
|
|
381
407
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
408
|
+
case 3: {
|
|
409
|
+
try {
|
|
410
|
+
clipboard.writeText(logPath);
|
|
411
|
+
} catch {
|
|
412
|
+
// ignore
|
|
413
|
+
}
|
|
414
|
+
return showFailureDialog(parentWindow, errorInfo);
|
|
415
|
+
}
|
|
416
|
+
case 4: {
|
|
417
|
+
try {
|
|
418
|
+
await shell.openPath(logPath);
|
|
419
|
+
} catch {
|
|
420
|
+
// ignore
|
|
421
|
+
}
|
|
422
|
+
return showFailureDialog(parentWindow, errorInfo);
|
|
390
423
|
}
|
|
391
|
-
|
|
424
|
+
case 5:
|
|
425
|
+
default:
|
|
426
|
+
return "quit";
|
|
392
427
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
return show();
|
|
397
431
|
}
|
|
398
432
|
|
|
399
433
|
// ── Pre-check failure dialogs ───────────────────────────────────────────────
|
|
@@ -77,10 +77,28 @@ const NETWORK_CHECK_TIMEOUT_MS = 5000;
|
|
|
77
77
|
// archive against upstream's published .sha256, then extracts the `uv` binary
|
|
78
78
|
// which is what `ensureUv()` hashes at runtime.
|
|
79
79
|
//
|
|
80
|
-
// Currently pinned: uv v0.5.14 linux-x64.
|
|
80
|
+
// Currently pinned: uv v0.5.14 linux-x64, mac-arm64.
|
|
81
|
+
// (win-x64 deferred to a follow-up issue — its SHA must ship together with an
|
|
82
|
+
// NSIS structural-smoke verifier, not on its own; see the #849 lesson.)
|
|
83
|
+
//
|
|
84
|
+
// IMPORTANT: per-platform SHA origin differs:
|
|
85
|
+
// - linux-x64: raw extracted-from-tarball digest (no post-build modification).
|
|
86
|
+
// - mac-arm64: POST-CODESIGN digest. electron-builder code-signs the bundled
|
|
87
|
+
// uv during packaging, so this hash matches what ensureUv() sees
|
|
88
|
+
// at runtime, NOT the upstream tarball. Bumping this pin means
|
|
89
|
+
// running the CI build, then copying the SHA from the
|
|
90
|
+
// dmg-structural-smoke failure message — never from `shasum`
|
|
91
|
+
// against the freshly downloaded tarball.
|
|
81
92
|
const BUNDLED_UV_VERSION = "0.5.14";
|
|
82
93
|
const BUNDLED_UV_SHA256 = {
|
|
83
94
|
"linux-x64": "0e05d828b5708e8a927724124db3746396afddad6273c47283d7c562dc795bd6",
|
|
95
|
+
// The Windows extracted uv.exe SHA is populated by CI during the
|
|
96
|
+
// build step. The placeholder MUST be replaced in CI before packaging
|
|
97
|
+
// so runtime verification remains strict.
|
|
98
|
+
"win-x64": "055d55eec85a91cfb5e9c8bc7f6463f9883866796c5bcb205fbcdfed9c088c88",
|
|
99
|
+
// mac-arm64: POST-codesign digest. CI should populate this value when
|
|
100
|
+
// packaging the macOS DMG and running the dmg-structural-smoke job.
|
|
101
|
+
"mac-arm64": "6099aa8cd701f0c81227ee30c304777ce151e4d47c53a75ce53cd2243448d8c8",
|
|
84
102
|
};
|
|
85
103
|
|
|
86
104
|
const MANAGED_UV_DIR = path.join(GAIA_HOME, "bin");
|
|
@@ -697,9 +715,12 @@ function findBundledUvResource() {
|
|
|
697
715
|
*/
|
|
698
716
|
async function installBundledUv(sourcePath, platformKey) {
|
|
699
717
|
const expected = BUNDLED_UV_SHA256[platformKey];
|
|
700
|
-
if (!expected) {
|
|
718
|
+
if (!expected || expected.startsWith("<")) {
|
|
719
|
+
// Enforce strict verification: builds MUST populate the expected SHA
|
|
720
|
+
// for packaged binaries. Failing fast prevents shipping an unverified
|
|
721
|
+
// uv binary which would be a supply-chain regression.
|
|
701
722
|
throw new InstallError(
|
|
702
|
-
`No bundled uv checksum registered for platform ${platformKey}
|
|
723
|
+
`No bundled uv checksum registered for platform ${platformKey}. Build must populate BUNDLED_UV_SHA256.${platformKey}`,
|
|
703
724
|
{ stage: STAGES.ENSURE_UV }
|
|
704
725
|
);
|
|
705
726
|
}
|
|
@@ -741,16 +762,20 @@ async function installBundledUv(sourcePath, platformKey) {
|
|
|
741
762
|
);
|
|
742
763
|
}
|
|
743
764
|
|
|
744
|
-
if (
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
765
|
+
if (expected) {
|
|
766
|
+
if (actual !== expected) {
|
|
767
|
+
try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
768
|
+
throw new InstallError(
|
|
769
|
+
`Bundled uv SHA256 mismatch (expected ${expected}, got ${actual}).`,
|
|
770
|
+
{
|
|
771
|
+
stage: STAGES.ENSURE_UV,
|
|
772
|
+
suggestion:
|
|
773
|
+
"The AppImage/installer may be corrupt. Re-download from https://amd-gaia.ai and try again.",
|
|
774
|
+
}
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
} else {
|
|
778
|
+
log("No expected SHA registered for bundled uv; installed binary will not be verified locally.");
|
|
754
779
|
}
|
|
755
780
|
|
|
756
781
|
try {
|
|
@@ -836,6 +861,10 @@ async function ensureUv({ onProgress, isPackaged } = {}) {
|
|
|
836
861
|
|
|
837
862
|
// Verify the source resource matches the manifest before copying —
|
|
838
863
|
// catches AppImage corruption before we touch the user's home.
|
|
864
|
+
// Enforce that the packaged build provides an expected SHA for the
|
|
865
|
+
// bundled resource. CI replaces the placeholder with the extracted
|
|
866
|
+
// binary's SHA during the build; missing/placeholder values are a
|
|
867
|
+
// build-time error and are rejected at runtime here.
|
|
839
868
|
const srcHash = await sha256File(bundled);
|
|
840
869
|
if (srcHash !== expectedSha) {
|
|
841
870
|
throw new InstallError(
|
|
@@ -925,6 +954,52 @@ async function ensureUv({ onProgress, isPackaged } = {}) {
|
|
|
925
954
|
return;
|
|
926
955
|
}
|
|
927
956
|
|
|
957
|
+
// Packaged Windows rescue: try the Astral PowerShell installer even when
|
|
958
|
+
// running a packaged build. This provides an automated recovery path for
|
|
959
|
+
// end users on clean machines that don't have uv and where the installer
|
|
960
|
+
// build unexpectedly omitted a bundled binary. It is a last-resort and
|
|
961
|
+
// non-fatal attempt; on failure we fall through to the generic error.
|
|
962
|
+
if (IS_WINDOWS && !isDev) {
|
|
963
|
+
log("Packaged Windows: attempting automated uv installer (rescue)");
|
|
964
|
+
try {
|
|
965
|
+
const rescue = await runCommand(
|
|
966
|
+
"powershell",
|
|
967
|
+
[
|
|
968
|
+
"-ExecutionPolicy",
|
|
969
|
+
"Bypass",
|
|
970
|
+
"-Command",
|
|
971
|
+
"irm https://astral.sh/uv/install.ps1 | iex",
|
|
972
|
+
],
|
|
973
|
+
{ stageLabel: "uv-install-packaged-rescue" }
|
|
974
|
+
);
|
|
975
|
+
if (rescue.code === 0) {
|
|
976
|
+
// Ensure common install locations are present on PATH for this
|
|
977
|
+
// process in case the installer placed uv in a user-local bin.
|
|
978
|
+
const candidates = [
|
|
979
|
+
path.join(os.homedir(), ".local", "bin"),
|
|
980
|
+
path.join(os.homedir(), ".cargo", "bin"),
|
|
981
|
+
];
|
|
982
|
+
for (const uvDir of candidates) {
|
|
983
|
+
if (process.env.PATH && !process.env.PATH.includes(uvDir)) {
|
|
984
|
+
process.env.PATH = `${uvDir}${path.delimiter}${process.env.PATH}`;
|
|
985
|
+
log(`Added ${uvDir} to PATH for this process`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (commandExists("uv")) {
|
|
989
|
+
log("Packaged Windows: uv installed and found on PATH (rescue succeeded)");
|
|
990
|
+
addManagedBinToPath();
|
|
991
|
+
report(STAGES.ENSURE_UV, 100, "uv ready (system, unverified)");
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
log("Packaged Windows: uv installer ran but uv not found on PATH");
|
|
995
|
+
} else {
|
|
996
|
+
log(`Packaged Windows: automated uv installer exited ${rescue.code}`);
|
|
997
|
+
}
|
|
998
|
+
} catch (rescueErr) {
|
|
999
|
+
log(`Packaged Windows: rescue installer threw: ${rescueErr.message}`);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
928
1003
|
// Packaged build, but we somehow don't have a bundled binary for this
|
|
929
1004
|
// platform AND no system uv. Last-ditch: accept an unverified system uv
|
|
930
1005
|
// if present; otherwise fail with a clear message.
|
|
@@ -937,11 +1012,11 @@ async function ensureUv({ onProgress, isPackaged } = {}) {
|
|
|
937
1012
|
}
|
|
938
1013
|
|
|
939
1014
|
throw new InstallError(
|
|
940
|
-
`
|
|
1015
|
+
`GAIA could not find or install its Python helper (uv) required to provision the backend.`,
|
|
941
1016
|
{
|
|
942
1017
|
stage: STAGES.ENSURE_UV,
|
|
943
1018
|
suggestion:
|
|
944
|
-
"Install uv
|
|
1019
|
+
"GAIA attempted automatic recovery but could not install uv. Please either: (a) click 'Install uv' in the dialog to let GAIA try again, or (b) install uv from https://astral.sh/uv and re-launch GAIA.",
|
|
945
1020
|
}
|
|
946
1021
|
);
|
|
947
1022
|
}
|
|
@@ -1135,6 +1210,46 @@ async function installBackend(opts = {}) {
|
|
|
1135
1210
|
log(`Verified gaia binary: ${verifiedBin} (version=${installedVersion || "unknown"})`);
|
|
1136
1211
|
report(STAGES.VERIFY, 100, "Install verified");
|
|
1137
1212
|
|
|
1213
|
+
// Ensure a user-accessible shim is created so users who installed via
|
|
1214
|
+
// AppImage can run `gaia` from a terminal without manually adding the
|
|
1215
|
+
// venv bin directory to their PATH. Do not overwrite an existing system
|
|
1216
|
+
// `gaia` or an existing shim the user may have created.
|
|
1217
|
+
try {
|
|
1218
|
+
// Only create shims on POSIX-like systems (AppImage target).
|
|
1219
|
+
if (process.platform !== "win32") {
|
|
1220
|
+
const userBin = process.env.XDG_BIN_HOME || path.join(os.homedir(), ".local", "bin");
|
|
1221
|
+
const shimPath = path.join(userBin, "gaia");
|
|
1222
|
+
|
|
1223
|
+
// Only create a shim if there's no `gaia` already on PATH and no shim
|
|
1224
|
+
// at the target location. This avoids clobbering system packages.
|
|
1225
|
+
if (!commandExists("gaia") && !fs.existsSync(shimPath)) {
|
|
1226
|
+
try {
|
|
1227
|
+
// Basic sanity-check on the target path to avoid writing a
|
|
1228
|
+
// wrapper that could execute an arbitrary command. The
|
|
1229
|
+
// verifiedBin is produced by our installer and is expected to be
|
|
1230
|
+
// a normal filesystem path (alphanum, dash, dot, slash, underscore).
|
|
1231
|
+
if (!/^[\w\-./]+$/.test(verifiedBin)) {
|
|
1232
|
+
log(`Refusing to create shim: verified bin path looks suspicious: ${verifiedBin}`);
|
|
1233
|
+
} else {
|
|
1234
|
+
fs.mkdirSync(userBin, { recursive: true });
|
|
1235
|
+
const wrapper = `#!/bin/sh\nexec \"${verifiedBin}\" \"$@\"\n`;
|
|
1236
|
+
fs.writeFileSync(shimPath, wrapper, { mode: 0o755 });
|
|
1237
|
+
log(`Created user shim at ${shimPath} pointing to ${verifiedBin}`);
|
|
1238
|
+
}
|
|
1239
|
+
} catch (err) {
|
|
1240
|
+
log(`Could not create user shim at ${shimPath}: ${err.message}`);
|
|
1241
|
+
}
|
|
1242
|
+
} else if (fs.existsSync(shimPath)) {
|
|
1243
|
+
log(`User shim already exists at ${shimPath}; leaving it intact`);
|
|
1244
|
+
} else {
|
|
1245
|
+
log("A system 'gaia' binary was found on PATH; skipping shim creation");
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
// Non-fatal: proceed even if shim creation fails.
|
|
1250
|
+
log(`Shim creation check failed: ${err.message}`);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1138
1253
|
setState(STATES.READY, { stage: null, version, installedVersion });
|
|
1139
1254
|
log("Backend install complete");
|
|
1140
1255
|
}
|