@amd-gaia/agent-ui 0.17.5 → 0.18.0

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.
@@ -345,55 +345,89 @@ async function showFailureDialog(parentWindow, errorInfo = {}) {
345
345
  .filter(Boolean)
346
346
  .join("\n");
347
347
 
348
- const result = await dialog.showMessageBox(parentWindow || null, {
349
- type: "error",
350
- title: "GAIA install failed",
351
- message,
352
- detail,
353
- buttons: [
354
- "Retry",
355
- "Manual install instructions",
356
- "Copy log path",
357
- "Open log file",
358
- "Quit",
359
- ],
360
- defaultId: 0,
361
- cancelId: 4,
362
- noLink: true,
363
- });
364
-
365
- switch (result.response) {
366
- case 0:
367
- return "retry";
368
- case 1: {
369
- try {
370
- await shell.openExternal("https://amd-gaia.ai/quickstart#cli-install");
371
- } catch {
372
- // ignore
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
- return "manual";
375
- }
376
- case 2: {
377
- try {
378
- clipboard.writeText(logPath);
379
- } catch {
380
- // ignore
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
- // Keep the user in the loop — re-show the dialog so they can pick an action.
383
- return showFailureDialog(parentWindow, errorInfo);
384
- }
385
- case 3: {
386
- try {
387
- await shell.openPath(logPath);
388
- } catch {
389
- // ignore
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
- return showFailureDialog(parentWindow, errorInfo);
424
+ case 5:
425
+ default:
426
+ return "quit";
392
427
  }
393
- case 4:
394
- default:
395
- return "quit";
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");
@@ -139,6 +157,10 @@ let logStream = null;
139
157
  */
140
158
  let logRotatedThisSession = false;
141
159
 
160
+ function isTruthyEnv(value) {
161
+ return /^(1|true|yes|on)$/i.test(String(value || ""));
162
+ }
163
+
142
164
  function ensureGaiaHome() {
143
165
  try {
144
166
  if (!fs.existsSync(GAIA_HOME)) {
@@ -697,9 +719,12 @@ function findBundledUvResource() {
697
719
  */
698
720
  async function installBundledUv(sourcePath, platformKey) {
699
721
  const expected = BUNDLED_UV_SHA256[platformKey];
700
- if (!expected) {
722
+ if (!expected || expected.startsWith("<")) {
723
+ // Enforce strict verification: builds MUST populate the expected SHA
724
+ // for packaged binaries. Failing fast prevents shipping an unverified
725
+ // uv binary which would be a supply-chain regression.
701
726
  throw new InstallError(
702
- `No bundled uv checksum registered for platform ${platformKey}.`,
727
+ `No bundled uv checksum registered for platform ${platformKey}. Build must populate BUNDLED_UV_SHA256.${platformKey}`,
703
728
  { stage: STAGES.ENSURE_UV }
704
729
  );
705
730
  }
@@ -741,16 +766,20 @@ async function installBundledUv(sourcePath, platformKey) {
741
766
  );
742
767
  }
743
768
 
744
- if (actual !== expected) {
745
- try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
746
- throw new InstallError(
747
- `Bundled uv SHA256 mismatch (expected ${expected}, got ${actual}).`,
748
- {
749
- stage: STAGES.ENSURE_UV,
750
- suggestion:
751
- "The AppImage/installer may be corrupt. Re-download from https://amd-gaia.ai and try again.",
752
- }
753
- );
769
+ if (expected) {
770
+ if (actual !== expected) {
771
+ try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
772
+ throw new InstallError(
773
+ `Bundled uv SHA256 mismatch (expected ${expected}, got ${actual}).`,
774
+ {
775
+ stage: STAGES.ENSURE_UV,
776
+ suggestion:
777
+ "The AppImage/installer may be corrupt. Re-download from https://amd-gaia.ai and try again.",
778
+ }
779
+ );
780
+ }
781
+ } else {
782
+ log("No expected SHA registered for bundled uv; installed binary will not be verified locally.");
754
783
  }
755
784
 
756
785
  try {
@@ -836,6 +865,10 @@ async function ensureUv({ onProgress, isPackaged } = {}) {
836
865
 
837
866
  // Verify the source resource matches the manifest before copying —
838
867
  // catches AppImage corruption before we touch the user's home.
868
+ // Enforce that the packaged build provides an expected SHA for the
869
+ // bundled resource. CI replaces the placeholder with the extracted
870
+ // binary's SHA during the build; missing/placeholder values are a
871
+ // build-time error and are rejected at runtime here.
839
872
  const srcHash = await sha256File(bundled);
840
873
  if (srcHash !== expectedSha) {
841
874
  throw new InstallError(
@@ -925,6 +958,52 @@ async function ensureUv({ onProgress, isPackaged } = {}) {
925
958
  return;
926
959
  }
927
960
 
961
+ // Packaged Windows rescue: try the Astral PowerShell installer even when
962
+ // running a packaged build. This provides an automated recovery path for
963
+ // end users on clean machines that don't have uv and where the installer
964
+ // build unexpectedly omitted a bundled binary. It is a last-resort and
965
+ // non-fatal attempt; on failure we fall through to the generic error.
966
+ if (IS_WINDOWS && !isDev) {
967
+ log("Packaged Windows: attempting automated uv installer (rescue)");
968
+ try {
969
+ const rescue = await runCommand(
970
+ "powershell",
971
+ [
972
+ "-ExecutionPolicy",
973
+ "Bypass",
974
+ "-Command",
975
+ "irm https://astral.sh/uv/install.ps1 | iex",
976
+ ],
977
+ { stageLabel: "uv-install-packaged-rescue" }
978
+ );
979
+ if (rescue.code === 0) {
980
+ // Ensure common install locations are present on PATH for this
981
+ // process in case the installer placed uv in a user-local bin.
982
+ const candidates = [
983
+ path.join(os.homedir(), ".local", "bin"),
984
+ path.join(os.homedir(), ".cargo", "bin"),
985
+ ];
986
+ for (const uvDir of candidates) {
987
+ if (process.env.PATH && !process.env.PATH.includes(uvDir)) {
988
+ process.env.PATH = `${uvDir}${path.delimiter}${process.env.PATH}`;
989
+ log(`Added ${uvDir} to PATH for this process`);
990
+ }
991
+ }
992
+ if (commandExists("uv")) {
993
+ log("Packaged Windows: uv installed and found on PATH (rescue succeeded)");
994
+ addManagedBinToPath();
995
+ report(STAGES.ENSURE_UV, 100, "uv ready (system, unverified)");
996
+ return;
997
+ }
998
+ log("Packaged Windows: uv installer ran but uv not found on PATH");
999
+ } else {
1000
+ log(`Packaged Windows: automated uv installer exited ${rescue.code}`);
1001
+ }
1002
+ } catch (rescueErr) {
1003
+ log(`Packaged Windows: rescue installer threw: ${rescueErr.message}`);
1004
+ }
1005
+ }
1006
+
928
1007
  // Packaged build, but we somehow don't have a bundled binary for this
929
1008
  // platform AND no system uv. Last-ditch: accept an unverified system uv
930
1009
  // if present; otherwise fail with a clear message.
@@ -937,11 +1016,11 @@ async function ensureUv({ onProgress, isPackaged } = {}) {
937
1016
  }
938
1017
 
939
1018
  throw new InstallError(
940
- `No bundled uv available for ${process.platform}-${process.arch} and no system uv found.`,
1019
+ `GAIA could not find or install its Python helper (uv) required to provision the backend.`,
941
1020
  {
942
1021
  stage: STAGES.ENSURE_UV,
943
1022
  suggestion:
944
- "Install uv manually from https://astral.sh/uv and re-launch GAIA.",
1023
+ "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
1024
  }
946
1025
  );
947
1026
  }
@@ -995,6 +1074,8 @@ async function installBackend(opts = {}) {
995
1074
  const pipPackage = localWheel
996
1075
  ? `${localWheel}[ui]`
997
1076
  : `amd-gaia[ui]==${version}`;
1077
+ const skipGaiaInit =
1078
+ Boolean(opts.skipGaiaInit) || isTruthyEnv(process.env.GAIA_SKIP_GAIA_INIT);
998
1079
 
999
1080
  log("================================================");
1000
1081
  log(" Installing GAIA backend");
@@ -1091,7 +1172,7 @@ async function installBackend(opts = {}) {
1091
1172
  report(STAGES.INSTALL_PACKAGE, 100, "GAIA package installed");
1092
1173
 
1093
1174
  // Stage 4: gaia init
1094
- if (!opts.skipGaiaInit) {
1175
+ if (!skipGaiaInit) {
1095
1176
  setState(STATES.INSTALLING, { stage: STAGES.GAIA_INIT, version });
1096
1177
  report(
1097
1178
  STAGES.GAIA_INIT,
@@ -1114,7 +1195,7 @@ async function installBackend(opts = {}) {
1114
1195
  }
1115
1196
  report(STAGES.GAIA_INIT, 100, "Lemonade Server setup complete");
1116
1197
  } else {
1117
- log("Skipping gaia init (skipGaiaInit=true)");
1198
+ log("Skipping gaia init (skipGaiaInit=true or GAIA_SKIP_GAIA_INIT set)");
1118
1199
  }
1119
1200
 
1120
1201
  // Stage 5: verify
@@ -1135,6 +1216,46 @@ async function installBackend(opts = {}) {
1135
1216
  log(`Verified gaia binary: ${verifiedBin} (version=${installedVersion || "unknown"})`);
1136
1217
  report(STAGES.VERIFY, 100, "Install verified");
1137
1218
 
1219
+ // Ensure a user-accessible shim is created so users who installed via
1220
+ // AppImage can run `gaia` from a terminal without manually adding the
1221
+ // venv bin directory to their PATH. Do not overwrite an existing system
1222
+ // `gaia` or an existing shim the user may have created.
1223
+ try {
1224
+ // Only create shims on POSIX-like systems (AppImage target).
1225
+ if (process.platform !== "win32") {
1226
+ const userBin = process.env.XDG_BIN_HOME || path.join(os.homedir(), ".local", "bin");
1227
+ const shimPath = path.join(userBin, "gaia");
1228
+
1229
+ // Only create a shim if there's no `gaia` already on PATH and no shim
1230
+ // at the target location. This avoids clobbering system packages.
1231
+ if (!commandExists("gaia") && !fs.existsSync(shimPath)) {
1232
+ try {
1233
+ // Basic sanity-check on the target path to avoid writing a
1234
+ // wrapper that could execute an arbitrary command. The
1235
+ // verifiedBin is produced by our installer and is expected to be
1236
+ // a normal filesystem path (alphanum, dash, dot, slash, underscore).
1237
+ if (!/^[\w\-./]+$/.test(verifiedBin)) {
1238
+ log(`Refusing to create shim: verified bin path looks suspicious: ${verifiedBin}`);
1239
+ } else {
1240
+ fs.mkdirSync(userBin, { recursive: true });
1241
+ const wrapper = `#!/bin/sh\nexec \"${verifiedBin}\" \"$@\"\n`;
1242
+ fs.writeFileSync(shimPath, wrapper, { mode: 0o755 });
1243
+ log(`Created user shim at ${shimPath} pointing to ${verifiedBin}`);
1244
+ }
1245
+ } catch (err) {
1246
+ log(`Could not create user shim at ${shimPath}: ${err.message}`);
1247
+ }
1248
+ } else if (fs.existsSync(shimPath)) {
1249
+ log(`User shim already exists at ${shimPath}; leaving it intact`);
1250
+ } else {
1251
+ log("A system 'gaia' binary was found on PATH; skipping shim creation");
1252
+ }
1253
+ }
1254
+ } catch (err) {
1255
+ // Non-fatal: proceed even if shim creation fails.
1256
+ log(`Shim creation check failed: ${err.message}`);
1257
+ }
1258
+
1138
1259
  setState(STATES.READY, { stage: null, version, installedVersion });
1139
1260
  log("Backend install complete");
1140
1261
  }