@chatcode/cco-market 1.45.1
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/LICENSE +21 -0
- package/README.md +146 -0
- package/README.zh.md +143 -0
- package/UPDATE-API-V1.md +132 -0
- package/client/client.js +11534 -0
- package/cordis.patch.yml +5 -0
- package/lib/accelerate.js +184 -0
- package/lib/agents.js +36 -0
- package/lib/backup.js +572 -0
- package/lib/catalog-local-match.js +126 -0
- package/lib/catalog-npm.js +111 -0
- package/lib/changelog.js +242 -0
- package/lib/channels.js +64 -0
- package/lib/check.js +1067 -0
- package/lib/compatibility.js +198 -0
- package/lib/diagnostics.js +49 -0
- package/lib/discovery-compatibility.js +260 -0
- package/lib/dsh-cli.js +949 -0
- package/lib/dsh-install.js +108 -0
- package/lib/gist.js +352 -0
- package/lib/groups.js +97 -0
- package/lib/home-paths.js +42 -0
- package/lib/hot.js +519 -0
- package/lib/http.js +39 -0
- package/lib/index.js +97 -0
- package/lib/install.js +387 -0
- package/lib/log.js +197 -0
- package/lib/ndjson.js +154 -0
- package/lib/net.js +98 -0
- package/lib/order.js +284 -0
- package/lib/patch.js +521 -0
- package/lib/pnpm-compat.js +490 -0
- package/lib/presets.js +278 -0
- package/lib/profile.js +911 -0
- package/lib/region-probe.js +93 -0
- package/lib/regions.js +253 -0
- package/lib/registry.js +204 -0
- package/lib/restart.js +373 -0
- package/lib/routes.js +4372 -0
- package/lib/settings.js +109 -0
- package/lib/skill-market/api.js +119 -0
- package/lib/skill-market/config.js +29 -0
- package/lib/skill-market/installer.js +352 -0
- package/lib/skill-market/runtime.js +13 -0
- package/lib/skill-market/tui.js +119 -0
- package/lib/skill-market/types.js +1 -0
- package/lib/skill-market/web-routes.js +123 -0
- package/lib/snapshot.js +500 -0
- package/lib/source-migration.js +54 -0
- package/lib/sources.js +596 -0
- package/lib/store.js +91 -0
- package/lib/themes.js +102 -0
- package/lib/trial.js +116 -0
- package/lib/types/accelerate.d.ts +85 -0
- package/lib/types/agents.d.ts +22 -0
- package/lib/types/backup.d.ts +131 -0
- package/lib/types/catalog-local-match.d.ts +50 -0
- package/lib/types/catalog-npm.d.ts +47 -0
- package/lib/types/changelog.d.ts +102 -0
- package/lib/types/channels.d.ts +56 -0
- package/lib/types/check.d.ts +266 -0
- package/lib/types/compatibility.d.ts +64 -0
- package/lib/types/diagnostics.d.ts +29 -0
- package/lib/types/discovery-compatibility.d.ts +80 -0
- package/lib/types/dsh-cli.d.ts +324 -0
- package/lib/types/dsh-install.d.ts +46 -0
- package/lib/types/gist.d.ts +53 -0
- package/lib/types/groups.d.ts +34 -0
- package/lib/types/home-paths.d.ts +16 -0
- package/lib/types/hot.d.ts +221 -0
- package/lib/types/http.d.ts +12 -0
- package/lib/types/index.d.ts +14 -0
- package/lib/types/install.d.ts +172 -0
- package/lib/types/log.d.ts +43 -0
- package/lib/types/ndjson.d.ts +52 -0
- package/lib/types/net.d.ts +58 -0
- package/lib/types/order.d.ts +100 -0
- package/lib/types/patch.d.ts +129 -0
- package/lib/types/pnpm-compat.d.ts +93 -0
- package/lib/types/presets.d.ts +90 -0
- package/lib/types/profile.d.ts +253 -0
- package/lib/types/region-probe.d.ts +50 -0
- package/lib/types/regions.d.ts +122 -0
- package/lib/types/registry.d.ts +86 -0
- package/lib/types/restart.d.ts +194 -0
- package/lib/types/routes.d.ts +67 -0
- package/lib/types/settings.d.ts +78 -0
- package/lib/types/skill-market/api.d.ts +10 -0
- package/lib/types/skill-market/config.d.ts +3 -0
- package/lib/types/skill-market/installer.d.ts +23 -0
- package/lib/types/skill-market/runtime.d.ts +10 -0
- package/lib/types/skill-market/tui.d.ts +4 -0
- package/lib/types/skill-market/types.d.ts +71 -0
- package/lib/types/skill-market/web-routes.d.ts +19 -0
- package/lib/types/snapshot.d.ts +93 -0
- package/lib/types/source-migration.d.ts +11 -0
- package/lib/types/sources.d.ts +216 -0
- package/lib/types/store.d.ts +30 -0
- package/lib/types/themes.d.ts +40 -0
- package/lib/types/trial.d.ts +61 -0
- package/lib/types/update-api-v1.d.ts +66 -0
- package/lib/types/updates.d.ts +123 -0
- package/lib/types/verify.d.ts +139 -0
- package/lib/update-api-v1.js +215 -0
- package/lib/updates.js +361 -0
- package/lib/verify.js +453 -0
- package/package.json +121 -0
- package/src/accelerate.ts +213 -0
- package/src/agents.ts +43 -0
- package/src/backup.ts +583 -0
- package/src/catalog-local-match.ts +144 -0
- package/src/catalog-npm.ts +120 -0
- package/src/changelog.ts +282 -0
- package/src/channels.ts +70 -0
- package/src/check.ts +1239 -0
- package/src/client/CommentsModal.tsx +119 -0
- package/src/client/Diagnostics.tsx +907 -0
- package/src/client/ErrorBoundary.tsx +111 -0
- package/src/client/InstallToast.tsx +31 -0
- package/src/client/Market.module.css +830 -0
- package/src/client/MarketSection.tsx +5301 -0
- package/src/client/OperationsPanel.tsx +365 -0
- package/src/client/SettingsCard.tsx +621 -0
- package/src/client/SkillMarket.module.css +21 -0
- package/src/client/SkillMarketSection.tsx +162 -0
- package/src/client/comments.ts +54 -0
- package/src/client/globals.d.ts +13 -0
- package/src/client/index.ts +180 -0
- package/src/client/locales.ts +1112 -0
- package/src/client/market-data.ts +1321 -0
- package/src/client/operations.ts +201 -0
- package/src/client/preset-panel.tsx +263 -0
- package/src/client/primitives.d.ts +140 -0
- package/src/client/self-check.ts +147 -0
- package/src/client/snapshot-panel.tsx +244 -0
- package/src/compatibility.ts +237 -0
- package/src/diagnostics.ts +84 -0
- package/src/discovery-compatibility.ts +315 -0
- package/src/dsh-cli.ts +1126 -0
- package/src/dsh-install.ts +118 -0
- package/src/gist.ts +362 -0
- package/src/groups.ts +111 -0
- package/src/home-paths.ts +53 -0
- package/src/hot.ts +628 -0
- package/src/http.ts +41 -0
- package/src/index.ts +128 -0
- package/src/install.ts +420 -0
- package/src/log.ts +206 -0
- package/src/ndjson.ts +185 -0
- package/src/net.ts +106 -0
- package/src/order.ts +303 -0
- package/src/patch.ts +522 -0
- package/src/pnpm-compat.ts +527 -0
- package/src/presets.ts +344 -0
- package/src/profile.ts +940 -0
- package/src/region-probe.ts +97 -0
- package/src/regions.ts +310 -0
- package/src/registry.ts +250 -0
- package/src/restart.ts +396 -0
- package/src/routes.ts +4506 -0
- package/src/settings.ts +141 -0
- package/src/skill-market/api.ts +130 -0
- package/src/skill-market/config.ts +32 -0
- package/src/skill-market/installer.ts +337 -0
- package/src/skill-market/runtime.ts +14 -0
- package/src/skill-market/tui.ts +157 -0
- package/src/skill-market/types.ts +80 -0
- package/src/skill-market/web-routes.ts +132 -0
- package/src/snapshot.ts +532 -0
- package/src/source-migration.ts +61 -0
- package/src/sources.ts +565 -0
- package/src/store.ts +89 -0
- package/src/themes.ts +125 -0
- package/src/trial.ts +156 -0
- package/src/update-api-v1.ts +277 -0
- package/src/updates.ts +400 -0
- package/src/verify.ts +492 -0
package/lib/updates.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update detection: per-plugin comparison of what the profile has against
|
|
3
|
+
* the source of truth — git HEAD for github installs, the npm latest
|
|
4
|
+
* dist-tag for registry installs — with a TTL cache.
|
|
5
|
+
*/
|
|
6
|
+
import { DIST_TAG } from './channels.js';
|
|
7
|
+
import { resolveHeadCommit } from './accelerate.js';
|
|
8
|
+
import { marketFetch } from './net.js';
|
|
9
|
+
import { activeRegion } from './regions.js';
|
|
10
|
+
import { profileDir, readGitResolutionCommit, readInstalled, readInstalledVersion, readLockCommits } from './profile.js';
|
|
11
|
+
import { gitCommitOfTarget, gitUploadPackUrl, githubCommitOfTarget, githubRefOfTarget, isGitHostedSpec, repoOfTarget } from './sources.js';
|
|
12
|
+
const UPDATES_TTL_MS = 30 * 60 * 1000;
|
|
13
|
+
const GIT_REMOTE_HEAD_TIMEOUT_MS = 6000;
|
|
14
|
+
let updatesCache = null;
|
|
15
|
+
/**
|
|
16
|
+
* HEAD for an arbitrary smart-HTTP git remote (Gitea, GitLab, …). Same
|
|
17
|
+
* advertisement format as GitHub's info/refs; kept here so accelerate.ts
|
|
18
|
+
* does not need to import sources.ts.
|
|
19
|
+
*/
|
|
20
|
+
async function resolveGitRemoteHead(spec, ref) {
|
|
21
|
+
const url = gitUploadPackUrl(spec);
|
|
22
|
+
if (url === null)
|
|
23
|
+
return null;
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const timer = setTimeout(() => controller.abort(), GIT_REMOTE_HEAD_TIMEOUT_MS);
|
|
26
|
+
try {
|
|
27
|
+
const res = await marketFetch(url, {
|
|
28
|
+
signal: controller.signal,
|
|
29
|
+
headers: { 'user-agent': 'git/2.40.0' },
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok)
|
|
32
|
+
return null;
|
|
33
|
+
const body = await res.text();
|
|
34
|
+
const contentType = res.headers?.get?.('content-type')?.toLowerCase() ?? '';
|
|
35
|
+
if (contentType.includes('text/html')
|
|
36
|
+
|| !body.includes('# service=git-upload-pack')
|
|
37
|
+
|| !/[0-9a-f]{40} (?:HEAD|refs\/(?:heads|tags)\/)/u.test(body)) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (ref === undefined) {
|
|
41
|
+
const found = /([0-9a-f]{40}) HEAD/.exec(body);
|
|
42
|
+
return found === null ? null : found[1];
|
|
43
|
+
}
|
|
44
|
+
const quoted = ref.replace(/[.*+?^${}()|[\]\\]/gu, String.raw `\$&`);
|
|
45
|
+
for (const namespace of ['heads', 'tags']) {
|
|
46
|
+
const found = new RegExp(String.raw `([0-9a-f]{40}) refs/${namespace}/${quoted}(?![^\s])`, 'u').exec(body);
|
|
47
|
+
if (found !== null)
|
|
48
|
+
return found[1];
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
60
|
+
function parseSemver(v) {
|
|
61
|
+
const m = SEMVER.exec(v.trim());
|
|
62
|
+
if (m === null)
|
|
63
|
+
return null;
|
|
64
|
+
return { core: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] === undefined ? [] : m[4].split('.') };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Semver precedence: negative / 0 / positive like a comparator, or null when
|
|
68
|
+
* either side isn't a plain semver version. Build metadata is ignored, a
|
|
69
|
+
* release outranks any prerelease of the same core, and prerelease
|
|
70
|
+
* identifiers compare numerically when both are numeric (so `rc.10` > `rc.9`).
|
|
71
|
+
*/
|
|
72
|
+
export function compareVersions(a, b) {
|
|
73
|
+
const pa = parseSemver(a);
|
|
74
|
+
const pb = parseSemver(b);
|
|
75
|
+
if (pa === null || pb === null)
|
|
76
|
+
return null;
|
|
77
|
+
for (let i = 0; i < 3; i++) {
|
|
78
|
+
if (pa.core[i] !== pb.core[i])
|
|
79
|
+
return pa.core[i] - pb.core[i];
|
|
80
|
+
}
|
|
81
|
+
if (pa.pre.length === 0 || pb.pre.length === 0)
|
|
82
|
+
return pb.pre.length - pa.pre.length;
|
|
83
|
+
for (let i = 0; i < Math.max(pa.pre.length, pb.pre.length); i++) {
|
|
84
|
+
const x = pa.pre[i];
|
|
85
|
+
const y = pb.pre[i];
|
|
86
|
+
if (x === undefined)
|
|
87
|
+
return -1;
|
|
88
|
+
if (y === undefined)
|
|
89
|
+
return 1;
|
|
90
|
+
if (x === y)
|
|
91
|
+
continue;
|
|
92
|
+
const nx = /^\d+$/.test(x);
|
|
93
|
+
const ny = /^\d+$/.test(y);
|
|
94
|
+
if (nx && ny)
|
|
95
|
+
return Number(x) - Number(y);
|
|
96
|
+
if (nx !== ny)
|
|
97
|
+
return nx ? -1 : 1;
|
|
98
|
+
return x < y ? -1 : 1;
|
|
99
|
+
}
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* True only when the registry's `latest` is semantically HIGHER than what the
|
|
104
|
+
* profile has (#64 by @ZeroOrigin64). A plain `!==` also fires when a
|
|
105
|
+
* package's `latest` dist-tag is left pointing at an OLDER release than the
|
|
106
|
+
* pinned install — clicking "update" then rewrote the exact pin to `@latest`
|
|
107
|
+
* and downgraded the profile until it no longer booted.
|
|
108
|
+
*
|
|
109
|
+
* Undecidable inputs (missing or non-semver versions) report no update:
|
|
110
|
+
* without a direction we cannot promise the "update" isn't a downgrade.
|
|
111
|
+
*/
|
|
112
|
+
export function isUpgrade(installed, latest) {
|
|
113
|
+
if (installed === null || latest === null)
|
|
114
|
+
return false;
|
|
115
|
+
const cmp = compareVersions(latest, installed);
|
|
116
|
+
return cmp !== null && cmp > 0;
|
|
117
|
+
}
|
|
118
|
+
/** Drop the cached listing (after a successful install/update/uninstall). */
|
|
119
|
+
export function invalidateUpdates() {
|
|
120
|
+
updatesCache = null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The npm registry update checks read, no trailing slash.
|
|
124
|
+
*
|
|
125
|
+
* Module state driven from the routes, like `updatesCache` beside it, rather
|
|
126
|
+
* than a parameter on all five call sites: the registry is a property of the
|
|
127
|
+
* running market, not of any one question asked of it, and threading it
|
|
128
|
+
* through would put the same value in five signatures and every test that
|
|
129
|
+
* calls them.
|
|
130
|
+
*/
|
|
131
|
+
let registryBase = 'https://registry.npmjs.org';
|
|
132
|
+
/**
|
|
133
|
+
* Point update checks at a registry. Called when the download region
|
|
134
|
+
* resolves and whenever it changes.
|
|
135
|
+
*
|
|
136
|
+
* Dropping the cache is the load-bearing half. A mirror can lag the official
|
|
137
|
+
* registry by minutes, so answers gathered from one are not answers from the
|
|
138
|
+
* other — keeping them across a switch would report a version this registry
|
|
139
|
+
* cannot yet serve.
|
|
140
|
+
*/
|
|
141
|
+
export function setUpdateRegistry(base) {
|
|
142
|
+
const next = base.replace(/\/+$/, '');
|
|
143
|
+
if (next === registryBase)
|
|
144
|
+
return;
|
|
145
|
+
registryBase = next;
|
|
146
|
+
updatesCache = null;
|
|
147
|
+
}
|
|
148
|
+
/** A registry URL for `path`, on whichever registry is currently in force. */
|
|
149
|
+
function npmUrl(path) {
|
|
150
|
+
return `${registryBase}/${path}`;
|
|
151
|
+
}
|
|
152
|
+
async function fetchJson(url) {
|
|
153
|
+
// Through the proxy when one is configured: Node's global fetch ignores
|
|
154
|
+
// HTTP_PROXY, so on a machine whose route out is a local proxy every
|
|
155
|
+
// update check silently took the slow path — or none at all.
|
|
156
|
+
const res = await marketFetch(url, {
|
|
157
|
+
headers: { accept: 'application/json', 'user-agent': 'ChatCode-CLI-Market' },
|
|
158
|
+
signal: AbortSignal.timeout(10_000),
|
|
159
|
+
});
|
|
160
|
+
if (!res.ok)
|
|
161
|
+
throw new Error(`HTTP ${res.status}`);
|
|
162
|
+
return res.json();
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Evidence check behind the "wait a day" stale diagnosis (#45): whether the
|
|
166
|
+
* package's CURRENT latest release was published recently enough to sit
|
|
167
|
+
* inside pnpm's default fresh-release window. pnpm's silent hold leaves no
|
|
168
|
+
* trace in its output, so the publish time is the only verifiable signal.
|
|
169
|
+
* @returns true/false when the npm time metadata answers, null when it
|
|
170
|
+
* can't be determined (offline, unpublished, non-npm) — callers must NOT
|
|
171
|
+
* claim the safety wait on null.
|
|
172
|
+
*/
|
|
173
|
+
export async function latestPublishedRecently(name, windowMs = 26 * 60 * 60 * 1000) {
|
|
174
|
+
try {
|
|
175
|
+
const doc = (await fetchJson(npmUrl(`${encodeURIComponent(name)}`)));
|
|
176
|
+
const latest = doc['dist-tags']?.latest;
|
|
177
|
+
const published = latest !== undefined ? doc.time?.[latest] : undefined;
|
|
178
|
+
if (published === undefined)
|
|
179
|
+
return null;
|
|
180
|
+
const age = Date.now() - Date.parse(published);
|
|
181
|
+
return Number.isFinite(age) ? age < windowMs : null;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** The registry's current `latest` version for a package, or null when it can't be read. */
|
|
188
|
+
/**
|
|
189
|
+
* The version a channel subscriber should be offered: the newest build in
|
|
190
|
+
* the set that channel is willing to receive.
|
|
191
|
+
*
|
|
192
|
+
* A channel is a SET, not a tag. Someone on beta has not stopped accepting
|
|
193
|
+
* releases — they accept releases and prereleases — so beta means
|
|
194
|
+
* {latest, beta} and dev means {latest, beta, dev}. Reading it as one tag
|
|
195
|
+
* gets a real case wrong: once 1.14.0 ships, `beta` still points at
|
|
196
|
+
* 1.14.0-beta.1 until the next prerelease is cut, and following that tag
|
|
197
|
+
* literally would walk a subscriber BACKWARDS onto a build their channel
|
|
198
|
+
* has already moved past.
|
|
199
|
+
*
|
|
200
|
+
* The nesting is also what makes a channel leavable. Going backwards is
|
|
201
|
+
* only ever offered when the user narrows the set — picking stable while a
|
|
202
|
+
* prerelease is installed drops `beta` out of it, so the answer becomes
|
|
203
|
+
* `latest` and the market can finally offer the way back. That case used to
|
|
204
|
+
* be unreachable: comparing 1.13.1 against an installed 1.14.0-beta.1 found
|
|
205
|
+
* nothing newer and answered "up to date", so the control the user had just
|
|
206
|
+
* used appeared to do nothing.
|
|
207
|
+
*
|
|
208
|
+
* @param stable - the `latest` version, already fetched by the caller.
|
|
209
|
+
*/
|
|
210
|
+
export async function versionOnChannel(name, channel, stable) {
|
|
211
|
+
let best = stable;
|
|
212
|
+
for (const tag of EXTRA_TAGS[channel]) {
|
|
213
|
+
const candidate = await tagVersion(name, tag);
|
|
214
|
+
if (candidate !== null && (best === null || isUpgrade(best, candidate)))
|
|
215
|
+
best = candidate;
|
|
216
|
+
}
|
|
217
|
+
return best;
|
|
218
|
+
}
|
|
219
|
+
/** Tags a channel adds on top of `latest`, widest channel last. */
|
|
220
|
+
const EXTRA_TAGS = {
|
|
221
|
+
stable: [],
|
|
222
|
+
beta: [DIST_TAG.beta],
|
|
223
|
+
dev: [DIST_TAG.beta, DIST_TAG.dev],
|
|
224
|
+
};
|
|
225
|
+
/** One dist-tag's version, or null when it isn't published or can't be read. */
|
|
226
|
+
async function tagVersion(name, tag) {
|
|
227
|
+
try {
|
|
228
|
+
const meta = (await fetchJson(npmUrl(`${encodeURIComponent(name)}/${tag}`)));
|
|
229
|
+
return typeof meta.version === 'string' ? meta.version : null;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// An unpublished tag is the ordinary case for a channel nobody has cut
|
|
233
|
+
// a build on yet, and a registry hiccup must not take the whole update
|
|
234
|
+
// check down with it.
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
export async function fetchNpmLatest(name) {
|
|
239
|
+
try {
|
|
240
|
+
const meta = (await fetchJson(npmUrl(`${encodeURIComponent(name)}/latest`)));
|
|
241
|
+
return typeof meta.version === 'string' ? meta.version : null;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** Per-plugin update checks; a failed check reports no update rather than failing the listing. */
|
|
248
|
+
export async function checkUpdates(profile, force = false, explicitDir,
|
|
249
|
+
/**
|
|
250
|
+
* Packages that follow a release channel instead of plain `latest`. Only
|
|
251
|
+
* ever the market itself: opting into early builds is volunteering to try
|
|
252
|
+
* THIS plugin early, not a licence to pull every other author's
|
|
253
|
+
* unreleased work.
|
|
254
|
+
*/
|
|
255
|
+
channelFor = new Map(),
|
|
256
|
+
/**
|
|
257
|
+
* Curated npm sources for `file:` installs that were matched to the market
|
|
258
|
+
* catalog. `link:` workspaces remain development sources and are never
|
|
259
|
+
* opted into online updates.
|
|
260
|
+
*/
|
|
261
|
+
onlineSourceFor = new Map()) {
|
|
262
|
+
const activeProfileDir = profileDir(profile, explicitDir);
|
|
263
|
+
// The channel is part of the key: switching to betas has to change the
|
|
264
|
+
// answer immediately, and a cache keyed on the profile alone would serve
|
|
265
|
+
// the stable verdict for the rest of the TTL — reading as "the setting did
|
|
266
|
+
// nothing".
|
|
267
|
+
const cacheKey = `${activeProfileDir}\u0000${[...channelFor].map(([n, c]) => `${n}:${c}`).sort().join(',')}\u0000${[...onlineSourceFor].map(([n, s]) => `${n}:${s}`).sort().join(',')}`;
|
|
268
|
+
if (!force && updatesCache?.key === cacheKey && Date.now() - updatesCache.at < UPDATES_TTL_MS) {
|
|
269
|
+
return updatesCache.data;
|
|
270
|
+
}
|
|
271
|
+
const installed = readInstalled(profile, activeProfileDir);
|
|
272
|
+
const lockCommits = readLockCommits(profile, activeProfileDir);
|
|
273
|
+
const result = {};
|
|
274
|
+
await Promise.all(Object.entries(installed).map(async ([name, spec]) => {
|
|
275
|
+
const version = readInstalledVersion(profile, name, activeProfileDir);
|
|
276
|
+
const normalizedSpec = spec.toLowerCase();
|
|
277
|
+
if (normalizedSpec.startsWith('file:')) {
|
|
278
|
+
const onlineSource = onlineSourceFor.get(name);
|
|
279
|
+
if (onlineSource !== undefined) {
|
|
280
|
+
const latest = await fetchNpmLatest(onlineSource);
|
|
281
|
+
const updateAvailable = isUpgrade(version, latest);
|
|
282
|
+
result[name] = {
|
|
283
|
+
kind: 'linked', version, current: version, latest, updateAvailable,
|
|
284
|
+
...(updateAvailable ? { restoreRequired: true } : {}),
|
|
285
|
+
};
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
result[name] = { kind: 'linked', version, current: null, latest: null, updateAvailable: false };
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (normalizedSpec.startsWith('link:')) {
|
|
292
|
+
result[name] = { kind: 'linked', version, current: null, latest: null, updateAvailable: false };
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// The repo behind the spec, in every supported spelling. Older regional
|
|
296
|
+
// installs can carry a proxied codeload URL rather than a `github:`
|
|
297
|
+
// shortcut, and asking only about the shortcut sent those through the npm
|
|
298
|
+
// branch below — where a GitHub-only plugin
|
|
299
|
+
// either 404s or, far worse, matches an unrelated package that happens
|
|
300
|
+
// to share its name.
|
|
301
|
+
const repo = repoOfTarget(spec)?.split('#')[0] ?? null;
|
|
302
|
+
try {
|
|
303
|
+
if (repo !== null) {
|
|
304
|
+
// A proxied legacy URL or exact `github:#sha` carries its pin in the
|
|
305
|
+
// spec; a mutable `github:` shortcut keeps it in the lockfile. Prefer
|
|
306
|
+
// the spec because it is authoritative even if the lockfile is stale.
|
|
307
|
+
const current = githubCommitOfTarget(spec) ?? lockCommits.get(repo) ?? null;
|
|
308
|
+
// git's own ref advertisement, NOT api.github.com/repos/…/commits.
|
|
309
|
+
// The REST API allows 60 requests an hour per IP unauthenticated,
|
|
310
|
+
// shared across every plugin AND every check — a handful of
|
|
311
|
+
// github-installed plugins exhausts it and the whole list silently
|
|
312
|
+
// stops reporting updates (#349). The ref endpoint git itself uses
|
|
313
|
+
// has no such quota; this is the same call `acceleratedTarget`
|
|
314
|
+
// already makes to resolve a commit for the China region.
|
|
315
|
+
// Ask about the ref the install actually names. Answering with the
|
|
316
|
+
// default branch for a `#branch` install compares two lines that
|
|
317
|
+
// never converge, so the row reported an update forever (#446).
|
|
318
|
+
const latest = await resolveHeadCommit(repo, activeRegion(), process.env, githubRefOfTarget(spec) ?? undefined);
|
|
319
|
+
result[name] = {
|
|
320
|
+
kind: 'github', version, current, latest,
|
|
321
|
+
updateAvailable: current !== null && latest !== null && current !== latest,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
else if (isGitHostedSpec(spec)) {
|
|
325
|
+
// Private / self-hosted git (Gitea, GitLab, raw git+https). Never ask
|
|
326
|
+
// npm by package name — a colliding registry package would be offered
|
|
327
|
+
// as an update and then installed over the git source (#525).
|
|
328
|
+
// Reuse kind `github` as "git-sourced" so the existing Installed UI
|
|
329
|
+
// keeps treating the row as a VCS install without a client change.
|
|
330
|
+
const current = gitCommitOfTarget(spec)
|
|
331
|
+
?? readGitResolutionCommit(profile, spec, activeProfileDir);
|
|
332
|
+
const latest = await resolveGitRemoteHead(spec);
|
|
333
|
+
result[name] = {
|
|
334
|
+
kind: 'github', version, current, latest,
|
|
335
|
+
updateAvailable: current !== null && latest !== null && current !== latest,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
const meta = (await fetchJson(npmUrl(`${encodeURIComponent(name)}/latest`)));
|
|
340
|
+
const stable = typeof meta.version === 'string' ? meta.version : null;
|
|
341
|
+
const channel = channelFor.get(name);
|
|
342
|
+
const latest = channel === undefined ? stable : await versionOnChannel(name, channel, stable);
|
|
343
|
+
// Forwards is an update; a difference in the other direction is a
|
|
344
|
+
// channel switch and is reported as one, under its own field.
|
|
345
|
+
const upgrade = isUpgrade(version, latest);
|
|
346
|
+
const sideways = channel !== undefined && !upgrade
|
|
347
|
+
&& version !== null && latest !== null && version !== latest;
|
|
348
|
+
result[name] = {
|
|
349
|
+
kind: 'npm', version, current: version, latest,
|
|
350
|
+
updateAvailable: upgrade,
|
|
351
|
+
...(sideways ? { channelSwitch: latest } : {}),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
result[name] = { kind: (spec.startsWith('github:') || isGitHostedSpec(spec)) ? 'github' : 'npm', version, current: null, latest: null, updateAvailable: false };
|
|
357
|
+
}
|
|
358
|
+
}));
|
|
359
|
+
updatesCache = { key: cacheKey, at: Date.now(), data: result };
|
|
360
|
+
return result;
|
|
361
|
+
}
|