@amd-gaia/agent-ui 0.25.2-rc.3 → 0.25.2-rc.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/AdvancedSettings-DxzL09lt.js +1 -0
- package/dist/assets/ConnectorsPane-BOaVt5wd.js +1 -0
- package/dist/assets/DocumentLibrary-DjgHNHF0.js +4 -0
- package/dist/assets/FileBrowser-Dil1lZJ4.css +1 -0
- package/dist/assets/FileBrowser-mE75Zm-P.js +4 -0
- package/dist/assets/MemoryDashboard-AN4yi3u6.js +1 -0
- package/dist/assets/MobileAccessModal-SPNutZLD.js +2 -0
- package/dist/assets/ScheduleManager-DUwRXGd2.js +2 -0
- package/dist/assets/{apiBase-BmPG2Rib.js → apiBase-CS6zLsJY.js} +1 -1
- package/dist/assets/index-B73021Op.js +79 -0
- package/dist/assets/index-DJViDewM.css +1 -0
- package/dist/assets/{message-square-DuVoNniw.js → message-square-CLnmengs.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/preload.cjs +13 -0
- package/services/auto-updater.cjs +409 -79
- package/services/backend-installer.cjs +43 -4
- package/dist/assets/AdvancedSettings-CjFMBzrY.js +0 -1
- package/dist/assets/AdvancedSettings-D5VKpkPY.css +0 -1
- package/dist/assets/ConnectorsPane-C0mIy_8h.js +0 -1
- package/dist/assets/DocumentLibrary-BaLMM9_k.js +0 -4
- package/dist/assets/FileBrowser-D04I8Pvv.css +0 -1
- package/dist/assets/FileBrowser-DGmwRsbJ.js +0 -4
- package/dist/assets/MemoryDashboard-0_w56Tv9.js +0 -1
- package/dist/assets/MobileAccessModal-CvPI6iz2.js +0 -2
- package/dist/assets/ScheduleManager-DFFKqsC8.js +0 -2
- package/dist/assets/index-Bp5BAewB.js +0 -79
- package/dist/assets/index-DktitYpA.css +0 -1
|
@@ -4,14 +4,12 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* auto-updater.cjs — GAIA Agent UI auto-update service.
|
|
6
6
|
*
|
|
7
|
-
* Wraps `electron-updater` against a
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* no-
|
|
13
|
-
* work (#1719); until it lands, point the env var at any generic feed (a local
|
|
14
|
-
* mock works) to exercise the path.
|
|
7
|
+
* Wraps `electron-updater` against a generic feed. A feed set in
|
|
8
|
+
* `GAIA_UPDATE_FEED_URL` or `feedUrl` in ~/.gaia/update-config.json (the R2
|
|
9
|
+
* channel of #1724) wins. Otherwise each check reads the GitHub releases and
|
|
10
|
+
* points the feed at the newest one on this install's channel that publishes
|
|
11
|
+
* this platform's `latest*.yml`; when none does, the updater enters the
|
|
12
|
+
* `no-channel` state and says so (no silent "up to date").
|
|
15
13
|
*
|
|
16
14
|
* Implements §4 Layer 3 + §7 Phase F of docs/plans/desktop-installer.mdx.
|
|
17
15
|
* Issue #1336: in-app rollback to a specific previous release still lists and
|
|
@@ -23,7 +21,9 @@
|
|
|
23
21
|
* `app.whenReady`)
|
|
24
22
|
* - Subsequent checks every 4 hours via re-scheduling setTimeout
|
|
25
23
|
* - Concurrent-check guard so checks never overlap
|
|
26
|
-
* -
|
|
24
|
+
* - Auto-update is OFF by default (`autoUpdate` in ~/.gaia/update-config.json,
|
|
25
|
+
* shared with the Terminal UI). Off, a found update is only offered: the
|
|
26
|
+
* renderer's "Update now" calls downloadUpdate(). On, it downloads silently.
|
|
27
27
|
* - On `update-downloaded`, show a native dialog: "Update ready — restart?"
|
|
28
28
|
* - Renderer integration via IPC channel `gaia:update:status`
|
|
29
29
|
* - Disabled entirely via GAIA_DISABLE_UPDATE=1 env var (CI / dev / corp)
|
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
* - listReleases() → fetch available releases from GitHub
|
|
39
39
|
* - installVersion(tag) → downgrade/install a specific tagged release
|
|
40
40
|
* - clearPin() → resume auto-updates (clear the version pin)
|
|
41
|
+
* - setAutoUpdate(enabled) → persist the auto-update preference
|
|
42
|
+
* - downloadUpdate() → download the offered update ("Update now")
|
|
43
|
+
* - restartToInstall() → restart into a downloaded update
|
|
41
44
|
* - STATES → string constants for valid states
|
|
42
45
|
*
|
|
43
46
|
* Design note: `electron` and `electron-updater` are lazy-required inside
|
|
@@ -80,6 +83,62 @@ const GITHUB_API_RELEASES =
|
|
|
80
83
|
// release unreachable.
|
|
81
84
|
const MAX_RELEASES_SHOWN = 10;
|
|
82
85
|
|
|
86
|
+
const RELEASES_PAGE = "https://github.com/amd/gaia/releases";
|
|
87
|
+
|
|
88
|
+
// A black-holed connection must fail, not leave "Checking…" up forever.
|
|
89
|
+
const GITHUB_TIMEOUT_MS = 20 * 1000;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* GET the release list within GITHUB_TIMEOUT_MS. Resolves to the parsed JSON;
|
|
93
|
+
* rejects with `.kind` set to "timeout", "network", "http" (with `.status`)
|
|
94
|
+
* or "garbled", so each caller can word its own message.
|
|
95
|
+
*/
|
|
96
|
+
async function _fetchReleases() {
|
|
97
|
+
let resp;
|
|
98
|
+
try {
|
|
99
|
+
resp = await fetch(GITHUB_API_RELEASES, {
|
|
100
|
+
headers: { Accept: "application/vnd.github+json", "User-Agent": "GAIA-Agent-UI" },
|
|
101
|
+
signal: AbortSignal.timeout(GITHUB_TIMEOUT_MS),
|
|
102
|
+
});
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const timedOut = err && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
105
|
+
throw Object.assign(new Error((err && err.message) || String(err)), {
|
|
106
|
+
kind: timedOut ? "timeout" : "network",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (!resp.ok) {
|
|
110
|
+
throw Object.assign(new Error(`HTTP ${resp.status}`), { kind: "http", status: resp.status });
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
return await resp.json();
|
|
114
|
+
} catch (err) {
|
|
115
|
+
const timedOut = err && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
116
|
+
throw Object.assign(new Error((err && err.message) || String(err)), {
|
|
117
|
+
kind: timedOut ? "timeout" : "garbled",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The user-facing sentence for a _fetchReleases failure while `doing`. */
|
|
123
|
+
function _releasesErrorMessage(err, doing) {
|
|
124
|
+
switch (err && err.kind) {
|
|
125
|
+
case "timeout":
|
|
126
|
+
return `GitHub didn't answer within ${GITHUB_TIMEOUT_MS / 1000} seconds when GAIA tried to ${doing} — try again later, or download an installer from ${RELEASES_PAGE}.`;
|
|
127
|
+
case "http": {
|
|
128
|
+
const limited = err.status === 403 || err.status === 429;
|
|
129
|
+
return (
|
|
130
|
+
`GitHub answered HTTP ${err.status} when GAIA tried to ${doing}` +
|
|
131
|
+
(limited ? " — its hourly limit for this network may be used up; try again later" : " — try again later") +
|
|
132
|
+
`, or download an installer from ${RELEASES_PAGE}.`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
case "garbled":
|
|
136
|
+
return `GitHub sent a response GAIA couldn't read when it tried to ${doing} (${err.message}) — try again later, or download an installer from ${RELEASES_PAGE}.`;
|
|
137
|
+
default:
|
|
138
|
+
return `Couldn't reach GitHub to ${doing} (${(err && err.message) || err}) — check your connection, or download an installer from ${RELEASES_PAGE}.`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
83
142
|
const STATES = Object.freeze({
|
|
84
143
|
IDLE: "idle",
|
|
85
144
|
CHECKING: "checking",
|
|
@@ -88,7 +147,8 @@ const STATES = Object.freeze({
|
|
|
88
147
|
DOWNLOADED: "downloaded",
|
|
89
148
|
ERROR: "error",
|
|
90
149
|
DISABLED: "disabled",
|
|
91
|
-
// No
|
|
150
|
+
// No release publishes a desktop update for this platform — said in
|
|
151
|
+
// Settings, never shown as "up to date".
|
|
92
152
|
NO_CHANNEL: "no-channel",
|
|
93
153
|
});
|
|
94
154
|
|
|
@@ -103,6 +163,13 @@ const state = {
|
|
|
103
163
|
error: null,
|
|
104
164
|
currentVersion: null,
|
|
105
165
|
pinnedVersion: null,
|
|
166
|
+
autoUpdate: false,
|
|
167
|
+
// Offer release candidates too; off by default. A release-candidate build
|
|
168
|
+
// is offered newer candidates without it.
|
|
169
|
+
includePrereleases: false,
|
|
170
|
+
// update-config.json could not be read; kept apart from `error` so a later
|
|
171
|
+
// check's status never hides it.
|
|
172
|
+
configError: null,
|
|
106
173
|
};
|
|
107
174
|
|
|
108
175
|
let mainWindowRef = null;
|
|
@@ -163,26 +230,82 @@ function _loadPin() {
|
|
|
163
230
|
}
|
|
164
231
|
|
|
165
232
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
|
|
233
|
+
* Off unless the user turned it on: the app still changes fast. An unreadable
|
|
234
|
+
* file is reported (Settings shows it), and auto-update stays off until fixed.
|
|
235
|
+
*/
|
|
236
|
+
function _loadAutoUpdate() {
|
|
237
|
+
if (!fs.existsSync(UPDATE_CONFIG_PATH)) {
|
|
238
|
+
return { autoUpdate: false, includePrereleases: false, error: null };
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const cfg = JSON.parse(fs.readFileSync(UPDATE_CONFIG_PATH, "utf8"));
|
|
242
|
+
return {
|
|
243
|
+
autoUpdate: Boolean(cfg && cfg.autoUpdate === true),
|
|
244
|
+
includePrereleases: Boolean(cfg && cfg.includePrereleases === true),
|
|
245
|
+
error: null,
|
|
246
|
+
};
|
|
247
|
+
} catch (err) {
|
|
248
|
+
const error =
|
|
249
|
+
`${UPDATE_CONFIG_PATH} cannot be read (${(err && err.message) || err}), so auto-update is off — ` +
|
|
250
|
+
"fix the file by hand or delete it.";
|
|
251
|
+
log("error", error);
|
|
252
|
+
return { autoUpdate: false, includePrereleases: false, error };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** "0.25.2-rc.3" → true. A candidate build keeps being offered candidates. */
|
|
257
|
+
function _isPrereleaseVersion(version) {
|
|
258
|
+
return typeof version === "string" && /^\d+\.\d+\.\d+-/.test(version);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Whether release candidates are offered: the user opted in, this build is
|
|
263
|
+
* itself a candidate, or GAIA_UPDATE_PRERELEASE=1. A channel, not an install
|
|
264
|
+
* trigger: with auto-update off an offered candidate still waits to be asked.
|
|
169
265
|
*/
|
|
266
|
+
function _prereleasesAllowed() {
|
|
267
|
+
return (
|
|
268
|
+
process.env.GAIA_UPDATE_PRERELEASE === "1" ||
|
|
269
|
+
state.includePrereleases ||
|
|
270
|
+
_isPrereleaseVersion(state.currentVersion)
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
170
274
|
function _savePin(pinnedVersion) {
|
|
275
|
+
_writeConfig({ pinnedVersion });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Merge `patch` into update-config.json. Propagates write errors so a failed
|
|
280
|
+
* pin write can abort a rollback (AC2 must not be silently broken). Callers
|
|
281
|
+
* that can tolerate a failed write (e.g. clearPin) should catch.
|
|
282
|
+
*
|
|
283
|
+
* A file that can't be read or parsed is refused, never overwritten: it holds
|
|
284
|
+
* keys this app doesn't own (feedUrl, the Terminal UI's pin). The write goes
|
|
285
|
+
* to a temp file and is renamed over, so a crash never leaves half a file.
|
|
286
|
+
*/
|
|
287
|
+
function _writeConfig(patch) {
|
|
171
288
|
fs.mkdirSync(path.dirname(UPDATE_CONFIG_PATH), { recursive: true });
|
|
172
|
-
// Merge into any existing config so we don't clobber a persisted feedUrl.
|
|
173
289
|
let existing = {};
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
existing = JSON.parse(fs.readFileSync(UPDATE_CONFIG_PATH, "utf8"))
|
|
290
|
+
if (fs.existsSync(UPDATE_CONFIG_PATH)) {
|
|
291
|
+
try {
|
|
292
|
+
existing = JSON.parse(fs.readFileSync(UPDATE_CONFIG_PATH, "utf8"));
|
|
293
|
+
} catch (err) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
`${UPDATE_CONFIG_PATH} cannot be read (${(err && err.message) || err}), so the setting was not saved — ` +
|
|
296
|
+
"fix the file by hand or delete it."
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`${UPDATE_CONFIG_PATH} does not hold a settings object, so the setting was not saved — ` +
|
|
302
|
+
"fix the file by hand or delete it."
|
|
303
|
+
);
|
|
177
304
|
}
|
|
178
|
-
} catch {
|
|
179
|
-
existing = {};
|
|
180
305
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
"utf8"
|
|
185
|
-
);
|
|
306
|
+
const tmp = `${UPDATE_CONFIG_PATH}.tmp`;
|
|
307
|
+
fs.writeFileSync(tmp, JSON.stringify({ ...existing, ...patch }, null, 2), "utf8");
|
|
308
|
+
fs.renameSync(tmp, UPDATE_CONFIG_PATH);
|
|
186
309
|
}
|
|
187
310
|
|
|
188
311
|
// ── Forward-update feed resolution (issue #1724) ────────────────────────────────
|
|
@@ -209,25 +332,19 @@ function _resolveFeedUrl() {
|
|
|
209
332
|
|
|
210
333
|
/**
|
|
211
334
|
* Point electron-updater at the configured generic (R2) feed + mutable channel.
|
|
212
|
-
*
|
|
213
|
-
*
|
|
335
|
+
* With nothing configured, each check instead reads the newest GitHub release
|
|
336
|
+
* on this install's channel (_applyGithubFeed) — where releases publish their
|
|
337
|
+
* latest*.yml today.
|
|
214
338
|
*
|
|
215
|
-
* @returns {boolean} true when a feed
|
|
339
|
+
* @returns {boolean} true when a feed is ready, false when setting it failed.
|
|
216
340
|
*/
|
|
217
341
|
function _applyForwardFeed() {
|
|
218
342
|
if (!autoUpdaterRef) return false;
|
|
219
343
|
const url = _resolveFeedUrl();
|
|
220
344
|
if (!url) {
|
|
221
345
|
forwardFeedUrl = null;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
error:
|
|
225
|
-
"No update channel configured — set GAIA_UPDATE_FEED_URL (or feedUrl " +
|
|
226
|
-
"in ~/.gaia/update-config.json) to your R2 channel base URL. " +
|
|
227
|
-
"Auto-update is paused until a feed is configured.",
|
|
228
|
-
});
|
|
229
|
-
log("warn", "No update feed configured — auto-update paused (NO_CHANNEL)");
|
|
230
|
-
return false;
|
|
346
|
+
log("info", "No update feed configured — checks read GitHub releases");
|
|
347
|
+
return true;
|
|
231
348
|
}
|
|
232
349
|
try {
|
|
233
350
|
autoUpdaterRef.setFeedURL({
|
|
@@ -249,6 +366,79 @@ function _applyForwardFeed() {
|
|
|
249
366
|
}
|
|
250
367
|
}
|
|
251
368
|
|
|
369
|
+
/** -1, 0 or 1 comparing "0.25.2-rc.3"-style versions; candidates sort before their final. */
|
|
370
|
+
function _compareVersions(a, b) {
|
|
371
|
+
const parse = (v) => {
|
|
372
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-rc\.?(\d+))?$/.exec(String(v || ""));
|
|
373
|
+
if (!m) return null;
|
|
374
|
+
return [Number(m[1]), Number(m[2]), Number(m[3]), m[4] === undefined ? Infinity : Number(m[4])];
|
|
375
|
+
};
|
|
376
|
+
const x = parse(a);
|
|
377
|
+
const y = parse(b);
|
|
378
|
+
if (!x || !y) return NaN;
|
|
379
|
+
for (let i = 0; i < 4; i++) {
|
|
380
|
+
if (x[i] !== y[i]) return x[i] < y[i] ? -1 : 1;
|
|
381
|
+
}
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** The update manifest electron-updater reads on this platform. */
|
|
386
|
+
function _platformManifest() {
|
|
387
|
+
if (process.platform === "win32") return "latest.yml";
|
|
388
|
+
if (process.platform === "darwin") return "latest-mac.yml";
|
|
389
|
+
return "latest-linux.yml";
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Point the feed at the newest GitHub release on this install's channel that
|
|
394
|
+
* carries this platform's installer and update manifest; returns its tag.
|
|
395
|
+
* Throws an actionable error when GitHub can't be read. Returns null, in the
|
|
396
|
+
* NO_CHANNEL state, when no release has an update for this platform.
|
|
397
|
+
*/
|
|
398
|
+
async function _applyGithubFeed() {
|
|
399
|
+
const allowPrerelease = _prereleasesAllowed();
|
|
400
|
+
let releases;
|
|
401
|
+
try {
|
|
402
|
+
releases = await _fetchReleases();
|
|
403
|
+
} catch (err) {
|
|
404
|
+
throw new Error(_releasesErrorMessage(err, "check for updates"));
|
|
405
|
+
}
|
|
406
|
+
if (!Array.isArray(releases)) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
`GitHub sent a response GAIA couldn't read when it tried to check for updates (not a list) — try again later, or see ${RELEASES_PAGE}.`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
const manifest = _platformManifest();
|
|
412
|
+
const candidates = releases.filter(
|
|
413
|
+
(r) =>
|
|
414
|
+
r &&
|
|
415
|
+
!r.draft &&
|
|
416
|
+
TAG_RE.test(r.tag_name || "") &&
|
|
417
|
+
!Number.isNaN(_compareVersions(r.tag_name, "0.0.0")) &&
|
|
418
|
+
(allowPrerelease || !r.prerelease) &&
|
|
419
|
+
_releaseHasPlatformAsset(r) &&
|
|
420
|
+
Array.isArray(r.assets) &&
|
|
421
|
+
r.assets.some((a) => a && a.name === manifest)
|
|
422
|
+
);
|
|
423
|
+
if (candidates.length === 0) {
|
|
424
|
+
const error =
|
|
425
|
+
`No GAIA release publishes a desktop update for this platform yet (it needs ${manifest} ` +
|
|
426
|
+
`and its installer), so GAIA can't update itself here — download an installer from ${RELEASES_PAGE}.`;
|
|
427
|
+
setState({ status: STATES.NO_CHANNEL, error });
|
|
428
|
+
log("warn", error);
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
candidates.sort((a, b) => _compareVersions(b.tag_name, a.tag_name));
|
|
432
|
+
const tag = candidates[0].tag_name;
|
|
433
|
+
autoUpdaterRef.setFeedURL({
|
|
434
|
+
provider: "generic",
|
|
435
|
+
url: `https://github.com/amd/gaia/releases/download/${tag}/`,
|
|
436
|
+
channel: DEFAULT_CHANNEL,
|
|
437
|
+
});
|
|
438
|
+
log("info", `Update feed set from GitHub: ${tag}`);
|
|
439
|
+
return tag;
|
|
440
|
+
}
|
|
441
|
+
|
|
252
442
|
// ── State management ─────────────────────────────────────────────────────────
|
|
253
443
|
|
|
254
444
|
function broadcastState() {
|
|
@@ -307,7 +497,7 @@ function _releaseHasPlatformAsset(release) {
|
|
|
307
497
|
* @returns {Promise<Array<ReleaseInfo> | { error: string }>}
|
|
308
498
|
*/
|
|
309
499
|
async function listReleases() {
|
|
310
|
-
const allowPrerelease =
|
|
500
|
+
const allowPrerelease = _prereleasesAllowed();
|
|
311
501
|
const pinnedVersion = _loadPin();
|
|
312
502
|
|
|
313
503
|
// Get the running version — use the cached electronApi if available, else
|
|
@@ -330,23 +520,13 @@ async function listReleases() {
|
|
|
330
520
|
|
|
331
521
|
let releases;
|
|
332
522
|
try {
|
|
333
|
-
|
|
334
|
-
headers: {
|
|
335
|
-
Accept: "application/vnd.github+json",
|
|
336
|
-
"User-Agent": "GAIA-Agent-UI",
|
|
337
|
-
},
|
|
338
|
-
});
|
|
339
|
-
if (!resp.ok) {
|
|
340
|
-
return {
|
|
341
|
-
error: `Couldn't reach GitHub to list releases (HTTP ${resp.status}) — check your connection; you can still download installers from the Releases page.`,
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
releases = await resp.json();
|
|
523
|
+
releases = await _fetchReleases();
|
|
345
524
|
} catch (err) {
|
|
525
|
+
return { error: _releasesErrorMessage(err, "list releases") };
|
|
526
|
+
}
|
|
527
|
+
if (!Array.isArray(releases)) {
|
|
346
528
|
return {
|
|
347
|
-
error: `
|
|
348
|
-
err && err.message
|
|
349
|
-
})`,
|
|
529
|
+
error: `GitHub sent a response GAIA couldn't read when it tried to list releases (not a list) — try again later, or see ${RELEASES_PAGE}.`,
|
|
350
530
|
};
|
|
351
531
|
}
|
|
352
532
|
|
|
@@ -414,6 +594,7 @@ async function installVersion(tag) {
|
|
|
414
594
|
// the download completes but the user dismisses the restart prompt. If the
|
|
415
595
|
// pin can't be persisted we ABORT — proceeding would let the next launch
|
|
416
596
|
// silently auto-upgrade past the rollback (no silent fallbacks).
|
|
597
|
+
const previousPin = state.pinnedVersion;
|
|
417
598
|
try {
|
|
418
599
|
_savePin(tag);
|
|
419
600
|
} catch (err) {
|
|
@@ -423,6 +604,8 @@ async function installVersion(tag) {
|
|
|
423
604
|
);
|
|
424
605
|
}
|
|
425
606
|
setState({ pinnedVersion: tag });
|
|
607
|
+
// A pin always stops background downloads; this one is downloaded below.
|
|
608
|
+
autoUpdaterRef.autoDownload = false;
|
|
426
609
|
|
|
427
610
|
checkInProgress = true;
|
|
428
611
|
try {
|
|
@@ -441,14 +624,54 @@ async function installVersion(tag) {
|
|
|
441
624
|
// in-flight rollback download (which outlives this checkInProgress window),
|
|
442
625
|
// so it's intentionally deferred.
|
|
443
626
|
|
|
444
|
-
//
|
|
445
|
-
//
|
|
627
|
+
// Targeted check → update-available. autoDownload is off (default, and
|
|
628
|
+
// always while pinned), so download explicitly; update-downloaded then
|
|
629
|
+
// prompts the restart.
|
|
446
630
|
await autoUpdaterRef.checkForUpdates();
|
|
631
|
+
if (state.status !== STATES.AVAILABLE) {
|
|
632
|
+
throw new Error(
|
|
633
|
+
`GAIA ${tag} has no installer for this platform at its release feed` +
|
|
634
|
+
(state.error ? `: ${state.error}` : ".") +
|
|
635
|
+
" Download it from https://github.com/amd/gaia/releases instead."
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
setState({ status: STATES.DOWNLOADING, progress: 0, error: null });
|
|
639
|
+
await autoUpdaterRef.downloadUpdate();
|
|
640
|
+
} catch (err) {
|
|
641
|
+
// Don't leave the app pinned to a version that never installed.
|
|
642
|
+
const pinNote = _restorePinAfterFailedInstall(previousPin, tag);
|
|
643
|
+
const msg = (err && err.message) || String(err);
|
|
644
|
+
setState({ status: STATES.ERROR, error: msg });
|
|
645
|
+
throw new Error(`${msg} ${pinNote}`);
|
|
447
646
|
} finally {
|
|
448
647
|
checkInProgress = false;
|
|
449
648
|
}
|
|
450
649
|
}
|
|
451
650
|
|
|
651
|
+
/** Undo the pin a failed install wrote; returns the sentence saying where things stand. */
|
|
652
|
+
function _restorePinAfterFailedInstall(previousPin, failedTag) {
|
|
653
|
+
try {
|
|
654
|
+
_savePin(previousPin || null);
|
|
655
|
+
} catch (err) {
|
|
656
|
+
// The file still pins failedTag; say so rather than claim otherwise.
|
|
657
|
+
log("error", "Failed to restore the pin after a failed install:", err && err.message);
|
|
658
|
+
return (
|
|
659
|
+
`The pin to ${failedTag} could not be undone in ${UPDATE_CONFIG_PATH} ` +
|
|
660
|
+
`(${(err && err.message) || err}) — fix that file, then use Unpin in Settings → General → Updates.`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
if (!previousPin) {
|
|
664
|
+
clearPin();
|
|
665
|
+
return "Updates are not paused.";
|
|
666
|
+
}
|
|
667
|
+
setState({ pinnedVersion: previousPin });
|
|
668
|
+
autoUpdaterRef.setFeedURL({
|
|
669
|
+
provider: "generic",
|
|
670
|
+
url: `https://github.com/amd/gaia/releases/download/${previousPin}/`,
|
|
671
|
+
});
|
|
672
|
+
return `Still pinned to ${previousPin}.`;
|
|
673
|
+
}
|
|
674
|
+
|
|
452
675
|
// ── clearPin / resumeUpdates ──────────────────────────────────────────────────
|
|
453
676
|
|
|
454
677
|
/**
|
|
@@ -456,8 +679,8 @@ async function installVersion(tag) {
|
|
|
456
679
|
*
|
|
457
680
|
* Restores the configured forward (R2 generic) feed, sets autoDownload=true,
|
|
458
681
|
* and clears allowDowngrade so the next check is the normal forward-only flow.
|
|
459
|
-
*
|
|
460
|
-
* than
|
|
682
|
+
* With no feed configured, the next check reads GitHub releases again rather
|
|
683
|
+
* than staying on the rollback feed.
|
|
461
684
|
*/
|
|
462
685
|
function clearPin() {
|
|
463
686
|
// A failed clear is non-fatal — worst case the stale pin re-pauses updates
|
|
@@ -470,16 +693,101 @@ function clearPin() {
|
|
|
470
693
|
setState({ pinnedVersion: null });
|
|
471
694
|
|
|
472
695
|
if (autoUpdaterRef) {
|
|
473
|
-
autoUpdaterRef.autoDownload =
|
|
696
|
+
autoUpdaterRef.autoDownload = state.autoUpdate;
|
|
474
697
|
autoUpdaterRef.allowDowngrade = false;
|
|
475
698
|
// Restore the forward channel (overrides any generic/tagged rollback feed
|
|
476
|
-
// set by installVersion)
|
|
699
|
+
// set by installVersion); unconfigured means GitHub releases.
|
|
477
700
|
_applyForwardFeed();
|
|
478
701
|
}
|
|
479
702
|
|
|
480
703
|
log("info", "Version pin cleared — auto-updates resumed");
|
|
481
704
|
}
|
|
482
705
|
|
|
706
|
+
// ── Auto-update preference / user-driven install ─────────────────────────────
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Persist the auto-update preference. Throws when it can't be saved, so the
|
|
710
|
+
* Settings toggle never shows a value the next launch won't honour.
|
|
711
|
+
*/
|
|
712
|
+
async function setAutoUpdate(enabled) {
|
|
713
|
+
if (typeof enabled !== "boolean") {
|
|
714
|
+
throw new TypeError(`setAutoUpdate expects true or false, got ${typeof enabled}`);
|
|
715
|
+
}
|
|
716
|
+
try {
|
|
717
|
+
_writeConfig({ autoUpdate: enabled });
|
|
718
|
+
} catch (err) {
|
|
719
|
+
throw new Error(
|
|
720
|
+
`Couldn't save the auto-update setting to ${UPDATE_CONFIG_PATH}: ${(err && err.message) || err}`
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
setState({ autoUpdate: enabled });
|
|
724
|
+
if (autoUpdaterRef) {
|
|
725
|
+
autoUpdaterRef.autoDownload = enabled && !state.pinnedVersion;
|
|
726
|
+
autoUpdaterRef.allowPrerelease = _prereleasesAllowed();
|
|
727
|
+
}
|
|
728
|
+
log("info", `Auto-update ${enabled ? "enabled" : "disabled"}`);
|
|
729
|
+
// Turning it on with an update already offered means "install it".
|
|
730
|
+
if (enabled && !state.pinnedVersion && state.status === STATES.AVAILABLE) {
|
|
731
|
+
await downloadUpdate();
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Persist whether release candidates are offered as updates. Independent of
|
|
737
|
+
* auto-update: it changes what is offered, not whether it installs unasked.
|
|
738
|
+
*/
|
|
739
|
+
async function setIncludePrereleases(enabled) {
|
|
740
|
+
if (typeof enabled !== "boolean") {
|
|
741
|
+
throw new TypeError(`setIncludePrereleases expects true or false, got ${typeof enabled}`);
|
|
742
|
+
}
|
|
743
|
+
try {
|
|
744
|
+
_writeConfig({ includePrereleases: enabled });
|
|
745
|
+
} catch (err) {
|
|
746
|
+
throw new Error(
|
|
747
|
+
`Couldn't save the release-candidate setting to ${UPDATE_CONFIG_PATH}: ${(err && err.message) || err}`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
setState({ includePrereleases: enabled });
|
|
751
|
+
if (autoUpdaterRef) {
|
|
752
|
+
autoUpdaterRef.allowPrerelease = _prereleasesAllowed();
|
|
753
|
+
}
|
|
754
|
+
log("info", `Release-candidate updates ${enabled ? "enabled" : "disabled"}`);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** "Update now": download the update a check found; the restart dialog follows. */
|
|
758
|
+
async function downloadUpdate() {
|
|
759
|
+
if (!autoUpdaterRef) {
|
|
760
|
+
throw new Error("downloadUpdate called before auto-updater was initialised");
|
|
761
|
+
}
|
|
762
|
+
if (state.status !== STATES.AVAILABLE) {
|
|
763
|
+
throw new Error(
|
|
764
|
+
`No update is waiting to download (status: ${state.status}) — check for updates first.`
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
log("info", `User asked to install ${state.version}`);
|
|
768
|
+
setState({ status: STATES.DOWNLOADING, progress: 0, error: null });
|
|
769
|
+
try {
|
|
770
|
+
await autoUpdaterRef.downloadUpdate();
|
|
771
|
+
} catch (err) {
|
|
772
|
+
const message = (err && err.message) || String(err);
|
|
773
|
+
setState({ status: STATES.ERROR, error: message });
|
|
774
|
+
throw new Error(`Downloading GAIA ${state.version || ""} failed: ${message}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** Restart into a downloaded update, asking first if work would be cut off. */
|
|
779
|
+
async function restartToInstall() {
|
|
780
|
+
if (!autoUpdaterRef || state.status !== STATES.DOWNLOADED) {
|
|
781
|
+
throw new Error(`No downloaded update to install (status: ${state.status}).`);
|
|
782
|
+
}
|
|
783
|
+
if (!(await confirmRestartRef())) {
|
|
784
|
+
log("info", "Restart put off: work is still running — will install on next quit");
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
log("info", "User chose to restart from the update chip — calling quitAndInstall");
|
|
788
|
+
autoUpdaterRef.quitAndInstall(false, true);
|
|
789
|
+
}
|
|
790
|
+
|
|
483
791
|
// ── Core check ───────────────────────────────────────────────────────────────
|
|
484
792
|
|
|
485
793
|
async function checkForUpdates() {
|
|
@@ -492,15 +800,9 @@ async function checkForUpdates() {
|
|
|
492
800
|
log("warn", "checkForUpdates called before init — ignoring");
|
|
493
801
|
return;
|
|
494
802
|
}
|
|
495
|
-
//
|
|
496
|
-
// of asking electron-updater to check a feed that doesn't exist. Re-resolve
|
|
497
|
-
// first so a feed written to config after init() is picked up here.
|
|
803
|
+
// Re-resolve so a feed written to config after init() is picked up here.
|
|
498
804
|
if (!forwardFeedUrl && !state.pinnedVersion) {
|
|
499
805
|
if (!_applyForwardFeed()) return;
|
|
500
|
-
// Feed just became available after a no-feed (NO_CHANNEL) start — init()
|
|
501
|
-
// skipped arming the periodic scheduler, so arm it now or background checks
|
|
502
|
-
// never resume once the feed goes live.
|
|
503
|
-
if (!scheduledTimeout) scheduleNextCheck();
|
|
504
806
|
}
|
|
505
807
|
if (checkInProgress) {
|
|
506
808
|
log("info", "Skipping check — another check is already in progress");
|
|
@@ -509,6 +811,11 @@ async function checkForUpdates() {
|
|
|
509
811
|
checkInProgress = true;
|
|
510
812
|
setState({ status: STATES.CHECKING, error: null });
|
|
511
813
|
try {
|
|
814
|
+
// No configured feed: aim at the newest GitHub release on this channel,
|
|
815
|
+
// re-read every check so a new release is found.
|
|
816
|
+
if (!forwardFeedUrl && !state.pinnedVersion && !(await _applyGithubFeed())) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
512
819
|
log("info", "Checking for updates...");
|
|
513
820
|
await autoUpdaterRef.checkForUpdates();
|
|
514
821
|
} catch (err) {
|
|
@@ -669,6 +976,22 @@ function registerIpcHandlers() {
|
|
|
669
976
|
clearPin();
|
|
670
977
|
return getState();
|
|
671
978
|
});
|
|
979
|
+
ipcMain.handle("gaia:update:set-auto-update", async (_event, enabled) => {
|
|
980
|
+
await setAutoUpdate(enabled);
|
|
981
|
+
return getState();
|
|
982
|
+
});
|
|
983
|
+
ipcMain.handle("gaia:update:set-include-prereleases", async (_event, enabled) => {
|
|
984
|
+
await setIncludePrereleases(enabled);
|
|
985
|
+
return getState();
|
|
986
|
+
});
|
|
987
|
+
ipcMain.handle("gaia:update:download", async () => {
|
|
988
|
+
await downloadUpdate();
|
|
989
|
+
return getState();
|
|
990
|
+
});
|
|
991
|
+
ipcMain.handle("gaia:update:restart", async () => {
|
|
992
|
+
await restartToInstall();
|
|
993
|
+
return getState();
|
|
994
|
+
});
|
|
672
995
|
|
|
673
996
|
ipcHandlersRegistered = true;
|
|
674
997
|
}
|
|
@@ -682,6 +1005,10 @@ function unregisterIpcHandlers() {
|
|
|
682
1005
|
"gaia:update:list-releases",
|
|
683
1006
|
"gaia:update:install-version",
|
|
684
1007
|
"gaia:update:resume",
|
|
1008
|
+
"gaia:update:set-auto-update",
|
|
1009
|
+
"gaia:update:set-include-prereleases",
|
|
1010
|
+
"gaia:update:download",
|
|
1011
|
+
"gaia:update:restart",
|
|
685
1012
|
];
|
|
686
1013
|
for (const ch of channels) {
|
|
687
1014
|
try {
|
|
@@ -739,6 +1066,10 @@ function init(mainWindow, options = {}) {
|
|
|
739
1066
|
state.pinnedVersion = savedPin;
|
|
740
1067
|
log("info", `Found persisted version pin: ${savedPin} — autoDownload will be paused`);
|
|
741
1068
|
}
|
|
1069
|
+
const loaded = _loadAutoUpdate();
|
|
1070
|
+
state.autoUpdate = loaded.autoUpdate;
|
|
1071
|
+
state.includePrereleases = loaded.includePrereleases;
|
|
1072
|
+
state.configError = loaded.error;
|
|
742
1073
|
|
|
743
1074
|
// Lazy-load Electron and electron-updater. Any failure here is logged
|
|
744
1075
|
// and the updater stays in `idle` — we never crash the app.
|
|
@@ -780,14 +1111,13 @@ function init(mainWindow, options = {}) {
|
|
|
780
1111
|
|
|
781
1112
|
// Configure electron-updater.
|
|
782
1113
|
try {
|
|
783
|
-
//
|
|
784
|
-
// cannot silently download/install a newer version (AC2).
|
|
785
|
-
autoUpdaterRef.autoDownload = savedPin
|
|
1114
|
+
// Off by default; a pin always suppresses it so the scheduled forward-only
|
|
1115
|
+
// check cannot silently download/install a newer version (AC2).
|
|
1116
|
+
autoUpdaterRef.autoDownload = !savedPin && state.autoUpdate;
|
|
786
1117
|
autoUpdaterRef.autoInstallOnAppQuit = true;
|
|
787
1118
|
autoUpdaterRef.disableWebInstaller = true;
|
|
788
1119
|
autoUpdaterRef.allowDowngrade = false;
|
|
789
|
-
autoUpdaterRef.allowPrerelease =
|
|
790
|
-
process.env.GAIA_UPDATE_PRERELEASE === "1";
|
|
1120
|
+
autoUpdaterRef.allowPrerelease = _prereleasesAllowed();
|
|
791
1121
|
|
|
792
1122
|
autoUpdaterRef.logger = {
|
|
793
1123
|
info: (m) => log("info", String(m)),
|
|
@@ -812,10 +1142,9 @@ function init(mainWindow, options = {}) {
|
|
|
812
1142
|
log("warn", "Failed to register IPC handlers:", err && err.message);
|
|
813
1143
|
}
|
|
814
1144
|
|
|
815
|
-
// Point the updater at the configured R2 generic feed (#1724)
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
// don't schedule checks against a nonexistent feed.
|
|
1145
|
+
// Point the updater at the configured R2 generic feed (#1724), or at GitHub
|
|
1146
|
+
// releases when none is configured. When a pin is active, installVersion()
|
|
1147
|
+
// owns the feed (rollback path), so skip.
|
|
819
1148
|
let feedReady = true;
|
|
820
1149
|
if (!savedPin) {
|
|
821
1150
|
feedReady = _applyForwardFeed();
|
|
@@ -823,11 +1152,8 @@ function init(mainWindow, options = {}) {
|
|
|
823
1152
|
|
|
824
1153
|
initialized = true;
|
|
825
1154
|
|
|
826
|
-
if (!feedReady
|
|
827
|
-
log(
|
|
828
|
-
"info",
|
|
829
|
-
"Auto-updater initialized without a channel — checks paused until a feed is configured"
|
|
830
|
-
);
|
|
1155
|
+
if (!feedReady) {
|
|
1156
|
+
log("error", "Auto-updater initialized but its feed could not be set — see the error above");
|
|
831
1157
|
return;
|
|
832
1158
|
}
|
|
833
1159
|
|
|
@@ -872,5 +1198,9 @@ module.exports = {
|
|
|
872
1198
|
listReleases,
|
|
873
1199
|
installVersion,
|
|
874
1200
|
clearPin,
|
|
1201
|
+
setAutoUpdate,
|
|
1202
|
+
setIncludePrereleases,
|
|
1203
|
+
downloadUpdate,
|
|
1204
|
+
restartToInstall,
|
|
875
1205
|
STATES,
|
|
876
1206
|
};
|