@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
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"use strict";
|
|
32
32
|
|
|
33
33
|
const { spawn, spawnSync, execSync } = require("child_process");
|
|
34
|
+
const crypto = require("crypto");
|
|
34
35
|
const fs = require("fs");
|
|
35
36
|
const path = require("path");
|
|
36
37
|
const os = require("os");
|
|
@@ -61,12 +62,31 @@ const NETWORK_CHECK_HOSTS = Object.freeze([
|
|
|
61
62
|
]);
|
|
62
63
|
const NETWORK_CHECK_TIMEOUT_MS = 5000;
|
|
63
64
|
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
65
|
+
// ── Bundled `uv` binary ──────────────────────────────────────────────────────
|
|
66
|
+
//
|
|
67
|
+
// Issue #782 / T3: the AppImage now ships a pinned `uv` under
|
|
68
|
+
// `extraResources` (see electron-builder.yml). At runtime we copy it into
|
|
69
|
+
// `~/.gaia/bin/uv` with atomic-rename + SHA256 verification. The previous
|
|
70
|
+
// `curl | sh` path is retained only as an unpackaged-dev fallback so
|
|
71
|
+
// contributors running from source keep working.
|
|
72
|
+
//
|
|
73
|
+
// When bumping uv, update BOTH:
|
|
74
|
+
// - .github/workflows/build-installers.yml (tarball .tar.gz SHA256 — archive)
|
|
75
|
+
// - BUNDLED_UV_SHA256 below (extracted ELF binary SHA256)
|
|
76
|
+
// These are two different digests: the workflow verifies the downloaded
|
|
77
|
+
// archive against upstream's published .sha256, then extracts the `uv` binary
|
|
78
|
+
// which is what `ensureUv()` hashes at runtime.
|
|
79
|
+
//
|
|
80
|
+
// Currently pinned: uv v0.5.14 linux-x64.
|
|
81
|
+
const BUNDLED_UV_VERSION = "0.5.14";
|
|
82
|
+
const BUNDLED_UV_SHA256 = {
|
|
83
|
+
"linux-x64": "0e05d828b5708e8a927724124db3746396afddad6273c47283d7c562dc795bd6",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const MANAGED_UV_DIR = path.join(GAIA_HOME, "bin");
|
|
87
|
+
const MANAGED_UV_BIN = IS_WINDOWS
|
|
88
|
+
? path.join(MANAGED_UV_DIR, "uv.exe")
|
|
89
|
+
: path.join(MANAGED_UV_DIR, "uv");
|
|
70
90
|
|
|
71
91
|
const STATES = Object.freeze({
|
|
72
92
|
IDLE: "idle",
|
|
@@ -624,90 +644,321 @@ class InstallError extends Error {
|
|
|
624
644
|
}
|
|
625
645
|
|
|
626
646
|
/**
|
|
627
|
-
*
|
|
628
|
-
*
|
|
647
|
+
* Which `extraResources` subdirectory holds the bundled uv for this host.
|
|
648
|
+
* Returns null for platforms we don't yet ship a binary for (falls through
|
|
649
|
+
* to the dev fallback).
|
|
629
650
|
*/
|
|
630
|
-
|
|
631
|
-
|
|
651
|
+
function bundledUvPlatformKey() {
|
|
652
|
+
if (process.platform === "linux" && process.arch === "x64") return "linux-x64";
|
|
653
|
+
if (process.platform === "win32" && process.arch === "x64") return "win-x64";
|
|
654
|
+
if (process.platform === "darwin" && process.arch === "arm64") return "mac-arm64";
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
632
657
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
658
|
+
/**
|
|
659
|
+
* Stream-SHA256 a file. Returns lowercase hex.
|
|
660
|
+
*/
|
|
661
|
+
function sha256File(filePath) {
|
|
662
|
+
return new Promise((resolve, reject) => {
|
|
663
|
+
const hash = crypto.createHash("sha256");
|
|
664
|
+
const stream = fs.createReadStream(filePath);
|
|
665
|
+
stream.on("error", reject);
|
|
666
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
667
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Resolve the bundled uv binary path inside the Electron resources dir.
|
|
673
|
+
* Returns null if this isn't an Electron-packaged runtime (no
|
|
674
|
+
* `process.resourcesPath`) or if the host platform isn't bundled.
|
|
675
|
+
*/
|
|
676
|
+
function findBundledUvResource() {
|
|
677
|
+
const key = bundledUvPlatformKey();
|
|
678
|
+
if (!key) return null;
|
|
679
|
+
const resourcesPath = process.resourcesPath;
|
|
680
|
+
if (!resourcesPath) return null;
|
|
681
|
+
const candidate = path.join(
|
|
682
|
+
resourcesPath,
|
|
683
|
+
"vendor",
|
|
684
|
+
"uv",
|
|
685
|
+
key,
|
|
686
|
+
IS_WINDOWS ? "uv.exe" : "uv"
|
|
687
|
+
);
|
|
688
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
689
|
+
}
|
|
638
690
|
|
|
639
|
-
|
|
640
|
-
|
|
691
|
+
/**
|
|
692
|
+
* Atomically install the bundled uv into ~/.gaia/bin/uv after verifying
|
|
693
|
+
* its SHA256 against BUNDLED_UV_SHA256. Returns the installed path.
|
|
694
|
+
*
|
|
695
|
+
* Writes to `uv.tmp-<pid>-<rand>` with mode 0o700, verifies hash,
|
|
696
|
+
* `chmod +x`, then `fs.rename()` (atomic on same filesystem).
|
|
697
|
+
*/
|
|
698
|
+
async function installBundledUv(sourcePath, platformKey) {
|
|
699
|
+
const expected = BUNDLED_UV_SHA256[platformKey];
|
|
700
|
+
if (!expected) {
|
|
701
|
+
throw new InstallError(
|
|
702
|
+
`No bundled uv checksum registered for platform ${platformKey}.`,
|
|
703
|
+
{ stage: STAGES.ENSURE_UV }
|
|
704
|
+
);
|
|
705
|
+
}
|
|
641
706
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
707
|
+
ensureGaiaHome();
|
|
708
|
+
try {
|
|
709
|
+
fs.mkdirSync(MANAGED_UV_DIR, { recursive: true });
|
|
710
|
+
} catch (err) {
|
|
711
|
+
throw new InstallError(
|
|
712
|
+
`Could not create ${MANAGED_UV_DIR}: ${err.message}`,
|
|
713
|
+
{ stage: STAGES.ENSURE_UV }
|
|
648
714
|
);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const rand = crypto.randomBytes(6).toString("hex");
|
|
718
|
+
const tmpPath = path.join(
|
|
719
|
+
MANAGED_UV_DIR,
|
|
720
|
+
`uv.tmp-${process.pid}-${rand}${IS_WINDOWS ? ".exe" : ""}`
|
|
721
|
+
);
|
|
722
|
+
|
|
723
|
+
// Copy source → tmp with restrictive mode.
|
|
724
|
+
await new Promise((resolve, reject) => {
|
|
725
|
+
const rs = fs.createReadStream(sourcePath);
|
|
726
|
+
const ws = fs.createWriteStream(tmpPath, { mode: 0o700 });
|
|
727
|
+
rs.on("error", reject);
|
|
728
|
+
ws.on("error", reject);
|
|
729
|
+
ws.on("finish", resolve);
|
|
730
|
+
rs.pipe(ws);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
let actual;
|
|
734
|
+
try {
|
|
735
|
+
actual = await sha256File(tmpPath);
|
|
736
|
+
} catch (err) {
|
|
737
|
+
try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
738
|
+
throw new InstallError(
|
|
739
|
+
`Could not hash copied uv binary: ${err.message}`,
|
|
740
|
+
{ stage: STAGES.ENSURE_UV }
|
|
654
741
|
);
|
|
655
742
|
}
|
|
656
743
|
|
|
657
|
-
if (
|
|
744
|
+
if (actual !== expected) {
|
|
745
|
+
try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
658
746
|
throw new InstallError(
|
|
659
|
-
`
|
|
747
|
+
`Bundled uv SHA256 mismatch (expected ${expected}, got ${actual}).`,
|
|
660
748
|
{
|
|
661
749
|
stage: STAGES.ENSURE_UV,
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
? 'Install uv manually: powershell -c "irm https://astral.sh/uv/install.ps1 | iex"'
|
|
665
|
-
: "Install uv manually: curl -LsSf https://astral.sh/uv/install.sh | sh",
|
|
750
|
+
suggestion:
|
|
751
|
+
"The AppImage/installer may be corrupt. Re-download from https://amd-gaia.ai and try again.",
|
|
666
752
|
}
|
|
667
753
|
);
|
|
668
754
|
}
|
|
669
755
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
756
|
+
try {
|
|
757
|
+
if (!IS_WINDOWS) fs.chmodSync(tmpPath, 0o700);
|
|
758
|
+
} catch (err) {
|
|
759
|
+
log(`Warning: chmod on tmp uv failed: ${err.message}`);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
try {
|
|
763
|
+
// rename() is atomic on the same filesystem on POSIX; on Windows
|
|
764
|
+
// it requires the target not to exist, so unlink first.
|
|
765
|
+
if (IS_WINDOWS && fs.existsSync(MANAGED_UV_BIN)) {
|
|
766
|
+
try { fs.unlinkSync(MANAGED_UV_BIN); } catch { /* ignore */ }
|
|
767
|
+
}
|
|
768
|
+
fs.renameSync(tmpPath, MANAGED_UV_BIN);
|
|
769
|
+
} catch (err) {
|
|
770
|
+
try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
771
|
+
throw new InstallError(
|
|
772
|
+
`Could not install uv to ${MANAGED_UV_BIN}: ${err.message}`,
|
|
773
|
+
{ stage: STAGES.ENSURE_UV }
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
log(`Installed bundled uv v${BUNDLED_UV_VERSION} → ${MANAGED_UV_BIN}`);
|
|
778
|
+
return MANAGED_UV_BIN;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Prepend ~/.gaia/bin to this process's PATH so child spawns see our
|
|
783
|
+
* managed uv before any system-wide install.
|
|
784
|
+
*/
|
|
785
|
+
function addManagedBinToPath() {
|
|
786
|
+
if (
|
|
787
|
+
process.env.PATH &&
|
|
788
|
+
!process.env.PATH.split(path.delimiter).includes(MANAGED_UV_DIR)
|
|
789
|
+
) {
|
|
790
|
+
process.env.PATH = `${MANAGED_UV_DIR}${path.delimiter}${process.env.PATH}`;
|
|
791
|
+
log(`Prepended ${MANAGED_UV_DIR} to PATH for this process`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Ensure `uv` is available. Preference order (per issue #782 / T3):
|
|
797
|
+
* 1. Managed copy at ~/.gaia/bin/uv with matching SHA256 (warm-install fast path).
|
|
798
|
+
* 2. Bundled binary in process.resourcesPath/vendor/uv/<platform>/uv:
|
|
799
|
+
* copy atomically to ~/.gaia/bin/uv with SHA256 verification.
|
|
800
|
+
* 3. DEV-ONLY fallback (app.isPackaged === false OR no resourcesPath):
|
|
801
|
+
* the original `curl | sh` from astral.sh. Not a shipped-user path.
|
|
802
|
+
* 4. System `uv` on PATH (last resort — unverified version).
|
|
803
|
+
*
|
|
804
|
+
* Throws InstallError on failure.
|
|
805
|
+
*/
|
|
806
|
+
async function ensureUv({ onProgress, isPackaged } = {}) {
|
|
807
|
+
const report = makeProgressReporter(onProgress);
|
|
808
|
+
report(STAGES.ENSURE_UV, 0, "Checking uv (Python package manager)");
|
|
809
|
+
|
|
810
|
+
const platformKey = bundledUvPlatformKey();
|
|
811
|
+
const expectedSha = platformKey ? BUNDLED_UV_SHA256[platformKey] : null;
|
|
812
|
+
|
|
813
|
+
// Fast path: warm install already on disk with correct hash.
|
|
814
|
+
if (expectedSha && fs.existsSync(MANAGED_UV_BIN)) {
|
|
815
|
+
try {
|
|
816
|
+
const actual = await sha256File(MANAGED_UV_BIN);
|
|
817
|
+
if (actual === expectedSha) {
|
|
818
|
+
log(`Managed uv at ${MANAGED_UV_BIN} passed SHA256 check — reusing`);
|
|
819
|
+
addManagedBinToPath();
|
|
820
|
+
report(STAGES.ENSURE_UV, 100, "uv ready (cached)");
|
|
821
|
+
return;
|
|
685
822
|
}
|
|
823
|
+
log(
|
|
824
|
+
`Managed uv hash mismatch (expected ${expectedSha}, got ${actual}) — replacing`
|
|
825
|
+
);
|
|
826
|
+
} catch (err) {
|
|
827
|
+
log(`Could not verify managed uv: ${err.message} — replacing`);
|
|
686
828
|
}
|
|
687
829
|
}
|
|
688
830
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
831
|
+
// Bundled path (the shipped-user path — AppImage, NSIS, DMG).
|
|
832
|
+
const bundled = findBundledUvResource();
|
|
833
|
+
if (bundled && platformKey) {
|
|
834
|
+
report(STAGES.ENSURE_UV, 30, "Installing bundled uv");
|
|
835
|
+
log(`Using bundled uv from ${bundled}`);
|
|
836
|
+
|
|
837
|
+
// Verify the source resource matches the manifest before copying —
|
|
838
|
+
// catches AppImage corruption before we touch the user's home.
|
|
839
|
+
const srcHash = await sha256File(bundled);
|
|
840
|
+
if (srcHash !== expectedSha) {
|
|
841
|
+
throw new InstallError(
|
|
842
|
+
`Bundled uv resource SHA256 mismatch (expected ${expectedSha}, got ${srcHash}).`,
|
|
843
|
+
{
|
|
844
|
+
stage: STAGES.ENSURE_UV,
|
|
845
|
+
suggestion:
|
|
846
|
+
"The installer appears to be corrupt. Re-download GAIA from https://amd-gaia.ai and try again.",
|
|
847
|
+
}
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
await installBundledUv(bundled, platformKey);
|
|
851
|
+
addManagedBinToPath();
|
|
852
|
+
report(STAGES.ENSURE_UV, 100, "uv installed (bundled)");
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// DEV-ONLY fallback for contributors running from source (no
|
|
857
|
+
// extraResources, no packaged app). Never fires for end users.
|
|
858
|
+
const isDev = isPackaged === false || !process.resourcesPath;
|
|
859
|
+
if (isDev) {
|
|
860
|
+
if (commandExists("uv")) {
|
|
861
|
+
log("uv already on PATH (dev) — using system install");
|
|
862
|
+
report(STAGES.ENSURE_UV, 100, "uv is already installed (system)");
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
log("[dev] No bundled uv and no system uv — falling back to curl|sh installer");
|
|
867
|
+
let result;
|
|
868
|
+
if (IS_WINDOWS) {
|
|
869
|
+
result = await runCommand(
|
|
870
|
+
"powershell",
|
|
871
|
+
[
|
|
872
|
+
"-ExecutionPolicy",
|
|
873
|
+
"Bypass",
|
|
874
|
+
"-Command",
|
|
875
|
+
"irm https://astral.sh/uv/install.ps1 | iex",
|
|
876
|
+
],
|
|
877
|
+
{ stageLabel: "uv-install-dev" }
|
|
878
|
+
);
|
|
879
|
+
} else {
|
|
880
|
+
result = await runCommand(
|
|
881
|
+
"bash",
|
|
882
|
+
["-c", "curl -LsSf https://astral.sh/uv/install.sh | sh"],
|
|
883
|
+
{ stageLabel: "uv-install-dev" }
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (result.code !== 0) {
|
|
888
|
+
throw new InstallError(
|
|
889
|
+
`Could not install uv automatically (exit code ${result.code}).`,
|
|
890
|
+
{
|
|
891
|
+
stage: STAGES.ENSURE_UV,
|
|
892
|
+
code: result.code,
|
|
893
|
+
suggestion: IS_WINDOWS
|
|
894
|
+
? 'Install uv manually: powershell -c "irm https://astral.sh/uv/install.ps1 | iex"'
|
|
895
|
+
: "Install uv manually: curl -LsSf https://astral.sh/uv/install.sh | sh",
|
|
896
|
+
}
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (!commandExists("uv")) {
|
|
901
|
+
const candidates = [
|
|
902
|
+
path.join(os.homedir(), ".local", "bin"),
|
|
903
|
+
path.join(os.homedir(), ".cargo", "bin"),
|
|
904
|
+
];
|
|
905
|
+
for (const uvDir of candidates) {
|
|
906
|
+
if (process.env.PATH && !process.env.PATH.includes(uvDir)) {
|
|
907
|
+
process.env.PATH = `${uvDir}${path.delimiter}${process.env.PATH}`;
|
|
908
|
+
log(`Added ${uvDir} to PATH for this process`);
|
|
909
|
+
}
|
|
696
910
|
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (!commandExists("uv")) {
|
|
914
|
+
throw new InstallError(
|
|
915
|
+
"uv installed but not found on PATH. A shell restart may be required.",
|
|
916
|
+
{
|
|
917
|
+
stage: STAGES.ENSURE_UV,
|
|
918
|
+
suggestion:
|
|
919
|
+
"Restart your terminal or reboot, then re-launch GAIA. If the problem persists, install uv manually from https://astral.sh/uv",
|
|
920
|
+
}
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
report(STAGES.ENSURE_UV, 100, "uv installed (dev fallback)");
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Packaged build, but we somehow don't have a bundled binary for this
|
|
929
|
+
// platform AND no system uv. Last-ditch: accept an unverified system uv
|
|
930
|
+
// if present; otherwise fail with a clear message.
|
|
931
|
+
if (commandExists("uv")) {
|
|
932
|
+
log(
|
|
933
|
+
`No bundled uv for ${process.platform}-${process.arch}, using system uv on PATH (unverified)`
|
|
697
934
|
);
|
|
935
|
+
report(STAGES.ENSURE_UV, 100, "uv ready (system, unverified)");
|
|
936
|
+
return;
|
|
698
937
|
}
|
|
699
938
|
|
|
700
|
-
|
|
701
|
-
|
|
939
|
+
throw new InstallError(
|
|
940
|
+
`No bundled uv available for ${process.platform}-${process.arch} and no system uv found.`,
|
|
941
|
+
{
|
|
942
|
+
stage: STAGES.ENSURE_UV,
|
|
943
|
+
suggestion:
|
|
944
|
+
"Install uv manually from https://astral.sh/uv and re-launch GAIA.",
|
|
945
|
+
}
|
|
946
|
+
);
|
|
702
947
|
}
|
|
703
948
|
|
|
704
949
|
// ── Backend install ──────────────────────────────────────────────────────────
|
|
705
950
|
|
|
706
951
|
/**
|
|
707
952
|
* Read the pinned backend version from package.json (or a caller override).
|
|
953
|
+
* Returns null when GAIA_LOCAL_WHEEL is set — the caller uses the wheel path
|
|
954
|
+
* directly and skips the PyPI version pin (CI release-build fast-path).
|
|
708
955
|
*/
|
|
709
956
|
function resolveBackendVersion(opts = {}) {
|
|
710
957
|
if (opts.version) return opts.version;
|
|
958
|
+
// CI override: install from a local wheel instead of a pinned PyPI version.
|
|
959
|
+
// Breaks the circular dependency in release builds where the AppImage smoke
|
|
960
|
+
// test runs before PyPI publish.
|
|
961
|
+
if (process.env.GAIA_LOCAL_WHEEL) return null;
|
|
711
962
|
try {
|
|
712
963
|
// package.json is one directory up from the services/ (or bin/) directory.
|
|
713
964
|
// We look relative to this module's own location.
|
|
@@ -734,7 +985,16 @@ function resolveBackendVersion(opts = {}) {
|
|
|
734
985
|
async function installBackend(opts = {}) {
|
|
735
986
|
const report = makeProgressReporter(opts.onProgress);
|
|
736
987
|
const version = resolveBackendVersion(opts);
|
|
737
|
-
|
|
988
|
+
// GAIA_LOCAL_WHEEL: CI-only. When set, install from the given wheel path
|
|
989
|
+
// instead of pulling from PyPI. This breaks the circular dependency in
|
|
990
|
+
// release pipeline smoke tests that run before PyPI publish. The `[ui]`
|
|
991
|
+
// extras marker is preserved so the local install matches the PyPI path
|
|
992
|
+
// (fastapi, uvicorn, python-multipart, httpx, psutil) — otherwise the
|
|
993
|
+
// backend venv comes up missing every UI dep and /api/health never binds.
|
|
994
|
+
const localWheel = process.env.GAIA_LOCAL_WHEEL || null;
|
|
995
|
+
const pipPackage = localWheel
|
|
996
|
+
? `${localWheel}[ui]`
|
|
997
|
+
: `amd-gaia[ui]==${version}`;
|
|
738
998
|
|
|
739
999
|
log("================================================");
|
|
740
1000
|
log(" Installing GAIA backend");
|
|
@@ -745,7 +1005,7 @@ async function installBackend(opts = {}) {
|
|
|
745
1005
|
setState(STATES.INSTALLING, { stage: STAGES.ENSURE_UV, version });
|
|
746
1006
|
|
|
747
1007
|
// Stage 1: ensure uv
|
|
748
|
-
await ensureUv({ onProgress: opts.onProgress });
|
|
1008
|
+
await ensureUv({ onProgress: opts.onProgress, isPackaged: opts.isPackaged });
|
|
749
1009
|
|
|
750
1010
|
// Stage 2: create venv
|
|
751
1011
|
setState(STATES.INSTALLING, { stage: STAGES.CREATE_VENV, version });
|
|
@@ -800,7 +1060,8 @@ async function installBackend(opts = {}) {
|
|
|
800
1060
|
GAIA_PYTHON_BIN,
|
|
801
1061
|
];
|
|
802
1062
|
// Linux/macOS: use CPU-only PyTorch to avoid huge CUDA wheels.
|
|
803
|
-
|
|
1063
|
+
// Skip when installing from a local wheel — PyPI index not needed.
|
|
1064
|
+
if (!IS_WINDOWS && !localWheel) {
|
|
804
1065
|
pipArgs.push("--extra-index-url", "https://download.pytorch.org/whl/cpu");
|
|
805
1066
|
}
|
|
806
1067
|
|
|
@@ -979,11 +1240,13 @@ async function ensureBackend(opts = {}) {
|
|
|
979
1240
|
}
|
|
980
1241
|
|
|
981
1242
|
// Fast-path: already installed at the expected version.
|
|
1243
|
+
// Skip when expectedVersion is null (GAIA_LOCAL_WHEEL is set) — always
|
|
1244
|
+
// reinstall from the local wheel so CI gets a fresh install each run.
|
|
982
1245
|
const expectedVersion = resolveBackendVersion(opts);
|
|
983
1246
|
const existingBin = findGaiaBin();
|
|
984
1247
|
if (existingBin) {
|
|
985
1248
|
const installedVersion = getInstalledVersion(existingBin);
|
|
986
|
-
if (installedVersion === expectedVersion) {
|
|
1249
|
+
if (expectedVersion !== null && installedVersion === expectedVersion) {
|
|
987
1250
|
log(
|
|
988
1251
|
`GAIA backend already installed at version ${installedVersion} — nothing to do`
|
|
989
1252
|
);
|