aegis-desktop 1.0.13 → 1.0.15
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/SOURCE-OFFER.txt +9 -0
- package/bin/aegis.js +1 -32
- package/lib/account-key.js +1 -56
- package/lib/auto.js +1 -430
- package/lib/avatar/companion.js +1 -698
- package/lib/avatar/cosmetics.js +1 -532
- package/lib/avatar/events.js +1 -344
- package/lib/avatar/holder-turn.js +1 -350
- package/lib/avatar/holders.js +1 -612
- package/lib/avatar/identity.js +1 -641
- package/lib/avatar/level.js +1 -368
- package/lib/avatar/mirror.js +1 -524
- package/lib/avatar/persona.js +1 -388
- package/lib/avatar/profile.js +1 -977
- package/lib/avatar/register.js +1 -307
- package/lib/avatar/store.js +1 -472
- package/lib/avatar/tiers.js +1 -38
- package/lib/avatar/transfer.js +1 -415
- package/lib/avatar/turn.js +1 -216
- package/lib/avatar/voice.js +1 -328
- package/lib/avatar/xp.js +1 -404
- package/lib/deep-link.js +1 -120
- package/lib/fingerprint.js +2 -347
- package/lib/google-connect.js +1 -169
- package/lib/local/agents.js +1 -105
- package/lib/local/autonomous.js +1 -1045
- package/lib/local/claude-pro.js +1 -993
- package/lib/local/context.js +1 -95
- package/lib/local/engine.js +1 -3228
- package/lib/local/entitlement.js +1 -654
- package/lib/local/git-scope.js +1 -228
- package/lib/local/local.js +1 -2135
- package/lib/local/prompt.js +1 -275
- package/lib/local/queue.js +1 -560
- package/lib/local/session-rounds.js +1 -208
- package/lib/local/shell.js +1 -680
- package/lib/local/telemetry.js +1 -470
- package/lib/local/tools.js +1 -1238
- package/lib/local/turn-guard.js +1 -300
- package/lib/local/worktree-lock.js +1 -235
- package/lib/provider-key-mirror.js +1 -252
- package/lib/quick-launcher.js +1 -84
- package/lib/settings.js +1 -890
- package/lib/sync/memory-queue.js +1 -60
- package/lib/sync/persist-gate.js +1 -148
- package/lib/sync/sessions.js +1 -106
- package/lib/window-state.js +1 -66
- package/main.js +3 -4390
- package/package.json +3 -5
- package/preload.js +3 -628
- package/renderer/app.js +1 -6560
- package/renderer/avatar/assemble.js +1 -335
- package/renderer/avatar/avatar.js +1 -380
- package/renderer/avatar/cards.js +1 -141
- package/renderer/avatar/hud.js +1 -206
- package/renderer/avatar/machine.js +1 -213
- package/renderer/avatar/pane.js +1 -439
- package/renderer/avatar/parts.js +1 -437
- package/renderer/budget.js +1 -168
- package/renderer/companion.js +1 -223
- package/renderer/diff.js +1 -359
- package/renderer/diffview.js +1 -336
- package/renderer/fingerprint.js +3 -720
- package/renderer/markdown.js +1 -168
- package/renderer/provider-toolbar.js +1 -134
- package/renderer/providers.js +1 -259
- package/renderer/quick.js +1 -191
- package/renderer/stream-policy.js +1 -306
- package/renderer/transcript-view.js +1 -323
- package/renderer/usage.js +1 -1239
- package/vendor/aegis.js +1 -2054
- package/vendor/attest.js +1 -442
- package/vendor/authorship-key.js +1 -410
- package/vendor/auto-provision.js +1 -134
- package/vendor/brain-catalog.js +1 -268
- package/vendor/byok-catalog.js +1 -230
- package/vendor/credentials.js +1 -494
- package/vendor/device-fingerprint.js +1 -141
- package/vendor/drive-connect.js +1 -85
- package/vendor/drive-upload.js +1 -174
- package/vendor/env-file.js +1 -1273
- package/vendor/fingerprint.js +1 -1064
- package/vendor/foreign-memory.js +1 -669
- package/vendor/google-signin.js +1 -125
- package/vendor/hooks.js +1 -430
- package/vendor/model-discovery.js +1 -330
- package/vendor/platform.js +1 -937
- package/vendor/prefs.js +1 -351
- package/vendor/presence-constants.js +1 -107
- package/vendor/presence.js +1 -438
- package/vendor/record-vault.js +3 -1055
- package/vendor/session-store.js +1 -548
- package/vendor/topup.js +1 -297
- package/vendor/touch.js +1 -458
- package/vendor/update.js +1 -704
- package/vendor/user-models.js +1 -381
package/vendor/update.js
CHANGED
|
@@ -1,706 +1,3 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
// Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
|
|
3
|
-
|
|
4
|
-
// Conveying a modified copy, or running one as a network service, owes the
|
|
5
|
-
// corresponding source under this licence (§5/§13) — see LICENSING.md.
|
|
6
|
-
/**
|
|
7
|
-
* "There is a newer version" — the notice this CLI never had.
|
|
8
|
-
*
|
|
9
|
-
* Checked on 2026-09-15: there was no update check anywhere in the client, so
|
|
10
|
-
* a user learned about a release only by guessing to run
|
|
11
|
-
* `npm i -g aegiscode@latest`. Publishing therefore reached almost nobody, and
|
|
12
|
-
* the installed base drifted months behind the registry.
|
|
13
|
-
*
|
|
14
|
-
* Three rules, because an update check is a background nicety that must never
|
|
15
|
-
* become a liability:
|
|
16
|
-
* · it never blocks — the caller gets a cached answer immediately and the
|
|
17
|
-
* network call settles whenever it settles;
|
|
18
|
-
* · it never throws — offline, proxied, rate-limited or garbage JSON all
|
|
19
|
-
* mean "no notice", not a broken CLI;
|
|
20
|
-
* · it never nags — one check a day, cached, and nothing printed when the
|
|
21
|
-
* user is already current.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
const https = require('node:https');
|
|
25
|
-
|
|
26
|
-
const REGISTRY = 'https://registry.npmjs.org';
|
|
27
|
-
//: Default package. Both hosts ship from npm — the CLI as `aegiscode`, the
|
|
28
|
-
//: desktop as `aegis-desktop` — so one checker serves both; the caller names
|
|
29
|
-
//: which one it is.
|
|
30
|
-
const PKG = 'aegiscode';
|
|
31
|
-
/** One check a day. A CLI that pings the registry every launch is spyware. */
|
|
32
|
-
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
33
|
-
/** The registry is not on the critical path; give up quickly and silently. */
|
|
34
|
-
const TIMEOUT_MS = 2500;
|
|
35
|
-
|
|
36
|
-
/** Compare two semver-ish strings. Returns true when `latest` is newer. */
|
|
37
|
-
function isNewer(latest, current) {
|
|
38
|
-
const parse = (v) =>
|
|
39
|
-
String(v || '')
|
|
40
|
-
.trim()
|
|
41
|
-
.replace(/^v/, '')
|
|
42
|
-
.split('-')[0] // a prerelease never counts as newer than its release
|
|
43
|
-
.split('.')
|
|
44
|
-
.map((n) => Number.parseInt(n, 10) || 0);
|
|
45
|
-
const a = parse(latest);
|
|
46
|
-
const b = parse(current);
|
|
47
|
-
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
48
|
-
const x = a[i] || 0;
|
|
49
|
-
const y = b[i] || 0;
|
|
50
|
-
if (x !== y) return x > y;
|
|
51
|
-
}
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Why a check produced no version. Every one of these used to collapse into a
|
|
57
|
-
* bare `null`, which is enough for the CLI ("no notice") but NOT enough for a
|
|
58
|
-
* host that shows the user a sentence about it: the desktop rendered all six
|
|
59
|
-
* as "registry unreachable", so a 2.5s timeout on a slow VPN was reported as
|
|
60
|
-
* fact about the registry — a false accusation, and unactionable.
|
|
61
|
-
*/
|
|
62
|
-
const REASONS = Object.freeze({
|
|
63
|
-
TIMEOUT: 'timeout',
|
|
64
|
-
NETWORK: 'network',
|
|
65
|
-
NOT_FOUND: 'not-found',
|
|
66
|
-
FORBIDDEN: 'forbidden',
|
|
67
|
-
HTTP: 'http',
|
|
68
|
-
MALFORMED: 'malformed',
|
|
69
|
-
TOO_LARGE: 'too-large',
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Does this failure mean "the network let us down" (worth retrying silently, and
|
|
74
|
-
* NOT worth telling the user about) rather than "the registry answered, and the
|
|
75
|
-
* answer was no" (a real condition the user may need to act on)?
|
|
76
|
-
*
|
|
77
|
-
* `status` is the HTTP status when there was one, so 5xx/429 count as transient
|
|
78
|
-
* while 404/403 do not.
|
|
79
|
-
*/
|
|
80
|
-
function isTransientReason(reason, status = null) {
|
|
81
|
-
if (reason === REASONS.TIMEOUT || reason === REASONS.NETWORK) return true;
|
|
82
|
-
if (reason === REASONS.HTTP) return !status || status >= 500 || status === 429;
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Fetch the registry's `latest` dist-tag, saying WHY when it fails.
|
|
88
|
-
*
|
|
89
|
-
* Resolves `{ ok, latest, reason, status }` and never rejects: `ok` is the
|
|
90
|
-
* version question, `reason` (one of REASONS) is the diagnosis. Use this when
|
|
91
|
-
* you intend to tell someone what happened; use `fetchLatest` when you only
|
|
92
|
-
* care whether there is a notice.
|
|
93
|
-
*/
|
|
94
|
-
function fetchLatestDetailed({ pkg = PKG, timeoutMs = TIMEOUT_MS } = {}) {
|
|
95
|
-
return new Promise((resolve) => {
|
|
96
|
-
let settled = false;
|
|
97
|
-
const done = (v) => {
|
|
98
|
-
if (!settled) {
|
|
99
|
-
settled = true;
|
|
100
|
-
resolve(v);
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
const fail = (reason, status = null) =>
|
|
104
|
-
done({ ok: false, latest: null, reason, status });
|
|
105
|
-
const succeed = (latest) => done({ ok: true, latest, reason: null, status: 200 });
|
|
106
|
-
try {
|
|
107
|
-
// The abbreviated metadata document: a few KB instead of the full
|
|
108
|
-
// packument, which for a package with this many releases is megabytes.
|
|
109
|
-
const req = https.get(
|
|
110
|
-
`${REGISTRY}/${pkg}`,
|
|
111
|
-
{ headers: { accept: 'application/vnd.npm.install-v1+json' }, timeout: timeoutMs },
|
|
112
|
-
(res) => {
|
|
113
|
-
const status = res.statusCode;
|
|
114
|
-
if (status !== 200) {
|
|
115
|
-
res.resume();
|
|
116
|
-
if (status === 404) return fail(REASONS.NOT_FOUND, status);
|
|
117
|
-
if (status === 401 || status === 403) return fail(REASONS.FORBIDDEN, status);
|
|
118
|
-
return fail(REASONS.HTTP, status);
|
|
119
|
-
}
|
|
120
|
-
let body = '';
|
|
121
|
-
res.setEncoding('utf8');
|
|
122
|
-
res.on('data', (c) => {
|
|
123
|
-
body += c;
|
|
124
|
-
if (body.length > 2_000_000) {
|
|
125
|
-
req.destroy();
|
|
126
|
-
fail(REASONS.TOO_LARGE, status);
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
res.on('end', () => {
|
|
130
|
-
try {
|
|
131
|
-
const tags = JSON.parse(body)['dist-tags'];
|
|
132
|
-
const latest = (tags && tags.latest) || null;
|
|
133
|
-
// A 200 that parses but names no `latest` is a malformed
|
|
134
|
-
// document, not a slow network — do not invite a retry for it.
|
|
135
|
-
if (!latest) return fail(REASONS.MALFORMED, status);
|
|
136
|
-
succeed(latest);
|
|
137
|
-
} catch {
|
|
138
|
-
fail(REASONS.MALFORMED, status);
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
);
|
|
143
|
-
req.on('timeout', () => {
|
|
144
|
-
req.destroy();
|
|
145
|
-
fail(REASONS.TIMEOUT);
|
|
146
|
-
});
|
|
147
|
-
// Every socket-level failure — DNS (ENOTFOUND), refused (ECONNREFUSED),
|
|
148
|
-
// TLS, proxy, reset mid-body — is the same story to a user: the network.
|
|
149
|
-
req.on('error', () => fail(REASONS.NETWORK));
|
|
150
|
-
} catch {
|
|
151
|
-
fail(REASONS.NETWORK);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Fetch the registry's `latest` dist-tag. Resolves null on any failure. */
|
|
157
|
-
function fetchLatest(opts = {}) {
|
|
158
|
-
return fetchLatestDetailed(opts).then((r) => (r.ok ? r.latest : null));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
//: Where the AEGIS server answers "am I still allowed to talk to you".
|
|
162
|
-
const DEFAULT_API_BASE = 'https://aegiscloud.org';
|
|
163
|
-
/** /api/v1/version is a tiny JSON document; anything bigger is a proxy page. */
|
|
164
|
-
const VERSION_BODY_CAP = 64 * 1024;
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Ask the SERVER whether this client is still serviceable.
|
|
168
|
-
*
|
|
169
|
-
* Why this exists next to the npm check: they answer different questions.
|
|
170
|
-
* The registry says what was *published*; the server says what it will
|
|
171
|
-
* *accept*. aegis1 refuses every request from a client below
|
|
172
|
-
* `MIN_CLIENT_VERSION` with a 426, so a client that only asks npm learns it is
|
|
173
|
-
* unservicable by being refused mid-task — an outage, not a notice. Asking
|
|
174
|
-
* `/api/v1/version` first turns the same fact into a sentence the user reads
|
|
175
|
-
* before they hit the wall.
|
|
176
|
-
*
|
|
177
|
-
* `/api/v1/version` is informational: it never refuses anyone, and it echoes
|
|
178
|
-
* the verdict for the version header we send (`upgrade_required`).
|
|
179
|
-
*
|
|
180
|
-
* Resolves `{ ok, minClient, latestCli, latestDesktop, upgradeRequired,
|
|
181
|
-
* downloadUrl, reason, status }` and never rejects — same contract as
|
|
182
|
-
* `fetchLatestDetailed`, so a host can render "registry unreachable" and
|
|
183
|
-
* "server unreachable" differently instead of inventing a third story.
|
|
184
|
-
*/
|
|
185
|
-
function fetchServerVersionDetailed({
|
|
186
|
-
base = DEFAULT_API_BASE,
|
|
187
|
-
clientVersion = null,
|
|
188
|
-
timeoutMs = TIMEOUT_MS,
|
|
189
|
-
getImpl = null,
|
|
190
|
-
} = {}) {
|
|
191
|
-
return new Promise((resolve) => {
|
|
192
|
-
let settled = false;
|
|
193
|
-
const done = (v) => {
|
|
194
|
-
if (!settled) {
|
|
195
|
-
settled = true;
|
|
196
|
-
resolve(v);
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
const fail = (reason, status = null) =>
|
|
200
|
-
done({
|
|
201
|
-
ok: false, minClient: null, latestCli: null, latestDesktop: null,
|
|
202
|
-
upgradeRequired: null, downloadUrl: '', reason, status,
|
|
203
|
-
});
|
|
204
|
-
const succeed = (body, status) => {
|
|
205
|
-
const minClient = body && typeof body.min_client === 'string' ? body.min_client : null;
|
|
206
|
-
// A 200 without `min_client` is malformed, not a slow network: do not
|
|
207
|
-
// invite a retry ladder for a document that will parse the same way.
|
|
208
|
-
if (!minClient) return fail(REASONS.MALFORMED, status);
|
|
209
|
-
done({
|
|
210
|
-
ok: true,
|
|
211
|
-
minClient,
|
|
212
|
-
latestCli: typeof body.latest_cli === 'string' ? body.latest_cli : '',
|
|
213
|
-
latestDesktop: typeof body.latest_desktop === 'string' ? body.latest_desktop : '',
|
|
214
|
-
// Trust the header echo only when we actually sent a version: without
|
|
215
|
-
// one the server answers for `require_header=False` and the answer is
|
|
216
|
-
// about nothing in particular. `null` here means "the server did not
|
|
217
|
-
// say", which is different from "the server said no" — the caller must
|
|
218
|
-
// be able to fall back without overriding an answer it was given.
|
|
219
|
-
upgradeRequired:
|
|
220
|
-
clientVersion && typeof body.upgrade_required === 'boolean'
|
|
221
|
-
? body.upgrade_required
|
|
222
|
-
: null,
|
|
223
|
-
downloadUrl: typeof body.download_url === 'string' ? body.download_url : '',
|
|
224
|
-
reason: null,
|
|
225
|
-
status,
|
|
226
|
-
});
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
const url = `${String(base || DEFAULT_API_BASE).replace(/\/+$/, '')}/api/v1/version`;
|
|
231
|
-
const headers = { accept: 'application/json' };
|
|
232
|
-
if (clientVersion) headers['X-AEGIS-Version'] = String(clientVersion);
|
|
233
|
-
|
|
234
|
-
const onResponse = (res) => {
|
|
235
|
-
const status = res.statusCode;
|
|
236
|
-
if (status !== 200) {
|
|
237
|
-
res.resume();
|
|
238
|
-
if (status === 404) return fail(REASONS.NOT_FOUND, status);
|
|
239
|
-
if (status === 401 || status === 403) return fail(REASONS.FORBIDDEN, status);
|
|
240
|
-
// 426 here would mean the endpoint itself were gated; treat it as a
|
|
241
|
-
// real answer rather than a transport failure if it ever happens.
|
|
242
|
-
if (status === 426) return fail(REASONS.HTTP, status);
|
|
243
|
-
return fail(REASONS.HTTP, status);
|
|
244
|
-
}
|
|
245
|
-
let body = '';
|
|
246
|
-
res.setEncoding('utf8');
|
|
247
|
-
res.on('data', (c) => {
|
|
248
|
-
body += c;
|
|
249
|
-
if (body.length > VERSION_BODY_CAP) {
|
|
250
|
-
res.destroy();
|
|
251
|
-
fail(REASONS.TOO_LARGE, status);
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
res.on('end', () => {
|
|
255
|
-
if (settled) return;
|
|
256
|
-
try {
|
|
257
|
-
succeed(JSON.parse(body), status);
|
|
258
|
-
} catch {
|
|
259
|
-
fail(REASONS.MALFORMED, status);
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
if (typeof getImpl === 'function') {
|
|
265
|
-
// Injected transport for tests and hosts that already own a fetch:
|
|
266
|
-
// `getImpl({url, headers, timeoutMs})` resolves `{status, text}`.
|
|
267
|
-
Promise.resolve(getImpl({ url, headers, timeoutMs }))
|
|
268
|
-
.then((r) => {
|
|
269
|
-
const status = r && r.status != null ? r.status : 0;
|
|
270
|
-
if (!status) return fail(REASONS.NETWORK);
|
|
271
|
-
if (status !== 200) {
|
|
272
|
-
if (status === 404) return fail(REASONS.NOT_FOUND, status);
|
|
273
|
-
if (status === 401 || status === 403) return fail(REASONS.FORBIDDEN, status);
|
|
274
|
-
return fail(REASONS.HTTP, status);
|
|
275
|
-
}
|
|
276
|
-
const text = r && typeof r.text === 'string' ? r.text : '';
|
|
277
|
-
if (text.length > VERSION_BODY_CAP) return fail(REASONS.TOO_LARGE, status);
|
|
278
|
-
try {
|
|
279
|
-
succeed(JSON.parse(text), status);
|
|
280
|
-
} catch {
|
|
281
|
-
fail(REASONS.MALFORMED, status);
|
|
282
|
-
}
|
|
283
|
-
})
|
|
284
|
-
.catch(() => fail(REASONS.NETWORK));
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const req = require(/^http:\/\//i.test(url) ? 'node:http' : 'node:https')
|
|
289
|
-
.get(url, { headers, timeout: timeoutMs }, onResponse);
|
|
290
|
-
req.on('timeout', () => {
|
|
291
|
-
req.destroy();
|
|
292
|
-
fail(REASONS.TIMEOUT);
|
|
293
|
-
});
|
|
294
|
-
req.on('error', () => fail(REASONS.NETWORK));
|
|
295
|
-
} catch {
|
|
296
|
-
fail(REASONS.NETWORK);
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/** The server's minimum client version, or null. */
|
|
302
|
-
function fetchServerVersion(opts = {}) {
|
|
303
|
-
return fetchServerVersionDetailed(opts).then((r) => (r.ok ? r.minClient : null));
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* The server's verdict, expressed in the shape the notice/plan code already
|
|
308
|
-
* speaks: `{ behind, latest, required, downloadUrl, ok, reason, status }`.
|
|
309
|
-
*
|
|
310
|
-
* `behind` is true when the SERVER says this client is below its minimum —
|
|
311
|
-
* regardless of whether npm happens to have something newer. That ordering is
|
|
312
|
-
* deliberate: being refused is actionable now, being merely superseded is not.
|
|
313
|
-
*/
|
|
314
|
-
async function serverUpdateVerdict({
|
|
315
|
-
current,
|
|
316
|
-
base = DEFAULT_API_BASE,
|
|
317
|
-
timeoutMs = TIMEOUT_MS,
|
|
318
|
-
getImpl = null,
|
|
319
|
-
} = {}) {
|
|
320
|
-
const res = await fetchServerVersionDetailed({ base, clientVersion: current, timeoutMs, getImpl });
|
|
321
|
-
if (!res.ok) {
|
|
322
|
-
return { ok: false, behind: false, latest: null, required: null, downloadUrl: '', reason: res.reason, status: res.status };
|
|
323
|
-
}
|
|
324
|
-
const required = res.minClient || null;
|
|
325
|
-
// The server's echo decides. Falling back to local arithmetic only when it
|
|
326
|
-
// said nothing (`null`) — OR'ing the two would let our own comparison
|
|
327
|
-
// override the very authority we asked, which is the bug this line exists to
|
|
328
|
-
// not have.
|
|
329
|
-
const behind =
|
|
330
|
-
res.upgradeRequired === true
|
|
331
|
-
? true
|
|
332
|
-
: res.upgradeRequired === false
|
|
333
|
-
? false
|
|
334
|
-
: required
|
|
335
|
-
? isNewer(required, current)
|
|
336
|
-
: false;
|
|
337
|
-
return {
|
|
338
|
-
ok: true,
|
|
339
|
-
behind,
|
|
340
|
-
// The server's own minimum is the target when it is the thing refusing us;
|
|
341
|
-
// a published version is preferred when it names one, because that is what
|
|
342
|
-
// the user should actually install.
|
|
343
|
-
latest: (behind && (res.latestCli || res.latestDesktop)) || required,
|
|
344
|
-
latestCli: res.latestCli,
|
|
345
|
-
latestDesktop: res.latestDesktop,
|
|
346
|
-
required,
|
|
347
|
-
downloadUrl: res.downloadUrl,
|
|
348
|
-
reason: null,
|
|
349
|
-
status: res.status,
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* The notice to show, from cache. Never waits on the network.
|
|
355
|
-
*
|
|
356
|
-
* `cache` is the persisted `{ checkedAt, latest }` blob; `save` persists a new
|
|
357
|
-
* one. Returns `{ latest, behind }`, and kicks off a refresh in the background
|
|
358
|
-
* when the cache is stale.
|
|
359
|
-
*/
|
|
360
|
-
function updateNotice({
|
|
361
|
-
current,
|
|
362
|
-
cache,
|
|
363
|
-
save,
|
|
364
|
-
now = Date.now(),
|
|
365
|
-
fetchImpl = fetchLatest,
|
|
366
|
-
intervalMs = CHECK_INTERVAL_MS,
|
|
367
|
-
} = {}) {
|
|
368
|
-
const c = cache || {};
|
|
369
|
-
const fresh = typeof c.checkedAt === 'number' && now - c.checkedAt < intervalMs;
|
|
370
|
-
|
|
371
|
-
if (!fresh) {
|
|
372
|
-
// Fire and forget: this run reports on what was already known, and the
|
|
373
|
-
// answer lands for the next one. A first run therefore never shows a
|
|
374
|
-
// notice, which is correct — it has nothing to compare against yet.
|
|
375
|
-
Promise.resolve(fetchImpl())
|
|
376
|
-
.then((latest) => {
|
|
377
|
-
if (latest && typeof save === 'function') save({ checkedAt: now, latest });
|
|
378
|
-
})
|
|
379
|
-
.catch(() => {});
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const latest = c.latest || null;
|
|
383
|
-
return { latest, behind: !!(latest && isNewer(latest, current)) };
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* "What happens next", from a check result — the ONE verdict every applier
|
|
388
|
-
* consumes (AEGISONE, decision D7).
|
|
389
|
-
*
|
|
390
|
-
* There are two *mechanics* — `npm i -g` for the CLI's channel and
|
|
391
|
-
* `electron-updater` for the window's — and that is deliberate: they install
|
|
392
|
-
* different things. What must not be duplicated is the *decision*: whether
|
|
393
|
-
* there is anything to do, which version it is, which applier owns it and what
|
|
394
|
-
* the user is told to run. Both hosts ask this function, so the sentence a
|
|
395
|
-
* user reads in the terminal and the sentence the window shows cannot drift.
|
|
396
|
-
*
|
|
397
|
-
* Pure: no fetch, no clock, no cache. Feed it a result from `checkPublished`
|
|
398
|
-
* (or from the cached `updateNotice`) and it names the next action.
|
|
399
|
-
*
|
|
400
|
-
* @param {{current: string, latest: ?string, behind: boolean, channel?: 'npm'|'desktop', pkg?: string}} input
|
|
401
|
-
*/
|
|
402
|
-
function updatePlan({ current, latest, behind, channel = 'npm', pkg = PKG, method = 'npm' } = {}) {
|
|
403
|
-
const currentText = String(current == null ? '' : current);
|
|
404
|
-
const latestText = latest ? String(latest) : null;
|
|
405
|
-
const pending = Boolean(behind) && Boolean(latestText);
|
|
406
|
-
if (!pending) {
|
|
407
|
-
return {
|
|
408
|
-
channel,
|
|
409
|
-
method,
|
|
410
|
-
current: currentText,
|
|
411
|
-
latest: latestText,
|
|
412
|
-
behind: false,
|
|
413
|
-
action: 'none',
|
|
414
|
-
applier: 'none',
|
|
415
|
-
instruction: null,
|
|
416
|
-
apply: null,
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
if (channel === 'desktop') {
|
|
420
|
-
// The window installs its own build: there is nothing for the user to type,
|
|
421
|
-
// and offering `npm i -g aegis-desktop` here would install a *second* copy
|
|
422
|
-
// beside the running app. The applier is the app's own updater.
|
|
423
|
-
return {
|
|
424
|
-
channel,
|
|
425
|
-
method,
|
|
426
|
-
current: currentText,
|
|
427
|
-
latest: latestText,
|
|
428
|
-
behind: true,
|
|
429
|
-
action: 'desktop-update',
|
|
430
|
-
applier: 'electron-updater',
|
|
431
|
-
instruction: 'Aegis Desktop → Check for Updates…',
|
|
432
|
-
apply: null,
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Which installer owns this copy. The command is not cosmetic: `npm i -g`
|
|
437
|
-
// against a pip-installed `aegiscode` writes a second copy beside the one the
|
|
438
|
-
// user actually runs, and the "upgrade" appears to do nothing. Same for an
|
|
439
|
-
// `npx` run, which has nothing installed to upgrade at all.
|
|
440
|
-
const applier = APPLIERS[method] || APPLIERS.npm;
|
|
441
|
-
return {
|
|
442
|
-
channel,
|
|
443
|
-
method,
|
|
444
|
-
current: currentText,
|
|
445
|
-
latest: latestText,
|
|
446
|
-
behind: true,
|
|
447
|
-
action: applier.action,
|
|
448
|
-
applier: applier.name,
|
|
449
|
-
instruction: applier.instruction(pkg),
|
|
450
|
-
apply: applier.apply ? applier.apply(pkg) : null,
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* The ways this product reaches a machine, and what "update it" means for each.
|
|
456
|
-
*
|
|
457
|
-
* `apply` names argv to run — it is what the window's Update button and the
|
|
458
|
-
* CLI's `/upgrade apply` execute. `null` means there is nothing to install:
|
|
459
|
-
* `npx` fetches `latest` on every launch by definition, and a source checkout
|
|
460
|
-
* belongs to its owner's git remote.
|
|
461
|
-
*/
|
|
462
|
-
const APPLIERS = Object.freeze({
|
|
463
|
-
npm: Object.freeze({
|
|
464
|
-
name: 'npm',
|
|
465
|
-
action: 'npm-update',
|
|
466
|
-
instruction: (pkg) => `npm i -g ${pkg}@latest`,
|
|
467
|
-
apply: (pkg) => ({ command: 'npm', args: ['i', '-g', `${pkg}@latest`] }),
|
|
468
|
-
}),
|
|
469
|
-
pip: Object.freeze({
|
|
470
|
-
name: 'pip',
|
|
471
|
-
action: 'pip-update',
|
|
472
|
-
// The published pip package is a launcher for the npm one, so this is
|
|
473
|
-
// genuinely a different command — not an npm call with a different word.
|
|
474
|
-
instruction: (pkg) => `pip install -U ${pkg}`,
|
|
475
|
-
apply: (pkg) => ({ command: process.platform === 'win32' ? 'python' : 'python3',
|
|
476
|
-
args: ['-m', 'pip', 'install', '-U', pkg] }),
|
|
477
|
-
}),
|
|
478
|
-
npx: Object.freeze({
|
|
479
|
-
name: 'none',
|
|
480
|
-
action: 'npx-update',
|
|
481
|
-
instruction: () => 'already fetched fresh on every run — restart to pick up the new version',
|
|
482
|
-
apply: null,
|
|
483
|
-
}),
|
|
484
|
-
source: Object.freeze({
|
|
485
|
-
name: 'git',
|
|
486
|
-
action: 'source-update',
|
|
487
|
-
instruction: () => 'git pull && npm ci (this copy is a source checkout)',
|
|
488
|
-
apply: null,
|
|
489
|
-
}),
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* Which installer owns the copy that is running — so the sentence names a
|
|
494
|
-
* command that actually changes THIS binary.
|
|
495
|
-
*
|
|
496
|
-
* Order matters. An explicit declaration wins (the pip launcher sets
|
|
497
|
-
* `AEGIS_INSTALL_METHOD=pip` before exec'ing the real CLI, which is the only
|
|
498
|
-
* way the npm package can know a Python install is driving it). Then npx, which
|
|
499
|
-
* is detectable from the argv it builds. Then a source checkout, then npm.
|
|
500
|
-
*
|
|
501
|
-
* Pure and injectable: `env`, `argv` and a file probe, no `process` reads, so a
|
|
502
|
-
* test can assert every branch without npm or pip being installed.
|
|
503
|
-
*/
|
|
504
|
-
function detectInstallMethod({
|
|
505
|
-
env = {}, argv = [], pkg = PKG, fileExists = () => false, entry = '',
|
|
506
|
-
} = {}) {
|
|
507
|
-
const declared = String((env && env.AEGIS_INSTALL_METHOD) || '').trim().toLowerCase();
|
|
508
|
-
if (declared && Object.prototype.hasOwnProperty.call(APPLIERS, declared)) return declared;
|
|
509
|
-
|
|
510
|
-
const joined = argv.map((a) => String(a)).join(' ');
|
|
511
|
-
if (/\bnpx\b/.test(joined) || /_npx[\\/]/.test(joined) || /_npx[\\/]/.test(entry)) return 'npx';
|
|
512
|
-
if (/[\\/]pipx[\\/]venvs[\\/]/.test(entry) || /[\\/]site-packages[\\/]/.test(entry)) return 'pip';
|
|
513
|
-
if (/[\\/]node_modules[\\/]/.test(entry)) return 'npm';
|
|
514
|
-
// A git checkout has the repo's own marker files; a global npm install does
|
|
515
|
-
// not (they are not in the package's `files` list).
|
|
516
|
-
if (fileExists('.git') && fileExists('client/update.js')) return 'source';
|
|
517
|
-
return 'npm';
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* The same once-a-day check as `updateNotice`, but *awaited* and detailed.
|
|
522
|
-
*
|
|
523
|
-
* Why both exist: `updateNotice` answers immediately from cache and lets the
|
|
524
|
-
* refresh land later, which is right for a CLI that must never block a launch.
|
|
525
|
-
* A host that is *showing the user a banner about it* — the window, or
|
|
526
|
-
* `aegis doctor` — needs the full result (`.ok`, `.reason`, `.status`) and is
|
|
527
|
-
* willing to wait for it. So this is not a second check: it is the same probe
|
|
528
|
-
* (`fetchLatestDetailed`, same `TIMEOUT_MS`, same `REASONS` taxonomy) and the
|
|
529
|
-
* same `CHECK_INTERVAL_MS` cache rule, extracted so that the desktop applier
|
|
530
|
-
* stops carrying its own copy of the fetching logic (D7: the desktop keeps its
|
|
531
|
-
* applier and loses its version fetch).
|
|
532
|
-
*
|
|
533
|
-
* Never throws: a failure is a result with `ok: false` and a reason.
|
|
534
|
-
*
|
|
535
|
-
* @returns {Promise<{ok: boolean, latest: ?string, behind: boolean, reason: ?string,
|
|
536
|
-
* status: ?number, cached: boolean, checkedAt: ?number}>}
|
|
537
|
-
*/
|
|
538
|
-
async function checkPublished({
|
|
539
|
-
current,
|
|
540
|
-
cache,
|
|
541
|
-
save,
|
|
542
|
-
now = Date.now(),
|
|
543
|
-
fetchImpl = fetchLatestDetailed,
|
|
544
|
-
intervalMs = CHECK_INTERVAL_MS,
|
|
545
|
-
timeoutMs = TIMEOUT_MS,
|
|
546
|
-
pkg = PKG,
|
|
547
|
-
} = {}) {
|
|
548
|
-
const c = cache || null;
|
|
549
|
-
const fresh = Boolean(c) && typeof c.checkedAt === 'number' && now - c.checkedAt < intervalMs;
|
|
550
|
-
if (fresh) {
|
|
551
|
-
const latest = c.latest || null;
|
|
552
|
-
return {
|
|
553
|
-
ok: Boolean(latest),
|
|
554
|
-
latest,
|
|
555
|
-
behind: !!(latest && isNewer(latest, current)),
|
|
556
|
-
reason: latest ? null : c.reason || null,
|
|
557
|
-
status: latest ? 200 : c.status == null ? null : c.status,
|
|
558
|
-
cached: true,
|
|
559
|
-
checkedAt: c.checkedAt,
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
let res;
|
|
564
|
-
try {
|
|
565
|
-
res = await fetchImpl({ pkg, timeoutMs });
|
|
566
|
-
} catch (err) {
|
|
567
|
-
res = { ok: false, latest: null, reason: REASONS.NETWORK, status: null, error: String((err && err.message) || err) };
|
|
568
|
-
}
|
|
569
|
-
const record = {
|
|
570
|
-
checkedAt: now,
|
|
571
|
-
latest: res && res.latest ? res.latest : null,
|
|
572
|
-
reason: (res && res.reason) || null,
|
|
573
|
-
status: res && res.status != null ? res.status : null,
|
|
574
|
-
};
|
|
575
|
-
if (typeof save === 'function') {
|
|
576
|
-
try {
|
|
577
|
-
save(record);
|
|
578
|
-
} catch {
|
|
579
|
-
/* failing to persist a notice is never worth failing the call */
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
return {
|
|
583
|
-
ok: Boolean(res && res.ok && res.latest),
|
|
584
|
-
latest: record.latest,
|
|
585
|
-
behind: !!(record.latest && isNewer(record.latest, current)),
|
|
586
|
-
reason: record.reason,
|
|
587
|
-
status: record.status,
|
|
588
|
-
cached: false,
|
|
589
|
-
checkedAt: now,
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
* One line for the welcome box, or null when there is nothing to say.
|
|
595
|
-
*
|
|
596
|
-
* Rendered from `updatePlan`, not from a second copy of the sentence: the
|
|
597
|
-
* command the terminal tells the user to run and the command the window's
|
|
598
|
-
* banner offers are the same string produced by the same verdict.
|
|
599
|
-
*
|
|
600
|
-
* `method` is the install channel the caller detected (see
|
|
601
|
-
* `detectInstallMethod`). It is threaded through rather than assumed to be npm
|
|
602
|
-
* because the previous version returned `null` — silence — for every channel
|
|
603
|
-
* that was not npm, so a pip install and an npx run were never told a release
|
|
604
|
-
* existed. A host that knows how it was installed should say so.
|
|
605
|
-
*/
|
|
606
|
-
function updateLine({ current, latest, behind, pkg = PKG, method = 'npm' }) {
|
|
607
|
-
const plan = updatePlan({ current, latest, behind, channel: 'npm', pkg, method });
|
|
608
|
-
if (!plan.behind) return null;
|
|
609
|
-
const head = `Update available: ${plan.current} → ${plan.latest}`;
|
|
610
|
-
// `apply` is null for the two channels with nothing to install, whose
|
|
611
|
-
// instruction is already a sentence — prefixing those with "run:" would read
|
|
612
|
-
// as a command that does not exist.
|
|
613
|
-
return plan.apply
|
|
614
|
-
? `${head} run: ${plan.instruction}`
|
|
615
|
-
: `${head} ${plan.instruction}`;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/**
|
|
619
|
-
* The server's verdict, from cache. Never waits on the network.
|
|
620
|
-
*
|
|
621
|
-
* Same contract and the same once-a-day rule as `updateNotice`, for the same
|
|
622
|
-
* reason: the welcome box draws from what the last run learned, and the
|
|
623
|
-
* refresh lands for the next one. A first run shows nothing, which is right —
|
|
624
|
-
* it has no verdict yet.
|
|
625
|
-
*
|
|
626
|
-
* Returns `{ behind, latest, required, downloadUrl }`. `behind` is the server's
|
|
627
|
-
* answer, not a comparison this module invented, so the box says "update" only
|
|
628
|
-
* when the server would actually refuse this client.
|
|
629
|
-
*/
|
|
630
|
-
function serverNotice({
|
|
631
|
-
current,
|
|
632
|
-
cache,
|
|
633
|
-
save,
|
|
634
|
-
now = Date.now(),
|
|
635
|
-
verdictImpl = serverUpdateVerdict,
|
|
636
|
-
intervalMs = CHECK_INTERVAL_MS,
|
|
637
|
-
} = {}) {
|
|
638
|
-
const c = cache || {};
|
|
639
|
-
const fresh = typeof c.checkedAt === 'number' && now - c.checkedAt < intervalMs;
|
|
640
|
-
|
|
641
|
-
if (!fresh) {
|
|
642
|
-
Promise.resolve()
|
|
643
|
-
.then(() => verdictImpl({ current }))
|
|
644
|
-
.then((v) => {
|
|
645
|
-
if (!v || typeof save !== 'function') return;
|
|
646
|
-
// Persist the verdict even when it is "not behind": that is what tells
|
|
647
|
-
// the next run it already asked today, and stops a check per launch.
|
|
648
|
-
save({
|
|
649
|
-
checkedAt: now,
|
|
650
|
-
behind: Boolean(v.behind),
|
|
651
|
-
required: v.required || null,
|
|
652
|
-
latest: v.latest || null,
|
|
653
|
-
downloadUrl: v.downloadUrl || '',
|
|
654
|
-
reason: v.reason || null,
|
|
655
|
-
});
|
|
656
|
-
})
|
|
657
|
-
.catch(() => {});
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
return {
|
|
661
|
-
behind: Boolean(c.behind),
|
|
662
|
-
latest: c.latest || null,
|
|
663
|
-
required: c.required || null,
|
|
664
|
-
downloadUrl: c.downloadUrl || '',
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
/**
|
|
669
|
-
* The line for the welcome box when the SERVER says this build is too old.
|
|
670
|
-
*
|
|
671
|
-
* Distinct from `updateLine` on purpose: "a newer release exists" and "the
|
|
672
|
-
* server will refuse you" are different urgency, and only the second one means
|
|
673
|
-
* the app is about to stop working. Says where to get it, because a user who
|
|
674
|
-
* is being cut off needs a destination, not a version number.
|
|
675
|
-
*/
|
|
676
|
-
function serverUpdateLine({ behind, latest, required, downloadUrl } = {}) {
|
|
677
|
-
if (!behind) return null;
|
|
678
|
-
const target = latest || required || '';
|
|
679
|
-
const head = target
|
|
680
|
-
? `This build is no longer served — update to v${target}.`
|
|
681
|
-
: 'This build is no longer served — update to the latest release.';
|
|
682
|
-
const where = downloadUrl ? ` ${downloadUrl}` : '';
|
|
683
|
-
return `${head}${where}`;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
module.exports = {
|
|
687
|
-
isNewer,
|
|
688
|
-
fetchLatest,
|
|
689
|
-
fetchLatestDetailed,
|
|
690
|
-
fetchServerVersion,
|
|
691
|
-
fetchServerVersionDetailed,
|
|
692
|
-
serverUpdateVerdict,
|
|
693
|
-
serverNotice,
|
|
694
|
-
serverUpdateLine,
|
|
695
|
-
isTransientReason,
|
|
696
|
-
updateNotice,
|
|
697
|
-
checkPublished,
|
|
698
|
-
updatePlan,
|
|
699
|
-
updateLine,
|
|
700
|
-
detectInstallMethod,
|
|
701
|
-
APPLIERS,
|
|
702
|
-
PKG,
|
|
703
|
-
CHECK_INTERVAL_MS,
|
|
704
|
-
REASONS,
|
|
705
|
-
DEFAULT_API_BASE,
|
|
706
|
-
};
|
|
3
|
+
const https=require("node:https"),fs=require("node:fs");let platformHelpers=null;try{platformHelpers=require("./platform.js")}catch{platformHelpers=null}const REGISTRY="https://registry.npmjs.org",PKG="aegiscode",CHECK_INTERVAL_MS=864e5,TIMEOUT_MS=2500;function isNewer(e,t){const n=e=>String(e||"").trim().replace(/^v/,"").split("-")[0].split(".").map((e=>Number.parseInt(e,10)||0)),r=n(e),s=n(t);for(let e=0;e<Math.max(r.length,s.length);e++){const t=r[e]||0,n=s[e]||0;if(t!==n)return t>n}return!1}const REASONS=Object.freeze({TIMEOUT:"timeout",NETWORK:"network",NOT_FOUND:"not-found",FORBIDDEN:"forbidden",HTTP:"http",MALFORMED:"malformed",TOO_LARGE:"too-large"});function isTransientReason(e,t=null){return e===REASONS.TIMEOUT||e===REASONS.NETWORK||e===REASONS.HTTP&&(!t||t>=500||429===t)}function fetchLatestDetailed({pkg:e=PKG,timeoutMs:t=TIMEOUT_MS}={}){return new Promise((n=>{let r=!1;const s=e=>{r||(r=!0,n(e))},l=(e,t=null)=>s({ok:!1,latest:null,reason:e,status:t});try{const n=https.get(`${REGISTRY}/${e}`,{headers:{accept:"application/vnd.npm.install-v1+json"},timeout:t},(e=>{const t=e.statusCode;if(200!==t)return e.resume(),l(404===t?REASONS.NOT_FOUND:401===t||403===t?REASONS.FORBIDDEN:REASONS.HTTP,t);let r="";e.setEncoding("utf8"),e.on("data",(e=>{r+=e,r.length>2e6&&(n.destroy(),l(REASONS.TOO_LARGE,t))})),e.on("end",(()=>{try{const e=JSON.parse(r)["dist-tags"],n=e&&e.latest||null;if(!n)return l(REASONS.MALFORMED,t);(e=>{s({ok:!0,latest:e,reason:null,status:200})})(n)}catch{l(REASONS.MALFORMED,t)}}))}));n.on("timeout",(()=>{n.destroy(),l(REASONS.TIMEOUT)})),n.on("error",(()=>l(REASONS.NETWORK)))}catch{l(REASONS.NETWORK)}}))}function fetchLatest(e={}){return fetchLatestDetailed(e).then((e=>e.ok?e.latest:null))}const DEFAULT_API_BASE="https://aegiscloud.org",VERSION_BODY_CAP=65536;function fetchServerVersionDetailed({base:e=DEFAULT_API_BASE,clientVersion:t=null,timeoutMs:n=TIMEOUT_MS,getImpl:r=null}={}){return new Promise((s=>{let l=!1;const o=e=>{l||(l=!0,s(e))},a=(e,t=null)=>o({ok:!1,minClient:null,latestCli:null,latestDesktop:null,upgradeRequired:null,downloadUrl:"",reason:e,status:t}),i=(e,n)=>{const r=e&&"string"==typeof e.min_client?e.min_client:null;if(!r)return a(REASONS.MALFORMED,n);o({ok:!0,minClient:r,latestCli:"string"==typeof e.latest_cli?e.latest_cli:"",latestDesktop:"string"==typeof e.latest_desktop?e.latest_desktop:"",upgradeRequired:t&&"boolean"==typeof e.upgrade_required?e.upgrade_required:null,downloadUrl:"string"==typeof e.download_url?e.download_url:"",reason:null,status:n})};try{const s=`${String(e||DEFAULT_API_BASE).replace(/\/+$/,"")}/api/v1/version`,o={accept:"application/json"};t&&(o["X-AEGIS-Version"]=String(t));const c=e=>{const t=e.statusCode;if(200!==t)return e.resume(),a(404===t?REASONS.NOT_FOUND:401===t||403===t?REASONS.FORBIDDEN:REASONS.HTTP,t);let n="";e.setEncoding("utf8"),e.on("data",(r=>{n+=r,n.length>VERSION_BODY_CAP&&(e.destroy(),a(REASONS.TOO_LARGE,t))})),e.on("end",(()=>{if(!l)try{i(JSON.parse(n),t)}catch{a(REASONS.MALFORMED,t)}}))};if("function"==typeof r)return void Promise.resolve(r({url:s,headers:o,timeoutMs:n})).then((e=>{const t=e&&null!=e.status?e.status:0;if(!t)return a(REASONS.NETWORK);if(200!==t)return a(404===t?REASONS.NOT_FOUND:401===t||403===t?REASONS.FORBIDDEN:REASONS.HTTP,t);const n=e&&"string"==typeof e.text?e.text:"";if(n.length>VERSION_BODY_CAP)return a(REASONS.TOO_LARGE,t);try{i(JSON.parse(n),t)}catch{a(REASONS.MALFORMED,t)}})).catch((()=>a(REASONS.NETWORK)));const u=require(/^http:\/\//i.test(s)?"node:http":"node:https").get(s,{headers:o,timeout:n},c);u.on("timeout",(()=>{u.destroy(),a(REASONS.TIMEOUT)})),u.on("error",(()=>a(REASONS.NETWORK)))}catch{a(REASONS.NETWORK)}}))}function fetchServerVersion(e={}){return fetchServerVersionDetailed(e).then((e=>e.ok?e.minClient:null))}async function serverUpdateVerdict({current:e,base:t=DEFAULT_API_BASE,timeoutMs:n=TIMEOUT_MS,getImpl:r=null}={}){const s=await fetchServerVersionDetailed({base:t,clientVersion:e,timeoutMs:n,getImpl:r});if(!s.ok)return{ok:!1,behind:!1,latest:null,required:null,downloadUrl:"",reason:s.reason,status:s.status};const l=s.minClient||null,o=!0===s.upgradeRequired||!1!==s.upgradeRequired&&!!l&&isNewer(l,e);return{ok:!0,behind:o,latest:o&&(s.latestCli||s.latestDesktop)||l,latestCli:s.latestCli,latestDesktop:s.latestDesktop,required:l,downloadUrl:s.downloadUrl,reason:null,status:s.status}}function updateNotice({current:e,cache:t,save:n,now:r=Date.now(),fetchImpl:s=fetchLatest,intervalMs:l=864e5}={}){const o=t||{};"number"==typeof o.checkedAt&&r-o.checkedAt<l||Promise.resolve(s()).then((e=>{e&&"function"==typeof n&&n({checkedAt:r,latest:e})})).catch((()=>{}));const a=o.latest||null;return{latest:a,behind:!(!a||!isNewer(a,e))}}function installSpawn(e,t={}){if(!e||"string"!=typeof e.command||!e.command)return null;const n=t&&"object"==typeof t?t:{},r="string"==typeof n.platform&&n.platform?n.platform:process.platform,s=n.env&&"object"==typeof n.env?n.env:process.env,l=void 0===n.helpers?platformHelpers:n.helpers,o=Array.isArray(e.args)?e.args.map(String):[],a=[e.command,...o].join(" ");if("win32"===r){const e=l&&"function"==typeof l.shellSpec?l.shellSpec({platform:r,env:s}):{cmd:"cmd.exe",arg:"/d /s /c"};return{command:e.cmd,args:[...String(e.arg).split(" "),a],shell:!1,script:a}}let i=e.command;if(l&&"function"==typeof l.resolveExecutable)try{const t=l.resolveExecutable(e.command,{platform:r,env:s,..."function"==typeof n.exists?{exists:n.exists}:{}});t&&(i=t)}catch{}return{command:i,args:o.slice(),shell:!1,script:a}}function updatePlan({current:e,latest:t,behind:n,channel:r="npm",pkg:s=PKG,method:l="npm"}={}){const o=String(null==e?"":e),a=t?String(t):null;if(!Boolean(n)||!Boolean(a))return{channel:r,method:l,current:o,latest:a,behind:!1,action:"none",applier:"none",instruction:null,apply:null};const i=APPLIERS[l]||APPLIERS.npm;return{channel:r,method:l,current:o,latest:a,behind:!0,action:i.action,applier:i.name,instruction:i.instruction(s),apply:i.apply?i.apply(s):null}}const APPLIERS=Object.freeze({npm:Object.freeze({name:"npm",action:"npm-update",instruction:e=>`npm i -g ${e}@latest`,apply:e=>({command:"npm",args:["i","-g",`${e}@latest`]})}),pip:Object.freeze({name:"pip",action:"pip-update",instruction:e=>`pip install -U ${e}`,apply:e=>({command:"win32"===process.platform?"python":"python3",args:["-m","pip","install","-U",e]})}),npx:Object.freeze({name:"none",action:"npx-update",instruction:()=>"already fetched fresh on every run — restart to pick up the new version",apply:null}),source:Object.freeze({name:"git",action:"source-update",instruction:()=>"git pull && npm ci (this copy is a source checkout)",apply:null})});function detectInstallMethod({env:e={},argv:t=[],pkg:n=PKG,fileExists:r=(()=>!1),entry:s=""}={}){const l=String(e&&e.AEGIS_INSTALL_METHOD||"").trim().toLowerCase();if(l&&Object.prototype.hasOwnProperty.call(APPLIERS,l))return l;const o=t.map((e=>String(e))).join(" ");return/\bnpx\b/.test(o)||/_npx[\\/]/.test(o)||/_npx[\\/]/.test(s)?"npx":/[\\/]pipx[\\/]venvs[\\/]/.test(s)||/[\\/]site-packages[\\/]/.test(s)?"pip":/[\\/]node_modules[\\/]/.test(s)?"npm":r(".git")&&r("client/update.js")?"source":"npm"}async function checkPublished({current:e,cache:t,save:n,now:r=Date.now(),fetchImpl:s=fetchLatestDetailed,intervalMs:l=864e5,timeoutMs:o=TIMEOUT_MS,pkg:a=PKG}={}){const i=t||null;if(Boolean(i)&&"number"==typeof i.checkedAt&&r-i.checkedAt<l){const t=i.latest||null;return{ok:Boolean(t),latest:t,behind:!(!t||!isNewer(t,e)),reason:t?null:i.reason||null,status:t?200:null==i.status?null:i.status,cached:!0,checkedAt:i.checkedAt}}let c;try{c=await s({pkg:a,timeoutMs:o})}catch(e){c={ok:!1,latest:null,reason:REASONS.NETWORK,status:null,error:String(e&&e.message||e)}}const u={checkedAt:r,latest:c&&c.latest?c.latest:null,reason:c&&c.reason||null,status:c&&null!=c.status?c.status:null};if("function"==typeof n)try{n(u)}catch{}return{ok:Boolean(c&&c.ok&&c.latest),latest:u.latest,behind:!(!u.latest||!isNewer(u.latest,e)),reason:u.reason,status:u.status,cached:!1,checkedAt:r}}function updateLine({current:e,latest:t,behind:n,pkg:r=PKG,method:s="npm"}){const l=updatePlan({current:e,latest:t,behind:n,channel:"npm",pkg:r,method:s});if(!l.behind)return null;const o=`Update available: ${l.current} → ${l.latest}`;return l.apply?`${o} run: ${l.instruction}`:`${o} ${l.instruction}`}function serverNotice({current:e,cache:t,save:n,now:r=Date.now(),verdictImpl:s=serverUpdateVerdict,intervalMs:l=864e5}={}){const o=t||{};return"number"==typeof o.checkedAt&&r-o.checkedAt<l||Promise.resolve().then((()=>s({current:e}))).then((e=>{e&&"function"==typeof n&&n({checkedAt:r,behind:Boolean(e.behind),required:e.required||null,latest:e.latest||null,downloadUrl:e.downloadUrl||"",reason:e.reason||null})})).catch((()=>{})),{behind:Boolean(o.behind),latest:o.latest||null,required:o.required||null,downloadUrl:o.downloadUrl||""}}function serverUpdateLine({behind:e,latest:t,required:n,downloadUrl:r}={}){if(!e)return null;const s=t||n||"";return`${s?`This build is no longer served — update to v${s}.`:"This build is no longer served — update to the latest release."}${r?` ${r}`:""}`}module.exports={isNewer:isNewer,fetchLatest:fetchLatest,fetchLatestDetailed:fetchLatestDetailed,fetchServerVersion:fetchServerVersion,fetchServerVersionDetailed:fetchServerVersionDetailed,serverUpdateVerdict:serverUpdateVerdict,serverNotice:serverNotice,serverUpdateLine:serverUpdateLine,isTransientReason:isTransientReason,updateNotice:updateNotice,checkPublished:checkPublished,updatePlan:updatePlan,installSpawn:installSpawn,updateLine:updateLine,detectInstallMethod:detectInstallMethod,APPLIERS:APPLIERS,PKG:PKG,CHECK_INTERVAL_MS:864e5,REASONS:REASONS,DEFAULT_API_BASE:DEFAULT_API_BASE};
|