dsh-plugin-syncup 0.0.0-stage → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +183 -0
- package/README.md +362 -2
- package/client.js +1506 -0
- package/cordis.patch.yml +6 -0
- package/icon.svg +26 -0
- package/index.js +354 -0
- package/lib/config.js +330 -0
- package/lib/errors.js +81 -0
- package/lib/github.js +305 -0
- package/lib/http.js +285 -0
- package/lib/jobs.js +107 -0
- package/lib/manifest.js +272 -0
- package/lib/manual.js +85 -0
- package/lib/preview.js +217 -0
- package/lib/pushplan.js +200 -0
- package/lib/spec.js +127 -0
- package/lib/state.js +110 -0
- package/lib/sync.js +1636 -0
- package/lib/versions.js +447 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +52 -3
package/lib/versions.js
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { isRecord, parseVersion, compareVersions } from './spec.js'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_NPM = 'https://registry.npmjs.org'
|
|
4
|
+
const GITHUB_API = 'https://api.github.com'
|
|
5
|
+
const MAX_CHECK = 40
|
|
6
|
+
|
|
7
|
+
function timeoutSignal(ms, upstream) {
|
|
8
|
+
const controller = new AbortController()
|
|
9
|
+
const timer = setTimeout(() => controller.abort(new Error('timeout')), ms)
|
|
10
|
+
const abort = () => controller.abort()
|
|
11
|
+
if (upstream !== undefined && upstream !== null) {
|
|
12
|
+
if (upstream.aborted) abort()
|
|
13
|
+
else upstream.addEventListener('abort', abort, { once: true })
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
signal: controller.signal,
|
|
17
|
+
dispose: () => {
|
|
18
|
+
clearTimeout(timer)
|
|
19
|
+
if (upstream !== undefined && upstream !== null) upstream.removeEventListener?.('abort', abort)
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A registry-backed spec: `name` or `name@range`, never a git/tarball/local source. */
|
|
25
|
+
export function registryNameOf(spec) {
|
|
26
|
+
if (typeof spec !== 'string') return null
|
|
27
|
+
const text = spec.trim()
|
|
28
|
+
if (text === '') return null
|
|
29
|
+
if (/^(?:github:|git\+|git@|https?:|file:|link:|npm:|workspace:|\.{1,2}[\\/])/i.test(text)) return null
|
|
30
|
+
const at = text.indexOf('@', text.startsWith('@') ? 1 : 0)
|
|
31
|
+
const name = at === -1 ? text : text.slice(0, at)
|
|
32
|
+
const range = at === -1 ? '' : text.slice(at + 1)
|
|
33
|
+
if (name === '' || /\s/.test(name)) return null
|
|
34
|
+
if (range !== '' && (/\s/.test(range) || range.includes(':'))) return null
|
|
35
|
+
return { name, range }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `github:owner/repo#ref` → `{ owner, repo, ref }`. */
|
|
39
|
+
export function gitHubTargetOf(spec) {
|
|
40
|
+
if (typeof spec !== 'string') return null
|
|
41
|
+
const text = spec.trim()
|
|
42
|
+
const match = /^github:([^/#\s]+)\/([^/#\s]+)(?:#([^\s]+))?$/i.exec(text)
|
|
43
|
+
?? /^git\+(?:https?|ssh):\/\/(?:[^@/]+@)?github\.com\/([^/#\s]+)\/([^/#\s]+?)(?:\.git)?(?:#([^\s]+))?$/i.exec(text)
|
|
44
|
+
if (match === null) return null
|
|
45
|
+
return {
|
|
46
|
+
owner: match[1],
|
|
47
|
+
repo: match[2].replace(/\.git$/i, ''),
|
|
48
|
+
ref: match[3] ?? '',
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function cleanVersion(value) {
|
|
53
|
+
if (typeof value !== 'string') return ''
|
|
54
|
+
const text = value.trim().replace(/^v/i, '')
|
|
55
|
+
return parseVersion(text) === null ? '' : text
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A semver prerelease such as `5.18.0-beta.1` is not a stable release. */
|
|
59
|
+
export function isPrereleaseVersion(value) {
|
|
60
|
+
const parsed = parseVersion(typeof value === 'string' ? value.trim().replace(/^v/i, '') : value)
|
|
61
|
+
return parsed !== null && Array.isArray(parsed.prerelease) && parsed.prerelease.length > 0
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A version carrying build metadata such as `2.0.0+win` or `1.0.0+linux-x64`.
|
|
66
|
+
*
|
|
67
|
+
* Semver treats `+…` as build metadata, so `2.0.0+win` compares equal to
|
|
68
|
+
* `2.0.0`. Repositories use it for per-platform builds, which are not the
|
|
69
|
+
* project's newest release and can be older than the current line — one real
|
|
70
|
+
* repo shipped `v2.0.0+win` a month before `v0.4.2`. They never win the
|
|
71
|
+
* newest-release pick.
|
|
72
|
+
*/
|
|
73
|
+
export function isPlatformVersion(value) {
|
|
74
|
+
const text = typeof value === 'string' ? value.trim().replace(/^v/i, '') : ''
|
|
75
|
+
return text.includes('+')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The moment a release went public (or a tag was created), as epoch ms. */
|
|
79
|
+
function releaseTime(entry) {
|
|
80
|
+
for (const key of ['published_at', 'created_at', 'updated_at']) {
|
|
81
|
+
const value = entry?.[key]
|
|
82
|
+
if (typeof value === 'string' && !Number.isNaN(Date.parse(value))) return Date.parse(value)
|
|
83
|
+
}
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Pick the newest version out of a list of tag names. */
|
|
88
|
+
export function newestTag(tags) {
|
|
89
|
+
let best = ''
|
|
90
|
+
for (const tag of Array.isArray(tags) ? tags : []) {
|
|
91
|
+
const version = cleanVersion(typeof tag === 'string' ? tag : tag?.name)
|
|
92
|
+
if (version === '' || !/^\d/.test(version)) continue
|
|
93
|
+
if (best === '' || compareVersions(version, best) > 0) best = version
|
|
94
|
+
}
|
|
95
|
+
return best
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** `git+https://github.com/owner/repo.git` (or a bare owner/repo) → GitHub target. */
|
|
99
|
+
export function gitHubTargetOfRepository(repository) {
|
|
100
|
+
const raw = typeof repository === 'string'
|
|
101
|
+
? repository
|
|
102
|
+
: typeof repository === 'object' && repository !== null && typeof repository.url === 'string'
|
|
103
|
+
? repository.url
|
|
104
|
+
: ''
|
|
105
|
+
const text = raw.trim()
|
|
106
|
+
if (text === '') return null
|
|
107
|
+
const shorthand = /^([\w.-]+)\/([\w.-]+)$/.exec(text)
|
|
108
|
+
if (shorthand !== null) return { owner: shorthand[1], repo: shorthand[2].replace(/\.git$/i, ''), ref: '' }
|
|
109
|
+
const match = /^(?:git\+)?(?:https?|ssh|git):\/\/(?:[^@/]+@)?github\.com\/([^/#\s]+)\/([^/#\s]+?)(?:\.git)?(?:#([^\s]+))?$/i.exec(text)
|
|
110
|
+
if (match === null) return null
|
|
111
|
+
return { owner: match[1], repo: match[2].replace(/\.git$/i, ''), ref: match[3] ?? '' }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function createVersionChecker({
|
|
115
|
+
fetchImpl = globalThis.fetch,
|
|
116
|
+
npmBaseUrl = DEFAULT_NPM,
|
|
117
|
+
githubBaseUrl = GITHUB_API,
|
|
118
|
+
token = '',
|
|
119
|
+
timeoutMs = 15_000,
|
|
120
|
+
onReleaseFallback = null,
|
|
121
|
+
} = {}) {
|
|
122
|
+
async function getJson(url, headers, upstream) {
|
|
123
|
+
const { signal, dispose } = timeoutSignal(timeoutMs, upstream)
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetchImpl(url, {
|
|
126
|
+
headers: { accept: 'application/json', 'user-agent': 'dsh-plugin-syncup/2.8.0', ...headers },
|
|
127
|
+
signal,
|
|
128
|
+
})
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
const hint = response.status === 404 ? 'not-found' : response.status === 401 || response.status === 403 ? 'auth' : 'http'
|
|
131
|
+
return { ok: false, code: hint, status: response.status }
|
|
132
|
+
}
|
|
133
|
+
return { ok: true, body: await response.json() }
|
|
134
|
+
} catch (error) {
|
|
135
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
136
|
+
return { ok: false, code: /abort/i.test(message) ? 'timeout' : 'network', message }
|
|
137
|
+
} finally {
|
|
138
|
+
dispose()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Read the newest published version and, when the registry exposes it, the
|
|
144
|
+
* source repository.
|
|
145
|
+
*
|
|
146
|
+
* The compact `install-v1` document omits `repository`, so a package that ships
|
|
147
|
+
* newer git tags than npm releases cannot be judged from it. Full metadata is
|
|
148
|
+
* asked for instead; the version fields are identical in both documents.
|
|
149
|
+
*/
|
|
150
|
+
async function npmLatest(name, upstream) {
|
|
151
|
+
const url = `${npmBaseUrl.replace(/\/+$/, '')}/${name.replace('/', '%2F')}`
|
|
152
|
+
const result = await getJson(url, { accept: 'application/json' }, upstream)
|
|
153
|
+
if (!result.ok) return { source: 'npm', name, latest: null, ...result }
|
|
154
|
+
const latest = cleanVersion(result.body?.['dist-tags']?.latest)
|
|
155
|
+
const repository = gitHubTargetOfRepository(
|
|
156
|
+
result.body?.repository ?? result.body?.versions?.[result.body?.['dist-tags']?.latest]?.repository,
|
|
157
|
+
)
|
|
158
|
+
if (latest === '') {
|
|
159
|
+
const versions = Object.keys(result.body?.versions ?? {})
|
|
160
|
+
const best = newestTag(versions)
|
|
161
|
+
return best === ''
|
|
162
|
+
? { source: 'npm', name, latest: null, ok: false, code: 'no-version', ...(repository === null ? {} : { repository }) }
|
|
163
|
+
: { source: 'npm', name, latest: best, ok: true, ...(repository === null ? {} : { repository }) }
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
source: 'npm',
|
|
167
|
+
name,
|
|
168
|
+
latest,
|
|
169
|
+
ok: true,
|
|
170
|
+
distTags: result.body?.['dist-tags'] ?? {},
|
|
171
|
+
...(repository === null ? {} : { repository }),
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function githubHeaders() {
|
|
176
|
+
const headers = { 'x-github-api-version': '2022-11-28' }
|
|
177
|
+
if (token !== '') headers.authorization = `Bearer ${token}`
|
|
178
|
+
return headers
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Pick the newest stable version out of GitHub release rows.
|
|
183
|
+
*
|
|
184
|
+
* Two independent ideas of "newest" exist here and the caller chooses:
|
|
185
|
+
*
|
|
186
|
+
* - by version (`prefer: 'version'`, the default): highest semver.
|
|
187
|
+
* - by release date (`prefer: 'date'`): the release published last. A repo can
|
|
188
|
+
* publish a high-numbered build early and then continue a lower line, so the
|
|
189
|
+
* highest number is not the newest code — `v2.0.0+win` (2026-09) versus
|
|
190
|
+
* `v0.4.2` (2026-10) is exactly that case.
|
|
191
|
+
*
|
|
192
|
+
* Drafts are never installable. A version is treated as a prerelease when GitHub
|
|
193
|
+
* flags it or when the version string itself carries a prerelease suffix, so a
|
|
194
|
+
* mistimed flag cannot hide a real stable release and a `-beta` tag cannot slip
|
|
195
|
+
* through. Platform builds (`+win`) are always skipped.
|
|
196
|
+
*/
|
|
197
|
+
function newestRelease(rows, { includePrerelease = false, prefer = 'version', includePlatform = false } = {}) {
|
|
198
|
+
let best = null
|
|
199
|
+
for (const entry of Array.isArray(rows) ? rows : []) {
|
|
200
|
+
if (entry?.draft === true) continue
|
|
201
|
+
const raw = typeof entry?.tag_name === 'string' ? entry.tag_name : ''
|
|
202
|
+
const version = cleanVersion(raw)
|
|
203
|
+
if (version === '' || !/^\d/.test(version)) continue
|
|
204
|
+
if (includePlatform === false && isPlatformVersion(raw)) continue
|
|
205
|
+
const flagged = entry?.prerelease === true
|
|
206
|
+
if (includePrerelease === false && (flagged || isPrereleaseVersion(version))) continue
|
|
207
|
+
|
|
208
|
+
const at = releaseTime(entry)
|
|
209
|
+
if (best === null) {
|
|
210
|
+
best = { latest: version, latestTag: raw === '' ? `v${version}` : raw, prerelease: flagged, publishedAt: at }
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
// Date ordering needs timestamps on both sides. When either is missing
|
|
214
|
+
// (a tag list carries none), fall back to the version so it still resolves.
|
|
215
|
+
const newer = prefer === 'date' && at !== null && best.publishedAt !== null
|
|
216
|
+
? at > best.publishedAt
|
|
217
|
+
: compareVersions(version, best.latest) > 0
|
|
218
|
+
if (newer) {
|
|
219
|
+
best = { latest: version, latestTag: raw === '' ? `v${version}` : raw, prerelease: flagged, publishedAt: at }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return best
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Read a repository's newest installable version.
|
|
227
|
+
*
|
|
228
|
+
* A git tag is not a release, and the two lists routinely disagree: a repo can
|
|
229
|
+
* carry `v5.18.0` with no matching release, or platform tags like `v2.0.0+win`
|
|
230
|
+
* that are far ahead of the newest published release. Releases are therefore
|
|
231
|
+
* authoritative.
|
|
232
|
+
*
|
|
233
|
+
* The tag fallback runs only when the release list was fetched successfully and
|
|
234
|
+
* is genuinely empty. A failed request (rate limit, network, auth) must NOT
|
|
235
|
+
* look like "no releases" — that would silently downgrade the answer to a tag,
|
|
236
|
+
* which is exactly the bug this guards against. Such a failure is reported as an
|
|
237
|
+
* error and the caller keeps whatever the other source said.
|
|
238
|
+
*/
|
|
239
|
+
async function gitHubLatestRef(target, upstream, { includePrerelease = false, prefer = 'date' } = {}) {
|
|
240
|
+
const repoUrl = `${githubBaseUrl}/repos/${target.owner}/${target.repo}`
|
|
241
|
+
|
|
242
|
+
const releases = await getJson(`${repoUrl}/releases?per_page=100`, githubHeaders(), upstream)
|
|
243
|
+
if (releases.ok && Array.isArray(releases.body)) {
|
|
244
|
+
const best = newestRelease(releases.body, { includePrerelease, prefer })
|
|
245
|
+
if (best !== null) {
|
|
246
|
+
const match = releases.body.find((item) => cleanVersion(item?.tag_name) === best.latest)
|
|
247
|
+
return {
|
|
248
|
+
source: 'release',
|
|
249
|
+
latest: best.latest,
|
|
250
|
+
latestTag: best.latestTag,
|
|
251
|
+
sha: typeof match?.target_commitish === 'string' && /^[0-9a-f]{40}$/i.test(match.target_commitish)
|
|
252
|
+
? match.target_commitish
|
|
253
|
+
: null,
|
|
254
|
+
ok: true,
|
|
255
|
+
prerelease: best.prerelease,
|
|
256
|
+
...(best.publishedAt === null
|
|
257
|
+
? {}
|
|
258
|
+
: { publishedAt: new Date(best.publishedAt).toISOString() }),
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// The release list was read and holds no stable release. Only now may a
|
|
262
|
+
// tag stand in for it.
|
|
263
|
+
if (releases.body.length > 0 && typeof onReleaseFallback === 'function') {
|
|
264
|
+
onReleaseFallback(target, { reason: 'no-stable-release' })
|
|
265
|
+
}
|
|
266
|
+
} else if (releases.ok === false) {
|
|
267
|
+
// Do not silently fall back to tags on a failed read.
|
|
268
|
+
return {
|
|
269
|
+
source: 'github',
|
|
270
|
+
latest: null,
|
|
271
|
+
ok: false,
|
|
272
|
+
code: releases.code ?? 'release-read-failed',
|
|
273
|
+
...(releases.status === undefined ? {} : { status: releases.status }),
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// No stable release exists: a tagged commit is still better than nothing.
|
|
278
|
+
const tags = await getJson(`${repoUrl}/tags?per_page=100`, githubHeaders(), upstream)
|
|
279
|
+
if (tags.ok && Array.isArray(tags.body)) {
|
|
280
|
+
const rows = tags.body.map((item) => ({ tag_name: item?.name, prerelease: false }))
|
|
281
|
+
const best = newestRelease(rows, { includePrerelease, prefer })
|
|
282
|
+
if (best !== null) {
|
|
283
|
+
const match = tags.body.find((item) => cleanVersion(item?.name) === best.latest)
|
|
284
|
+
return {
|
|
285
|
+
source: 'tag',
|
|
286
|
+
latest: best.latest,
|
|
287
|
+
latestTag: best.latestTag,
|
|
288
|
+
sha: typeof match?.commit?.sha === 'string' ? match.commit.sha : null,
|
|
289
|
+
ok: true,
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Nothing tag-shaped: report the default-branch head rather than guessing.
|
|
295
|
+
const repo = await getJson(repoUrl, githubHeaders(), upstream)
|
|
296
|
+
if (!repo.ok) return { source: 'github', latest: null, ...repo }
|
|
297
|
+
const branch = typeof repo.body?.default_branch === 'string' ? repo.body.default_branch : 'main'
|
|
298
|
+
const head = await getJson(`${repoUrl}/commits/${encodeURIComponent(branch)}`, githubHeaders(), upstream)
|
|
299
|
+
if (!head.ok) return { source: 'github', latest: null, ...head }
|
|
300
|
+
const sha = typeof head.body?.sha === 'string' ? head.body.sha : null
|
|
301
|
+
return sha === null
|
|
302
|
+
? { source: 'github', latest: null, ok: false, code: 'no-version' }
|
|
303
|
+
: { source: 'github', latest: sha.slice(0, 12), sha, branch, ok: true }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Ask both package channels what their newest installable version is.
|
|
308
|
+
*
|
|
309
|
+
* The automatic answer favors the newer of npm and the repository's newest
|
|
310
|
+
* **release** (a tie stays on npm, which installs more reliably). That guess is
|
|
311
|
+
* not always what the user wants — one channel can lead the other, and a
|
|
312
|
+
* platform build can be newer than either — so each channel's own answer is
|
|
313
|
+
* reported as `channels` for the page to offer as an explicit choice.
|
|
314
|
+
*/
|
|
315
|
+
async function latestFor(spec, { upstream } = {}) {
|
|
316
|
+
const npm = registryNameOf(spec)
|
|
317
|
+
if (npm !== null) {
|
|
318
|
+
const published = await npmLatest(npm.name, upstream)
|
|
319
|
+
const repo = published.repository ?? null
|
|
320
|
+
if (repo === null) {
|
|
321
|
+
return {
|
|
322
|
+
...published,
|
|
323
|
+
spec: spec.trim(),
|
|
324
|
+
kind: 'npm',
|
|
325
|
+
channels: { npm: published.latest ?? null, github: null },
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const fromGitHub = await gitHubLatestRef(repo, upstream)
|
|
330
|
+
const npmVersion = cleanVersion(published.latest)
|
|
331
|
+
const gitVersion = cleanVersion(fromGitHub.latest)
|
|
332
|
+
const channels = { npm: published.latest ?? null, github: fromGitHub.latest ?? null }
|
|
333
|
+
const githubInfo = {
|
|
334
|
+
githubLatest: fromGitHub.latest,
|
|
335
|
+
githubSource: fromGitHub.source,
|
|
336
|
+
...(fromGitHub.latestTag === undefined ? {} : { latestTag: fromGitHub.latestTag }),
|
|
337
|
+
...(fromGitHub.publishedAt === undefined ? {} : { githubPublishedAt: fromGitHub.publishedAt }),
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Both sides comparable: only a strictly newer GitHub version wins, so a
|
|
341
|
+
// tie stays on npm.
|
|
342
|
+
if (npmVersion !== '' && gitVersion !== '') {
|
|
343
|
+
const gitNewer = compareVersions(gitVersion, npmVersion) > 0
|
|
344
|
+
return {
|
|
345
|
+
...published,
|
|
346
|
+
spec: spec.trim(),
|
|
347
|
+
kind: 'npm',
|
|
348
|
+
source: gitNewer ? fromGitHub.source : 'npm',
|
|
349
|
+
latest: gitNewer ? fromGitHub.latest : published.latest,
|
|
350
|
+
npmLatest: published.latest,
|
|
351
|
+
channels,
|
|
352
|
+
...githubInfo,
|
|
353
|
+
...(gitNewer && fromGitHub.sha ? { sha: fromGitHub.sha } : {}),
|
|
354
|
+
...(gitNewer && fromGitHub.publishedAt !== undefined ? { publishedAt: fromGitHub.publishedAt } : {}),
|
|
355
|
+
ok: true,
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// The repository answer is only a fallback when npm gave nothing usable.
|
|
359
|
+
if (npmVersion === '' && gitVersion !== '') {
|
|
360
|
+
return {
|
|
361
|
+
...published,
|
|
362
|
+
spec: spec.trim(),
|
|
363
|
+
kind: 'npm',
|
|
364
|
+
source: fromGitHub.source,
|
|
365
|
+
latest: fromGitHub.latest,
|
|
366
|
+
npmLatest: published.latest,
|
|
367
|
+
channels,
|
|
368
|
+
...githubInfo,
|
|
369
|
+
...(fromGitHub.sha ? { sha: fromGitHub.sha } : {}),
|
|
370
|
+
...(fromGitHub.publishedAt === undefined ? {} : { publishedAt: fromGitHub.publishedAt }),
|
|
371
|
+
ok: true,
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
...published,
|
|
376
|
+
spec: spec.trim(),
|
|
377
|
+
kind: 'npm',
|
|
378
|
+
channels,
|
|
379
|
+
...(gitVersion === '' ? {} : githubInfo),
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const git = gitHubTargetOf(spec)
|
|
383
|
+
if (git !== null) {
|
|
384
|
+
const found = await gitHubLatestRef(git, upstream)
|
|
385
|
+
return {
|
|
386
|
+
...found,
|
|
387
|
+
spec: spec.trim(),
|
|
388
|
+
kind: 'github',
|
|
389
|
+
ref: git.ref,
|
|
390
|
+
channels: { npm: null, github: found.latest ?? null },
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
source: null,
|
|
395
|
+
spec: typeof spec === 'string' ? spec.trim() : '',
|
|
396
|
+
kind: 'other',
|
|
397
|
+
latest: null,
|
|
398
|
+
channels: { npm: null, github: null },
|
|
399
|
+
ok: false,
|
|
400
|
+
code: 'unsupported',
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function checkAll(entries, { upstream } = {}) {
|
|
405
|
+
const items = Array.isArray(entries) ? entries.slice(0, MAX_CHECK) : []
|
|
406
|
+
const results = []
|
|
407
|
+
for (const item of items) {
|
|
408
|
+
if (upstream?.aborted === true) break
|
|
409
|
+
const found = await latestFor(item.spec, { upstream })
|
|
410
|
+
const current = cleanVersion(item.version)
|
|
411
|
+
const latestVersion = cleanVersion(found.latest)
|
|
412
|
+
const comparable = current !== '' && latestVersion !== ''
|
|
413
|
+
results.push({
|
|
414
|
+
name: item.name,
|
|
415
|
+
spec: found.spec,
|
|
416
|
+
kind: found.kind,
|
|
417
|
+
current,
|
|
418
|
+
latest: found.latest,
|
|
419
|
+
// Per-source versions and the winning source must survive this mapping:
|
|
420
|
+
// the page needs them to offer npm and GitHub as separate choices, and
|
|
421
|
+
// without them every channel option was disabled.
|
|
422
|
+
...(found.source === undefined ? {} : { source: found.source }),
|
|
423
|
+
...(found.channels === undefined ? {} : { channels: found.channels }),
|
|
424
|
+
...(found.repository === undefined ? {} : { repository: found.repository }),
|
|
425
|
+
...(found.publishedAt === undefined ? {} : { publishedAt: found.publishedAt }),
|
|
426
|
+
...(found.npmLatest === undefined ? {} : { npmLatest: found.npmLatest }),
|
|
427
|
+
...(found.githubLatest === undefined ? {} : { githubLatest: found.githubLatest }),
|
|
428
|
+
...(found.latestTag === undefined ? {} : { latestTag: found.latestTag }),
|
|
429
|
+
...(found.sha === undefined ? {} : { sha: found.sha }),
|
|
430
|
+
...(found.ok === false ? { error: found.code ?? 'lookup-failed' } : {}),
|
|
431
|
+
...(comparable
|
|
432
|
+
? { outdated: compareVersions(latestVersion, current) > 0 }
|
|
433
|
+
: found.sha !== undefined && typeof item.version === 'string' && found.sha !== null
|
|
434
|
+
? { outdated: !found.sha.startsWith(item.version) && item.version.length < 40 }
|
|
435
|
+
: {}),
|
|
436
|
+
...(found.distTags === undefined ? {} : { distTags: found.distTags }),
|
|
437
|
+
})
|
|
438
|
+
}
|
|
439
|
+
return results
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return { latestFor, checkAll }
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function isRegistrySpec(spec) {
|
|
446
|
+
return isRecord({ spec }) && registryNameOf(spec) !== null
|
|
447
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-syncup",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "4.1.0",
|
|
4
|
+
"description": "Sync plugin inventories through a secret GitHub Gist and update every plugin to its newest release, with conflict-aware preview and an install log.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"icon": "icon.svg",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./client": "./client.js",
|
|
10
|
+
"./locale/*.json": "./locale/*.json",
|
|
11
|
+
"./icon": "./icon.svg",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"index.js",
|
|
16
|
+
"client.js",
|
|
17
|
+
"lib/*.js",
|
|
18
|
+
"cordis.patch.yml",
|
|
19
|
+
"locale/*.json",
|
|
20
|
+
"icon.svg",
|
|
21
|
+
"README.md",
|
|
22
|
+
"README.en.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"dsh": {
|
|
26
|
+
"bundle": {
|
|
27
|
+
"patch": "./cordis.patch.yml"
|
|
28
|
+
},
|
|
29
|
+
"client": {
|
|
30
|
+
"platform": "web"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"deepseek-harness",
|
|
38
|
+
"dsh",
|
|
39
|
+
"plugin",
|
|
40
|
+
"sync",
|
|
41
|
+
"github"
|
|
42
|
+
],
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/jijijitian/dsh-plugin-syncup.git"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/jijijitian/dsh-plugin-syncup",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/jijijitian/dsh-plugin-syncup/issues"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"test": "node --test"
|
|
54
|
+
}
|
|
6
55
|
}
|